+7
-2
@@ -90,5 +90,6 @@ /** | ||
| } | ||
| import { c, CROSS, dim } from './ui.js'; | ||
| import { c, dim, spinner } from './ui.js'; | ||
| /** `otterkit requests <subdomain>` - print stored history. */ | ||
| export async function printHistory(subdomain, opts) { | ||
| const spin = opts.json ? null : await spinner('Fetching stored history…'); | ||
| let data; | ||
@@ -108,6 +109,10 @@ try { | ||
| else { | ||
| console.log(`\n ${CROSS} ${err.message}\n`); | ||
| spin?.stop(err.message, true); | ||
| console.log(''); | ||
| } | ||
| process.exit(1); | ||
| } | ||
| spin?.stop(data.requests.length === 0 | ||
| ? dim('No stored requests') | ||
| : `${c.bold}${data.requests.length}${c.reset} stored request${data.requests.length === 1 ? '' : 's'}${data.hasMore ? ` ${dim('· more available')}` : ''}`); | ||
| if (opts.json) { | ||
@@ -114,0 +119,0 @@ console.log(JSON.stringify(data, null, 2)); |
+17
-14
@@ -43,3 +43,3 @@ /** | ||
| } | ||
| import { c, CHECK, CROSS, dim } from './ui.js'; | ||
| import { c, CHECK, CROSS, dim, label, spinner } from './ui.js'; | ||
| async function api(path, body, token) { | ||
@@ -103,5 +103,5 @@ const resp = await fetch(`${API_SERVER}${path}`, { | ||
| console.log(''); | ||
| console.log(` ${dim('Waiting for approval in your browser…')}`); | ||
| openBrowser(verification_uri_complete); | ||
| } | ||
| const approvalSpin = json ? null : await spinner('Waiting for approval in your browser…'); | ||
| const deadline = Date.now() + expires_in * 1000; | ||
@@ -126,4 +126,3 @@ const intervalMs = Math.max(1, interval) * 1000; | ||
| } | ||
| console.log(''); | ||
| console.log(` ${CHECK} ${c.bold}Logged in.${c.reset} Agents on this machine can now provision tunnels.`); | ||
| approvalSpin?.stop(`${c.bold}Logged in.${c.reset} Agents on this machine can now provision tunnels.`); | ||
| console.log(` ${dim('Check your balance with')} ${c.bold}otterkit balance${c.reset}`); | ||
@@ -138,3 +137,4 @@ console.log(''); | ||
| } | ||
| console.log(`\n ${CROSS} Login was denied.\n`); | ||
| approvalSpin?.stop('Login was denied.', true); | ||
| console.log(''); | ||
| process.exit(1); | ||
@@ -147,3 +147,4 @@ } | ||
| } | ||
| console.log(`\n ${CROSS} Login code expired. Run ${c.bold}otterkit login${c.reset} again.\n`); | ||
| approvalSpin?.stop(`Login code expired. Run ${c.bold}otterkit login${c.reset} again.`, true); | ||
| console.log(''); | ||
| process.exit(1); | ||
@@ -156,3 +157,4 @@ } | ||
| } | ||
| console.log(`\n ${CROSS} Login timed out. Run ${c.bold}otterkit login${c.reset} again.\n`); | ||
| approvalSpin?.stop(`Login timed out. Run ${c.bold}otterkit login${c.reset} again.`, true); | ||
| console.log(''); | ||
| process.exit(1); | ||
@@ -180,2 +182,3 @@ } | ||
| } | ||
| const spin = json ? null : await spinner('Fetching account…'); | ||
| const me = await api('/api/me', undefined, token); | ||
@@ -187,5 +190,7 @@ if (!me.ok || !me.data) { | ||
| } | ||
| console.log(`\n ${CROSS} ${me.error || 'Could not fetch account'}\n`); | ||
| spin?.stop(me.error || 'Could not fetch account', true); | ||
| console.log(''); | ||
| process.exit(1); | ||
| } | ||
| spin?.stop(`Signed in as ${c.bold}${me.data.email || '(no email)'}${c.reset}`); | ||
| const creds = loadCredentials(); | ||
@@ -217,2 +222,3 @@ if (json) { | ||
| } | ||
| const spin = json ? null : await spinner('Fetching balance…'); | ||
| const res = await api('/api/me/balance', undefined, token); | ||
@@ -224,3 +230,4 @@ if (!res.ok || !res.data) { | ||
| } | ||
| console.log(`\n ${CROSS} ${res.error || 'Could not fetch balance'}\n`); | ||
| spin?.stop(res.error || 'Could not fetch balance', true); | ||
| console.log(''); | ||
| process.exit(1); | ||
@@ -232,8 +239,4 @@ } | ||
| } | ||
| spin?.stop(`${c.bold}${res.data.balance}${c.reset} credits ${dim(`($${(res.data.balance / 100).toFixed(2)})`)}`); | ||
| console.log(''); | ||
| console.log(` ${c.bold}${res.data.balance}${c.reset} credits ${dim(`($${(res.data.balance / 100).toFixed(2)})`)}`); | ||
| console.log(''); | ||
| } | ||
| function label(text) { | ||
| return `${c.gray}${text}${c.reset}`; | ||
| } |
+9
-26
@@ -18,25 +18,3 @@ /** | ||
| import { getProvider } from './providers.js'; | ||
| /* ── ANSI helpers (mirrors index.ts) ── */ | ||
| const c = { | ||
| reset: '\x1b[0m', | ||
| bold: '\x1b[1m', | ||
| green: '\x1b[32m', | ||
| cyan: '\x1b[36m', | ||
| yellow: '\x1b[33m', | ||
| red: '\x1b[31m', | ||
| gray: '\x1b[90m', | ||
| white: '\x1b[97m', | ||
| }; | ||
| const CHECK = `${c.green}✓${c.reset}`; | ||
| const CROSS = `${c.red}✗${c.reset}`; | ||
| const dim = (s) => `${c.gray}${s}${c.reset}`; | ||
| function statusBadge(code) { | ||
| if (code < 300) | ||
| return `${c.green}${code}${c.reset}`; | ||
| if (code < 400) | ||
| return `${c.cyan}${code}${c.reset}`; | ||
| if (code < 500) | ||
| return `${c.yellow}${code}${c.reset}`; | ||
| return `${c.red}${code}${c.reset}`; | ||
| } | ||
| import { c, CROSS, dim, statusBadge, spinner } from './ui.js'; | ||
| // Connection-level headers that must not be copied onto a fresh request. | ||
@@ -274,2 +252,3 @@ const SKIP_HEADERS = new Set([ | ||
| export async function replayCmd(subdomain, options) { | ||
| const spin = options.json ? null : await spinner('Replaying capture…'); | ||
| let result; | ||
@@ -281,3 +260,8 @@ try { | ||
| if (err instanceof ReplayError) { | ||
| console.log(`\n ${CROSS} ${c.bold}${err.message}${c.reset}`); | ||
| if (options.json) { | ||
| console.log(`\n ${CROSS} ${c.bold}${err.message}${c.reset}`); | ||
| } | ||
| else { | ||
| spin?.stop(`${c.bold}${err.message}${c.reset}`, true); | ||
| } | ||
| if (err.hint) | ||
@@ -302,5 +286,4 @@ console.log(` ${dim(err.hint)}`); | ||
| : Buffer.alloc(0); | ||
| spin?.stop(`${c.bold}Replayed${c.reset} request #${result.index} ${dim(`of ${result.total}`)} ${c.white}${result.method}${c.reset} ${result.path}`); | ||
| console.log(''); | ||
| console.log(` ${CHECK} ${c.bold}Replayed${c.reset} request #${result.index} ${dim(`of ${result.total}`)} ${c.white}${result.method}${c.reset} ${result.path}`); | ||
| console.log(''); | ||
| console.log(` ${dim('Target')} ${c.white}${result.target}${c.reset}`); | ||
@@ -307,0 +290,0 @@ console.log(` ${dim('Response')} ${statusBadge(result.status)} ${dim(`${result.durationMs}ms · ${respBody.length}B`)}`); |
+23
-20
@@ -12,14 +12,3 @@ /** | ||
| const API_SERVER = process.env.OTTERKIT_API_URL || 'https://api.otterkit.com'; | ||
| /* ── ANSI helpers (mirrors index.ts) ── */ | ||
| const c = { | ||
| reset: '\x1b[0m', | ||
| bold: '\x1b[1m', | ||
| green: '\x1b[32m', | ||
| cyan: '\x1b[36m', | ||
| red: '\x1b[31m', | ||
| gray: '\x1b[90m', | ||
| }; | ||
| const CHECK = `${c.green}✓${c.reset}`; | ||
| const CROSS = `${c.red}✗${c.reset}`; | ||
| const dim = (s) => `${c.gray}${s}${c.reset}`; | ||
| import { c, dim, spinner } from './ui.js'; | ||
| async function api(method, path, body) { | ||
@@ -56,2 +45,3 @@ const token = getToken(); | ||
| export async function listSubdomains(json) { | ||
| const spin = json ? null : await spinner('Fetching reserved subdomains…'); | ||
| const { env } = await api('GET', '/api/me/subdomains'); | ||
@@ -63,3 +53,4 @@ if (!env.ok || !env.data) { | ||
| } | ||
| console.log(`\n ${CROSS} ${env.error || 'Could not fetch subdomains'}\n`); | ||
| spin?.stop(env.error || 'Could not fetch subdomains', true); | ||
| console.log(''); | ||
| process.exit(1); | ||
@@ -73,2 +64,5 @@ } | ||
| } | ||
| spin?.stop(list.length === 0 | ||
| ? dim('No reserved subdomains') | ||
| : `${c.bold}${list.length}${c.reset} reserved subdomain${list.length === 1 ? '' : 's'} ${dim('· up to 10 per workspace')}`); | ||
| console.log(''); | ||
@@ -92,2 +86,3 @@ if (list.length === 0) { | ||
| export async function reserveSubdomainCmd(name, label) { | ||
| const spin = await spinner(`Reserving ${name}…`); | ||
| const { status, env } = await api('POST', '/api/me/subdomains', { | ||
@@ -98,27 +93,35 @@ name, | ||
| if (status === 409) { | ||
| console.log(`\n ${CROSS} ${env.message || `Subdomain "${name}" is not available.`}\n`); | ||
| spin.stop(env.message || `Subdomain "${name}" is not available.`, true); | ||
| console.log(''); | ||
| process.exit(1); | ||
| } | ||
| if (status === 400) { | ||
| console.log(`\n ${CROSS} ${env.message || env.error || 'Invalid subdomain'}\n`); | ||
| spin.stop(env.message || env.error || 'Invalid subdomain', true); | ||
| console.log(''); | ||
| process.exit(1); | ||
| } | ||
| if (!env.ok) { | ||
| console.log(`\n ${CROSS} ${env.error || 'Could not reserve subdomain'}\n`); | ||
| spin.stop(env.error || 'Could not reserve subdomain', true); | ||
| console.log(''); | ||
| process.exit(1); | ||
| } | ||
| console.log(`\n ${CHECK} Reserved ${c.bold}${c.cyan}${name}.${tunnelHost()}${c.reset}\n`); | ||
| spin.stop(`Reserved ${c.bold}${c.cyan}${name}.${tunnelHost()}${c.reset}`); | ||
| console.log(''); | ||
| } | ||
| /** `otterkit subdomains release <name>` - release a reserved subdomain. */ | ||
| export async function releaseSubdomainCmd(name) { | ||
| const spin = await spinner(`Releasing ${name}…`); | ||
| const { status, env } = await api('DELETE', `/api/me/subdomains/${encodeURIComponent(name)}`); | ||
| if (status === 404) { | ||
| console.log(`\n ${CROSS} No reserved subdomain named ${c.bold}${name}${c.reset}\n`); | ||
| spin.stop(`No reserved subdomain named ${c.bold}${name}${c.reset}`, true); | ||
| console.log(''); | ||
| process.exit(1); | ||
| } | ||
| if (!env.ok) { | ||
| console.log(`\n ${CROSS} ${env.error || 'Could not release subdomain'}\n`); | ||
| spin.stop(env.error || 'Could not release subdomain', true); | ||
| console.log(''); | ||
| process.exit(1); | ||
| } | ||
| console.log(`\n ${CHECK} Released ${c.bold}${name}${c.reset}\n`); | ||
| spin.stop(`Released ${c.bold}${name}${c.reset}`); | ||
| console.log(''); | ||
| } |
+5
-0
@@ -29,2 +29,7 @@ export declare const c: { | ||
| export declare function printBanner(): void; | ||
| /** | ||
| * The big banner: hand-drawn ASCII wordmark, shown only where users ask to | ||
| * see the CLI itself (bare `otterkit`, `--help`) - never in command flows. | ||
| */ | ||
| export declare function bigBanner(version: string): string; | ||
| export interface Spin { | ||
@@ -31,0 +36,0 @@ message(text: string): void; |
+17
-1
@@ -59,5 +59,21 @@ /** | ||
| console.log(''); | ||
| console.log(` ${BRAND} ${dim('- instant tunnels & webhooks')}`); | ||
| console.log(` ${pc.green('◉')} ${BRAND} ${dim('- instant tunnels & webhooks')}`); | ||
| console.log(''); | ||
| } | ||
| /** | ||
| * The big banner: hand-drawn ASCII wordmark, shown only where users ask to | ||
| * see the CLI itself (bare `otterkit`, `--help`) - never in command flows. | ||
| */ | ||
| export function bigBanner(version) { | ||
| const art = [ | ||
| ' _ _ _ _ _', | ||
| ' ___ | |_| |_ ___ _ __| | _(_) |_', | ||
| " / _ \\| __| __/ _ \\ '__| |/ /| | __|", | ||
| '| (_) | |_| || __/ | | < | | |_', | ||
| ' \\___/ \\__|\\__\\___|_| |_|\\_\\|_|\\__|', | ||
| ] | ||
| .map(line => pc.green(line)) | ||
| .join('\n'); | ||
| return `\n${art}\n\n ${pc.bold('webhooks that never sleep · tunnels in one command')}\n ${dim(`v${version} · https://www.otterkit.com/docs`)}\n`; | ||
| } | ||
| export async function spinner(text) { | ||
@@ -64,0 +80,0 @@ if (!process.stdout.isTTY || process.env.CI) { |
+11
-14
@@ -11,16 +11,3 @@ /** | ||
| import { initPayment } from './provision.js'; | ||
| /* ── ANSI helpers (mirrors index.ts) ── */ | ||
| const c = { | ||
| reset: '\x1b[0m', | ||
| bold: '\x1b[1m', | ||
| green: '\x1b[32m', | ||
| cyan: '\x1b[36m', | ||
| yellow: '\x1b[33m', | ||
| red: '\x1b[31m', | ||
| gray: '\x1b[90m', | ||
| white: '\x1b[97m', | ||
| }; | ||
| const CHECK = `${c.green}✓${c.reset}`; | ||
| const CROSS = `${c.red}✗${c.reset}`; | ||
| const dim = (s) => `${c.gray}${s}${c.reset}`; | ||
| import { c, CHECK, CROSS, dim, spinner } from './ui.js'; | ||
| function findRunning(daemons, profile) { | ||
@@ -58,4 +45,8 @@ return daemons.find(d => d.profile === profile.name || (profile.subdomain && d.subdomain === profile.subdomain)); | ||
| } | ||
| const spin = options.json | ||
| ? null | ||
| : await spinner(`Starting ${profiles.length} profile${profiles.length === 1 ? '' : 's'}…`); | ||
| const results = []; | ||
| for (const profile of profiles) { | ||
| spin?.message(`Starting ${profile.name}…`); | ||
| const running = findRunning(cleanupDaemons(), profile); | ||
@@ -115,2 +106,8 @@ if (running) { | ||
| else { | ||
| const started = results.filter(r => r.status === 'started').length; | ||
| const errored = results.filter(r => r.status === 'error').length; | ||
| spin?.stop(errored > 0 | ||
| ? `${started} started, ${errored} failed` | ||
| : `${started} started${results.length - started > 0 ? ` ${dim(`· ${results.length - started} already running`)}` : ''}`, errored > 0); | ||
| console.log(''); | ||
| for (const r of results) { | ||
@@ -117,0 +114,0 @@ if (r.status === 'error') { |
+1
-1
| { | ||
| "name": "otterkit", | ||
| "version": "0.46.0", | ||
| "version": "0.47.0", | ||
| "description": "OtterKit CLI - provision and connect tunnels for AI agents", | ||
@@ -5,0 +5,0 @@ "mcpName": "io.github.useotterkit/otterkit", |
Sorry, the diff of this file is too big to display
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
305422
0.96%7053
0.69%32
3.23%