@sleepwalkerai/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js ADDED
@@ -0,0 +1,1173 @@
1
+ import readline from "node:readline";
2
+ import net from "node:net";
3
+ import {
4
+ configPath,
5
+ getApiBaseUrlSource,
6
+ getApiKeySource,
7
+ maskApiKey,
8
+ readConfig,
9
+ writeConfig,
10
+ } from "./config.js";
11
+ import { createApiClient, SleepwalkerApiError } from "./http.js";
12
+ import {
13
+ flagBool,
14
+ flagList,
15
+ flagNumber,
16
+ flagString,
17
+ parseFlags,
18
+ readLinesFromFile,
19
+ } from "./args.js";
20
+ import { printJson, printKeyValue, printList, printNextCommands, printRunSummary } from "./format.js";
21
+ import { createTheme, renderCommandsHelp, renderHelp, sanitizeTerminalText, styleStatus } from "./theme.js";
22
+
23
+ const VERSION = "0.1.0";
24
+ const DEFAULT_POLL_INTERVAL_MS = 5000;
25
+ const DEFAULT_MAX_WAIT_SECONDS = 900;
26
+ const TERMINAL_RUN_STATUSES = new Set([
27
+ "completed",
28
+ "partially_completed",
29
+ "failed",
30
+ "cancelled",
31
+ ]);
32
+
33
+ const PLATFORM_CHOICES = [
34
+ { label: "ChatGPT", value: "openai" },
35
+ { label: "Perplexity", value: "perplexity" },
36
+ { label: "Grok", value: "grok" },
37
+ { label: "Gemini", value: "gemini" },
38
+ ];
39
+
40
+ function makeError(message, exitCode = 1) {
41
+ const error = new Error(message);
42
+ error.exitCode = exitCode;
43
+ return error;
44
+ }
45
+
46
+ function requireArg(value, message) {
47
+ if (!value) {
48
+ throw makeError(message);
49
+ }
50
+ return value;
51
+ }
52
+
53
+ function urlArg(args, flags, usage) {
54
+ return requireArg(args[0] || flagString(flags, "url", ""), usage);
55
+ }
56
+
57
+ function splitCsvValues(values) {
58
+ return values
59
+ .flatMap((value) => String(value).split(","))
60
+ .map((value) => value.trim())
61
+ .filter(Boolean);
62
+ }
63
+
64
+ function promptsFromFlags(flags) {
65
+ const prompts = splitCsvValues(flagList(flags, "prompt"));
66
+ const promptFile = flagString(flags, "prompt-file", "");
67
+ if (promptFile) {
68
+ prompts.push(...readLinesFromFile(promptFile));
69
+ }
70
+ return prompts;
71
+ }
72
+
73
+ function createIdempotencyKey(prefix) {
74
+ const random = globalThis.crypto && typeof globalThis.crypto.randomUUID === "function"
75
+ ? globalThis.crypto.randomUUID()
76
+ : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
77
+ return `cli:${prefix}:${random}`;
78
+ }
79
+
80
+ function interactiveIdempotencyKey(io, prefix) {
81
+ if (typeof io.idempotencyKeyFactory === "function") {
82
+ return io.idempotencyKeyFactory(prefix);
83
+ }
84
+ return createIdempotencyKey(prefix);
85
+ }
86
+
87
+ function shortPreview(value, maxChars = 700) {
88
+ const text = sanitizeTerminalText(value).trim();
89
+ if (!text) {
90
+ return "";
91
+ }
92
+ if (text.length <= maxChars) {
93
+ return text;
94
+ }
95
+ return `${text.slice(0, maxChars).trimEnd()}\n...`;
96
+ }
97
+
98
+ function shellQuote(value) {
99
+ const text = sanitizeTerminalText(value);
100
+ if (!text) {
101
+ return "''";
102
+ }
103
+ return `'${text.replace(/'/g, "'\\''")}'`;
104
+ }
105
+
106
+ function isLoopbackHost(hostname) {
107
+ const value = String(hostname || "").toLowerCase().replace(/^\[|\]$/g, "");
108
+ if (value === "localhost" || value === "::1") {
109
+ return true;
110
+ }
111
+ if (net.isIP(value) === 4) {
112
+ return value.split(".")[0] === "127";
113
+ }
114
+ return false;
115
+ }
116
+
117
+ function firstDefined(...values) {
118
+ return values.find((value) => value !== undefined && value !== null && value !== "");
119
+ }
120
+
121
+ function hasNonZeroCreditValue(value) {
122
+ if (value === undefined || value === null || value === "") {
123
+ return false;
124
+ }
125
+ const numeric = Number(value);
126
+ return Number.isNaN(numeric) || numeric !== 0;
127
+ }
128
+
129
+ function billingDetailRows(payload, theme) {
130
+ const billing = payload && typeof payload.billing === "object" ? payload.billing : {};
131
+ const released = firstDefined(payload?.released_credits, billing.released_credits);
132
+ return [
133
+ ["Estimated credits", firstDefined(payload?.estimated_credits, billing.estimated_credits) ? theme.info(firstDefined(payload?.estimated_credits, billing.estimated_credits)) : ""],
134
+ ["Reserved credits", firstDefined(payload?.reserved_credits, billing.reserved_credits) ? theme.warning(firstDefined(payload?.reserved_credits, billing.reserved_credits)) : ""],
135
+ ["Settled credits", firstDefined(payload?.settled_credits, billing.settled_credits) ? theme.accent(firstDefined(payload?.settled_credits, billing.settled_credits)) : ""],
136
+ ["Released credits", hasNonZeroCreditValue(released) ? theme.info(released) : ""],
137
+ ["Billing", firstDefined(payload?.billing_status, billing.status) ? styleStatus(theme, firstDefined(payload?.billing_status, billing.status)) : ""],
138
+ ];
139
+ }
140
+
141
+ function readSetupState(io) {
142
+ try {
143
+ const config = readConfig(io.env);
144
+ const keyInfo = getApiKeySource(io.env, config);
145
+ const baseInfo = getApiBaseUrlSource(io.env, config);
146
+ return {
147
+ hasApiKey: Boolean(keyInfo.key),
148
+ apiKeySource: keyInfo.source,
149
+ apiBaseUrl: baseInfo.value,
150
+ configPath: configPath(io.env),
151
+ configError: "",
152
+ };
153
+ } catch (error) {
154
+ return {
155
+ hasApiKey: false,
156
+ apiKeySource: "missing",
157
+ apiBaseUrl: "https://api.sleepwalker.ai",
158
+ configPath: configPath(io.env),
159
+ configError: error && error.message ? error.message : String(error),
160
+ };
161
+ }
162
+ }
163
+
164
+ function isInteractiveSession(io, flags) {
165
+ return Boolean(
166
+ io.stdin &&
167
+ io.stdin.isTTY &&
168
+ io.stdout &&
169
+ io.stdout.isTTY &&
170
+ !flagBool(flags, "json"),
171
+ );
172
+ }
173
+
174
+ function withRawMode(stdin, enabled) {
175
+ if (stdin && typeof stdin.setRawMode === "function") {
176
+ stdin.setRawMode(enabled);
177
+ }
178
+ }
179
+
180
+ function askLine(io, question) {
181
+ if (io.stdin && typeof io.stdin.resume === "function") {
182
+ io.stdin.resume();
183
+ }
184
+ const rl = readline.createInterface({
185
+ input: io.stdin,
186
+ output: io.stdout,
187
+ });
188
+ return new Promise((resolve) => {
189
+ rl.question(question, (answer) => {
190
+ rl.close();
191
+ resolve(String(answer || "").trim());
192
+ });
193
+ });
194
+ }
195
+
196
+ async function askRequired(io, question) {
197
+ for (;;) {
198
+ const value = await askLine(io, question);
199
+ if (value) {
200
+ return value;
201
+ }
202
+ io.stdout.write(`${io.theme.warning("Required.")} Please enter a value.\n`);
203
+ }
204
+ }
205
+
206
+ async function askConfirm(io, question) {
207
+ const value = (await askLine(io, `${question} ${io.theme.muted("[y/N]")} `)).toLowerCase();
208
+ return value === "y" || value === "yes";
209
+ }
210
+
211
+ function writeMenu(stdout, theme, title, options, selectedIndex) {
212
+ stdout.write(`${theme.accent(title)}\n\n`);
213
+ options.forEach((option, index) => {
214
+ const marker = index === selectedIndex ? theme.accent("›") : " ";
215
+ const label = index === selectedIndex ? theme.bold(option.label) : option.label;
216
+ const detail = option.detail ? ` ${theme.muted(`- ${option.detail}`)}` : "";
217
+ stdout.write(`${marker} ${label}${detail}\n`);
218
+ });
219
+ stdout.write(`\n${theme.muted("Use ↑/↓, Enter to select, q to quit.")}\n`);
220
+ }
221
+
222
+ function selectOption(io, title, options) {
223
+ const stdin = io.stdin;
224
+ const stdout = io.stdout;
225
+ const theme = io.theme;
226
+ let selectedIndex = 0;
227
+ let rendered = false;
228
+
229
+ return new Promise((resolve) => {
230
+ const cleanup = (value) => {
231
+ stdin.off("keypress", onKeypress);
232
+ withRawMode(stdin, false);
233
+ if (typeof stdin.pause === "function") {
234
+ stdin.pause();
235
+ }
236
+ stdout.write("\u001b[?25h");
237
+ stdout.write("\n");
238
+ resolve(value);
239
+ };
240
+
241
+ const render = () => {
242
+ if (rendered) {
243
+ readline.moveCursor(stdout, 0, -(options.length + 4));
244
+ readline.clearScreenDown(stdout);
245
+ } else {
246
+ stdout.write("\u001b[?25l");
247
+ rendered = true;
248
+ }
249
+ writeMenu(stdout, theme, title, options, selectedIndex);
250
+ };
251
+
252
+ const onKeypress = (_chunk, key = {}) => {
253
+ if (key.name === "up") {
254
+ selectedIndex = (selectedIndex - 1 + options.length) % options.length;
255
+ render();
256
+ return;
257
+ }
258
+ if (key.name === "down") {
259
+ selectedIndex = (selectedIndex + 1) % options.length;
260
+ render();
261
+ return;
262
+ }
263
+ if (key.name === "return" || key.name === "enter") {
264
+ cleanup(options[selectedIndex]);
265
+ return;
266
+ }
267
+ if (key.name === "q" || (key.ctrl && key.name === "c")) {
268
+ cleanup(null);
269
+ }
270
+ };
271
+
272
+ readline.emitKeypressEvents(stdin);
273
+ if (typeof stdin.resume === "function") {
274
+ stdin.resume();
275
+ }
276
+ stdin.on("keypress", onKeypress);
277
+ withRawMode(stdin, true);
278
+ render();
279
+ });
280
+ }
281
+
282
+ function normalizeApiBaseUrl(value) {
283
+ const raw = String(value || "").trim();
284
+ if (!raw) {
285
+ throw makeError("Usage: sleepwalker config set api-base-url <url>");
286
+ }
287
+ let parsed;
288
+ try {
289
+ parsed = new URL(raw);
290
+ } catch {
291
+ throw makeError(`Invalid API base URL: ${raw}`);
292
+ }
293
+ if (!["https:", "http:"].includes(parsed.protocol)) {
294
+ throw makeError("API base URL must start with https:// or http://.");
295
+ }
296
+ if (parsed.protocol === "http:" && !isLoopbackHost(parsed.hostname)) {
297
+ throw makeError("HTTP API base URLs are only allowed for localhost or loopback addresses. Use https:// for remote API hosts.");
298
+ }
299
+ return parsed.toString().replace(/\/+$/, "");
300
+ }
301
+
302
+ async function sleep(ms, io) {
303
+ if (typeof io.sleep === "function") {
304
+ await io.sleep(ms);
305
+ return;
306
+ }
307
+ await new Promise((resolve) => setTimeout(resolve, ms));
308
+ }
309
+
310
+ function output(stdout, flags, value, humanPrinter) {
311
+ if (flagBool(flags, "json")) {
312
+ printJson(stdout, value);
313
+ return;
314
+ }
315
+ humanPrinter(value);
316
+ }
317
+
318
+ function formatTestType(value) {
319
+ if (value === "ai_citations") {
320
+ return "AI Visibility";
321
+ }
322
+ if (value === "content_intelligence") {
323
+ return "Content Intelligence";
324
+ }
325
+ return value || "Report";
326
+ }
327
+
328
+ async function pollUntilDone({
329
+ client,
330
+ io,
331
+ stdout,
332
+ flags,
333
+ path,
334
+ query,
335
+ type,
336
+ theme,
337
+ }) {
338
+ const intervalMs = Math.max(1000, flagNumber(flags, "poll-interval-ms", DEFAULT_POLL_INTERVAL_MS));
339
+ const maxWaitSeconds = Math.max(1, flagNumber(flags, "max-wait-seconds", DEFAULT_MAX_WAIT_SECONDS));
340
+ const maxWaitMs = maxWaitSeconds * 1000;
341
+ let elapsedMs = 0;
342
+ const quiet = flagBool(flags, "quiet");
343
+ let lastStatus = "";
344
+ let lastPayload = null;
345
+
346
+ for (;;) {
347
+ if (lastPayload && elapsedMs >= maxWaitMs) {
348
+ const id = lastPayload.run_id || lastPayload.id || "";
349
+ const hintCommand = type === "Content Intelligence" ? "sleepwalker ci status" : "sleepwalker visibility status";
350
+ const error = makeError(`${type} run ${id || "unknown"} is still ${lastStatus || "unknown"} after ${maxWaitSeconds}s. Check later with \`${hintCommand} ${id || "<run_id>"}\`.`);
351
+ error.payload = {
352
+ timed_out: true,
353
+ run_id: id || null,
354
+ last_status: lastStatus || null,
355
+ last_payload: lastPayload,
356
+ };
357
+ throw error;
358
+ }
359
+
360
+ const payload = await client.get(path, { query });
361
+ lastPayload = payload;
362
+ const status = String(payload.status || "").toLowerCase();
363
+ if (!quiet && status !== lastStatus && !flagBool(flags, "json")) {
364
+ const id = payload.run_id || payload.id || "";
365
+ stdout.write(`${theme.muted(type)} run ${theme.id(id)} is ${styleStatus(theme, status || "unknown")}\n`);
366
+ }
367
+ lastStatus = status;
368
+ if (TERMINAL_RUN_STATUSES.has(status)) {
369
+ return payload;
370
+ }
371
+ await sleep(intervalMs, io);
372
+ elapsedMs += intervalMs;
373
+ }
374
+ }
375
+
376
+ async function handleConfig(args, flags, io) {
377
+ const theme = io.theme;
378
+ const command = args[0];
379
+ const config = readConfig(io.env);
380
+
381
+ if (!command || command === "show") {
382
+ const keyInfo = getApiKeySource(io.env, config);
383
+ const baseInfo = getApiBaseUrlSource(io.env, config);
384
+ const payload = {
385
+ config_path: configPath(io.env),
386
+ api_base_url: baseInfo.value,
387
+ api_base_url_source: baseInfo.source,
388
+ api_key_source: keyInfo.source,
389
+ api_key: keyInfo.key ? maskApiKey(keyInfo.key) : null,
390
+ };
391
+ output(io.stdout, flags, payload, (data) => {
392
+ printKeyValue(io.stdout, [
393
+ ["Config file", theme.id(data.config_path)],
394
+ ["API base", `${theme.info(data.api_base_url)} ${theme.muted(`(${data.api_base_url_source})`)}`],
395
+ ["API key", data.api_key ? `${theme.id(data.api_key)} ${theme.muted(`(${data.api_key_source})`)}` : theme.warning("not configured")],
396
+ ], theme);
397
+ });
398
+ return;
399
+ }
400
+
401
+ if (command === "set" && args[1] === "api-base-url") {
402
+ const apiBaseUrl = normalizeApiBaseUrl(args[2]);
403
+ writeConfig({ ...config, apiBaseUrl }, io.env);
404
+ io.stdout.write(`${theme.accent("Stored")} API base URL ${theme.id(apiBaseUrl)} locally.\n`);
405
+ return;
406
+ }
407
+
408
+ if (command === "clear" && args[1] === "api-base-url") {
409
+ const next = { ...config };
410
+ delete next.apiBaseUrl;
411
+ writeConfig(next, io.env);
412
+ io.stdout.write(`${theme.warning("Removed")} stored API base URL override.\n`);
413
+ return;
414
+ }
415
+
416
+ throw makeError("Usage: sleepwalker config show | config set api-base-url <url> | config clear api-base-url");
417
+ }
418
+
419
+ async function handleInit(flags, io) {
420
+ const theme = io.theme;
421
+ const state = readSetupState(io);
422
+ const payload = {
423
+ configured: state.hasApiKey && !state.configError,
424
+ config_path: state.configPath,
425
+ api_base_url: state.apiBaseUrl,
426
+ api_key_source: state.apiKeySource,
427
+ config_error: state.configError || null,
428
+ next_commands: state.hasApiKey
429
+ ? [
430
+ "sleepwalker doctor",
431
+ "sleepwalker reports by-url https://www.sleepwalker.ai",
432
+ "sleepwalker commands",
433
+ ]
434
+ : [
435
+ "sleepwalker auth key set sw_api_live_...",
436
+ "sleepwalker doctor",
437
+ "sleepwalker reports by-url https://www.sleepwalker.ai",
438
+ ],
439
+ };
440
+
441
+ output(io.stdout, flags, payload, (data) => {
442
+ io.stdout.write(`${theme.accent("Sleepwalker CLI setup")}\n\n`);
443
+ printKeyValue(io.stdout, [
444
+ ["Config file", theme.id(data.config_path)],
445
+ ["API base", theme.info(data.api_base_url)],
446
+ ["API key", data.configured ? `${theme.accent("configured")} ${theme.muted(`(${data.api_key_source})`)}` : theme.warning("not configured")],
447
+ ], theme);
448
+
449
+ if (data.config_error) {
450
+ io.stdout.write(`\n${theme.warning("Config issue")}\n${data.config_error}\n`);
451
+ }
452
+
453
+ io.stdout.write(`\n${theme.accent("Next steps")}\n`);
454
+ if (data.configured) {
455
+ io.stdout.write(` 1. ${theme.command("sleepwalker doctor")} ${theme.muted("checks the live API connection and credits.")}\n`);
456
+ io.stdout.write(` 2. ${theme.command("sleepwalker reports by-url https://www.sleepwalker.ai")} ${theme.muted("runs a safe read-only check.")}\n`);
457
+ io.stdout.write(` 3. ${theme.command("sleepwalker commands")} ${theme.muted("shows the full command reference.")}\n`);
458
+ } else {
459
+ io.stdout.write(` 1. Open ${theme.info("https://app.sleepwalker.ai")} and go to ${theme.accent("API")}.\n`);
460
+ io.stdout.write(` 2. Generate a new API key.\n`);
461
+ io.stdout.write(` 3. ${theme.command("sleepwalker auth key set sw_api_live_...")}\n`);
462
+ io.stdout.write(` 4. ${theme.command("sleepwalker doctor")}\n`);
463
+ io.stdout.write(` 5. ${theme.command("sleepwalker reports by-url https://www.sleepwalker.ai")} ${theme.muted("runs a safe read-only check.")}\n`);
464
+ }
465
+ });
466
+ }
467
+
468
+ async function pauseForMenu(io) {
469
+ await askLine(io, `\n${io.theme.muted("Press Enter to return to the menu.")}`);
470
+ }
471
+
472
+ async function runMenuAction(label, io, action) {
473
+ try {
474
+ io.stdout.write(`\n${io.theme.accent(label)}\n`);
475
+ await action();
476
+ } catch (error) {
477
+ const message = error && error.message ? error.message : String(error);
478
+ io.stdout.write(`\n${io.theme.error("Error")} ${message}\n`);
479
+ }
480
+ await pauseForMenu(io);
481
+ }
482
+
483
+ async function handleInteractiveMenu(flags, io) {
484
+ if (!isInteractiveSession(io, flags)) {
485
+ throw makeError("Interactive menu requires a terminal. Run `sleepwalker --help` or `sleepwalker commands` instead.");
486
+ }
487
+
488
+ for (;;) {
489
+ const state = readSetupState(io);
490
+ const options = state.hasApiKey
491
+ ? [
492
+ { label: "Check connection", value: "doctor", detail: "account, API, credits" },
493
+ { label: "Show credits", value: "credits" },
494
+ { label: "Serialize a page", value: "page_serialize", detail: "clean content for agents" },
495
+ { label: "Run AI Visibility", value: "visibility_run", detail: "prompt + platform" },
496
+ { label: "Score content", value: "ci_score", detail: "quick Content Intelligence" },
497
+ { label: "Run Content Intelligence", value: "ci_run", detail: "saved run" },
498
+ { label: "Find reports by URL", value: "reports_by_url" },
499
+ { label: "Command reference", value: "commands" },
500
+ { label: "Clear stored API key", value: "clear_key" },
501
+ { label: "Exit", value: "exit" },
502
+ ]
503
+ : [
504
+ { label: "Enter API key", value: "set_key", detail: "paste sw_api_live_..." },
505
+ { label: "Setup checklist", value: "init", detail: "where to generate a key" },
506
+ { label: "Command reference", value: "commands" },
507
+ { label: "Exit", value: "exit" },
508
+ ];
509
+
510
+ const selected = await selectOption(io, "Sleepwalker CLI", options);
511
+ if (!selected || selected.value === "exit") {
512
+ io.stdout.write(`${io.theme.muted("Bye.")}\n`);
513
+ return;
514
+ }
515
+
516
+ if (selected.value === "set_key") {
517
+ await runMenuAction("Store API key", io, async () => {
518
+ const key = await askRequired(io, "Paste your Sleepwalker API key: ");
519
+ await handleAuth(["key", "set", key], {}, io);
520
+ await handleDoctor({}, io);
521
+ });
522
+ continue;
523
+ }
524
+
525
+ if (selected.value === "init") {
526
+ await runMenuAction("Setup checklist", io, async () => handleInit({}, io));
527
+ continue;
528
+ }
529
+
530
+ if (selected.value === "commands") {
531
+ await runMenuAction("Command reference", io, async () => {
532
+ io.stdout.write(renderCommandsHelp(io.theme));
533
+ });
534
+ continue;
535
+ }
536
+
537
+ if (selected.value === "clear_key") {
538
+ await runMenuAction("Clear stored API key", io, async () => handleAuth(["key", "clear"], {}, io));
539
+ continue;
540
+ }
541
+
542
+ if (selected.value === "doctor") {
543
+ await runMenuAction("Connection check", io, async () => handleDoctor({}, io));
544
+ continue;
545
+ }
546
+
547
+ if (selected.value === "credits") {
548
+ await runMenuAction("Credits", io, async () => handleUsage({}, io));
549
+ continue;
550
+ }
551
+
552
+ if (selected.value === "page_serialize") {
553
+ await runMenuAction("Page serialization", io, async () => {
554
+ const url = await askRequired(io, "URL: ");
555
+ await handlePage(["serialize", url], {}, io);
556
+ });
557
+ continue;
558
+ }
559
+
560
+ if (selected.value === "visibility_run") {
561
+ await runMenuAction("AI Visibility run", io, async () => {
562
+ const url = await askRequired(io, "URL: ");
563
+ const brand = await askRequired(io, "Brand / target entity: ");
564
+ const prompt = await askRequired(io, "Prompt: ");
565
+ const platform = await selectOption(io, "Choose platform", PLATFORM_CHOICES);
566
+ if (!platform) {
567
+ io.stdout.write(`${io.theme.warning("Cancelled.")}\n`);
568
+ return;
569
+ }
570
+ const watch = await askConfirm(io, "Watch until the run finishes?");
571
+ const confirmed = await askConfirm(io, "Queue this run now? It can use prepaid credits.");
572
+ if (!confirmed) {
573
+ io.stdout.write(`${io.theme.warning("Cancelled.")}\n`);
574
+ return;
575
+ }
576
+ await handleVisibility(["run", url], {
577
+ brand,
578
+ prompt,
579
+ platform: platform.value,
580
+ watch,
581
+ "idempotency-key": interactiveIdempotencyKey(io, "visibility-run"),
582
+ }, io);
583
+ });
584
+ continue;
585
+ }
586
+
587
+ if (selected.value === "ci_score") {
588
+ await runMenuAction("Content score", io, async () => {
589
+ const url = await askRequired(io, "URL: ");
590
+ const confirmed = await askConfirm(io, "Score this page now? It can use prepaid credits.");
591
+ if (!confirmed) {
592
+ io.stdout.write(`${io.theme.warning("Cancelled.")}\n`);
593
+ return;
594
+ }
595
+ await handleCi(["score", url], {}, io);
596
+ });
597
+ continue;
598
+ }
599
+
600
+ if (selected.value === "ci_run") {
601
+ await runMenuAction("Content Intelligence run", io, async () => {
602
+ const url = await askRequired(io, "URL: ");
603
+ const depth = await selectOption(io, "Choose analysis depth", [
604
+ { label: "Full", value: "full", detail: "saved run with recommendations" },
605
+ { label: "Score", value: "score", detail: "lighter saved run" },
606
+ ]);
607
+ if (!depth) {
608
+ io.stdout.write(`${io.theme.warning("Cancelled.")}\n`);
609
+ return;
610
+ }
611
+ const watch = await askConfirm(io, "Watch until the run finishes?");
612
+ const confirmed = await askConfirm(io, "Queue this run now? It can use prepaid credits.");
613
+ if (!confirmed) {
614
+ io.stdout.write(`${io.theme.warning("Cancelled.")}\n`);
615
+ return;
616
+ }
617
+ await handleCi(["run", url], {
618
+ depth: depth.value,
619
+ watch,
620
+ "idempotency-key": interactiveIdempotencyKey(io, "content-run"),
621
+ }, io);
622
+ });
623
+ continue;
624
+ }
625
+
626
+ if (selected.value === "reports_by_url") {
627
+ await runMenuAction("Reports by URL", io, async () => {
628
+ const url = await askRequired(io, "URL: ");
629
+ await handleReports(["by-url", url], {}, io);
630
+ });
631
+ }
632
+ }
633
+ }
634
+
635
+ async function handleDoctor(flags, io) {
636
+ const theme = io.theme;
637
+ const config = readConfig(io.env);
638
+ const keyInfo = getApiKeySource(io.env, config);
639
+ const baseInfo = getApiBaseUrlSource(io.env, config);
640
+ const client = createApiClient({ env: io.env, fetchImpl: io.fetch });
641
+ const checks = [
642
+ {
643
+ name: "API base",
644
+ status: "ok",
645
+ detail: `${baseInfo.value} (${baseInfo.source})`,
646
+ },
647
+ {
648
+ name: "API key",
649
+ status: keyInfo.key ? "ok" : "missing",
650
+ detail: keyInfo.key ? `${maskApiKey(keyInfo.key)} (${keyInfo.source})` : "Set SLEEPWALKER_API_KEY or run `sleepwalker auth key set <key>`.",
651
+ },
652
+ ];
653
+ let usage = null;
654
+
655
+ if (keyInfo.key) {
656
+ try {
657
+ usage = await client.get("/v1/usage", { query: { recent_limit: 1 } });
658
+ checks.push({
659
+ name: "API reachability",
660
+ status: "ok",
661
+ detail: "authenticated",
662
+ });
663
+ } catch (error) {
664
+ checks.push({
665
+ name: "API reachability",
666
+ status: "error",
667
+ detail: error instanceof SleepwalkerApiError ? `${error.status || "HTTP"} ${error.message}` : error.message,
668
+ });
669
+ }
670
+ }
671
+
672
+ const ready = checks.every((check) => check.status === "ok");
673
+ const credits = usage && usage.credits ? usage.credits : {};
674
+ const payload = {
675
+ ready,
676
+ api_base_url: baseInfo.value,
677
+ api_base_url_source: baseInfo.source,
678
+ api_key_source: keyInfo.source,
679
+ api_key: keyInfo.key ? maskApiKey(keyInfo.key) : null,
680
+ credits: usage ? credits : null,
681
+ checks,
682
+ };
683
+
684
+ output(io.stdout, flags, payload, (data) => {
685
+ io.stdout.write(`${data.ready ? theme.accent("Sleepwalker CLI is ready.") : theme.warning("Sleepwalker CLI needs attention.")}\n\n`);
686
+ printList(io.stdout, data.checks, (check) => {
687
+ const marker = check.status === "ok" ? theme.accent("ok") : theme.warning(check.status);
688
+ return `${marker} ${check.name}: ${check.detail}`;
689
+ });
690
+ if (data.credits) {
691
+ io.stdout.write("\n");
692
+ printKeyValue(io.stdout, [
693
+ ["Available credits", theme.accent(data.credits.available_credit_units || "0.00")],
694
+ ["Used credits", theme.info(data.credits.used_credit_units || "0.00")],
695
+ ], theme);
696
+ }
697
+ if (data.ready) {
698
+ printNextCommands(io.stdout, [
699
+ "sleepwalker reports by-url https://www.sleepwalker.ai",
700
+ "sleepwalker commands",
701
+ ], theme);
702
+ io.stdout.write(`\n${theme.muted("If an action fails with a scope error, generate a new key in Console > API.")}\n`);
703
+ }
704
+ });
705
+
706
+ if (!ready) {
707
+ throw makeError("Sleepwalker CLI doctor found setup issues.");
708
+ }
709
+ }
710
+
711
+ async function handleAuth(args, flags, io) {
712
+ const theme = io.theme;
713
+ const subcommand = args[0];
714
+ const nested = args[1];
715
+ if (subcommand === "key" && nested === "set") {
716
+ const key = requireArg(args[2], "Usage: sleepwalker auth key set <api_key>");
717
+ const config = readConfig(io.env);
718
+ writeConfig({ ...config, apiKey: key }, io.env);
719
+ io.stdout.write(`${theme.accent("Stored")} API key ${theme.id(maskApiKey(key))} locally.\n`);
720
+ return;
721
+ }
722
+ if (subcommand === "key" && nested === "show") {
723
+ const config = readConfig(io.env);
724
+ const envKey = io.env.SLEEPWALKER_API_KEY;
725
+ const key = envKey || config.apiKey || "";
726
+ if (!key) {
727
+ io.stdout.write("No API key configured.\n");
728
+ return;
729
+ }
730
+ io.stdout.write(`${theme.id(maskApiKey(key))}${theme.muted(envKey ? " (from env)" : " (from config)")}\n`);
731
+ return;
732
+ }
733
+ if (subcommand === "key" && nested === "clear") {
734
+ const config = readConfig(io.env);
735
+ const next = { ...config };
736
+ delete next.apiKey;
737
+ writeConfig(next, io.env);
738
+ io.stdout.write(`${theme.warning("Removed")} stored API key from local config.\n`);
739
+ return;
740
+ }
741
+ if (subcommand === "login") {
742
+ io.stdout.write("Create an API key in the Sleepwalker Console, then run:\n\n");
743
+ io.stdout.write(` ${theme.command("sleepwalker auth key set sw_api_live_...")}\n\n`);
744
+ io.stdout.write(`${theme.muted("OAuth/device login is not part of this first CLI scaffold.")}\n`);
745
+ return;
746
+ }
747
+ if (subcommand === "whoami") {
748
+ const client = createApiClient({ env: io.env, fetchImpl: io.fetch });
749
+ const payload = await client.get("/v1/usage", { query: { recent_limit: 5 } });
750
+ output(io.stdout, flags, payload, (data) => {
751
+ const credits = data.credits || {};
752
+ printKeyValue(io.stdout, [
753
+ ["Account", data.email || data.user_email || data.user_id || "authenticated"],
754
+ ["Available credits", theme.accent(credits.available_credit_units || "0.00")],
755
+ ["Used credits", theme.info(credits.used_credit_units || "0.00")],
756
+ ["API base", theme.id(client.baseUrl)],
757
+ ], theme);
758
+ });
759
+ return;
760
+ }
761
+ throw makeError("Unknown auth command. Run `sleepwalker --help`.");
762
+ }
763
+
764
+ async function handleReports(args, flags, io) {
765
+ const theme = io.theme;
766
+ if (args[0] !== "by-url") {
767
+ throw makeError("Usage: sleepwalker reports by-url <url> [--type ai_citations|content_intelligence] [--days 90] [--limit 5]");
768
+ }
769
+ const url = urlArg(args.slice(1), flags, "Usage: sleepwalker reports by-url <url>");
770
+ const client = createApiClient({ env: io.env, fetchImpl: io.fetch });
771
+ const payload = await client.get("/v1/reports/by-url", {
772
+ query: {
773
+ url,
774
+ test_type: flagString(flags, "type", flagString(flags, "test-type", "")),
775
+ days: flagNumber(flags, "days", 90),
776
+ limit: flagNumber(flags, "limit", 5),
777
+ },
778
+ });
779
+
780
+ output(io.stdout, flags, payload, (data) => {
781
+ printKeyValue(io.stdout, [
782
+ ["URL", theme.info(data.url || url)],
783
+ ["Matches", data.match_count ?? (data.matches || []).length],
784
+ ["Lookback", `${data.days || 90} days`],
785
+ ], theme);
786
+ if (data.matches && data.matches.length) {
787
+ io.stdout.write("\n");
788
+ printList(io.stdout, data.matches, (match) => {
789
+ const test = match.test || {};
790
+ const runs = match.runs || [];
791
+ const latest = runs[0] || {};
792
+ const result = latest.overall_band || latest.status || "no runs";
793
+ return `${theme.id(test.id || "")} ${theme.muted(formatTestType(test.test_type))} ${sanitizeTerminalText(test.name || "Untitled")} ${theme.info(test.url || "")} ${styleStatus(theme, result)}`;
794
+ });
795
+ }
796
+ });
797
+ }
798
+
799
+ async function handleUsage(flags, io) {
800
+ const theme = io.theme;
801
+ const client = createApiClient({ env: io.env, fetchImpl: io.fetch });
802
+ const payload = await client.get("/v1/usage", {
803
+ query: { recent_limit: flagNumber(flags, "recent-limit", 20) },
804
+ });
805
+ output(io.stdout, flags, payload, (data) => {
806
+ const credits = data.credits || {};
807
+ printKeyValue(io.stdout, [
808
+ ["Available credits", theme.accent(credits.available_credit_units || "0.00")],
809
+ ["Used credits", theme.info(credits.used_credit_units || "0.00")],
810
+ ["Active grants", credits.active_grant_count ?? 0],
811
+ ], theme);
812
+ });
813
+ }
814
+
815
+ async function handleActivity(args, flags, io) {
816
+ const theme = io.theme;
817
+ if (args[0] !== "list") {
818
+ throw makeError("Usage: sleepwalker activity list [--limit 10] [--kind all]");
819
+ }
820
+ const client = createApiClient({ env: io.env, fetchImpl: io.fetch });
821
+ const payload = await client.get("/v1/activity", {
822
+ query: {
823
+ limit: flagNumber(flags, "limit", 10),
824
+ kind: flagString(flags, "kind", "all"),
825
+ },
826
+ });
827
+ output(io.stdout, flags, payload, (data) => {
828
+ printList(io.stdout, data.items || [], (item) => {
829
+ const source = item.source || "unknown";
830
+ const status = item.status || "unknown";
831
+ const action = item.action || item.kind || "request";
832
+ const credits = item.credits || item.estimated_credits || "";
833
+ return `${theme.muted(item.created_at || "")} ${theme.info(source)} ${sanitizeTerminalText(action)} ${styleStatus(theme, status)}${credits ? ` ${theme.warning(`${credits} credits`)}` : ""}`;
834
+ });
835
+ });
836
+ }
837
+
838
+ async function handleTests(args, flags, io) {
839
+ const theme = io.theme;
840
+ if (args[0] !== "list") {
841
+ throw makeError("Usage: sleepwalker tests list [--limit 20] [--type ai_citations|content_intelligence]");
842
+ }
843
+ const client = createApiClient({ env: io.env, fetchImpl: io.fetch });
844
+ const payload = await client.get("/v1/tests", {
845
+ query: {
846
+ limit: flagNumber(flags, "limit", 20),
847
+ test_type: flagString(flags, "type", ""),
848
+ },
849
+ });
850
+ output(io.stdout, flags, payload, (data) => {
851
+ printList(io.stdout, data.tests || data.items || [], (test) => {
852
+ return `${theme.id(test.id || "")} ${theme.muted(test.test_type || "")} ${sanitizeTerminalText(test.name || test.title || "Untitled")} ${theme.info(test.url || "")}`;
853
+ });
854
+ });
855
+ }
856
+
857
+ async function handlePage(args, flags, io) {
858
+ const theme = io.theme;
859
+ if (args[0] !== "serialize") {
860
+ throw makeError("Usage: sleepwalker page serialize <url>");
861
+ }
862
+ const url = urlArg(args.slice(1), flags, "Usage: sleepwalker page serialize <url>");
863
+ const client = createApiClient({ env: io.env, fetchImpl: io.fetch });
864
+ const payload = await client.post("/v1/pages/content/serialize", {
865
+ url,
866
+ extraction_mode: flagString(flags, "mode", flagString(flags, "extraction-mode", "")) || undefined,
867
+ max_chars: flagNumber(flags, "max-chars", undefined),
868
+ offset: flagNumber(flags, "offset", undefined),
869
+ });
870
+ output(io.stdout, flags, payload, (data) => {
871
+ const serialization = data.serialization || data;
872
+ if (serialization.blocked || serialization.serialization_issue || data.blocked || data.serialization_issue) {
873
+ io.stdout.write(`${sanitizeTerminalText(serialization.blocked_message || serialization.issue_message || data.blocked_message || data.issue_message || "Page could not be serialized.")}\n`);
874
+ return;
875
+ }
876
+ const contentView = serialization.content_view || "";
877
+ io.stdout.write(`${theme.accent("Page serialized")}\n\n`);
878
+ printKeyValue(io.stdout, [
879
+ ["URL", theme.info(serialization.url || url)],
880
+ ["HTTP status", serialization.http_status ? styleStatus(theme, serialization.http_status) : ""],
881
+ ["Content", `${serialization.content_view_chars ?? contentView.length} chars${serialization.content_view_truncated ? " (truncated)" : ""}`],
882
+ ...billingDetailRows(serialization, theme),
883
+ ], theme);
884
+ const preview = shortPreview(contentView);
885
+ if (preview) {
886
+ io.stdout.write(`\n${theme.accent("Preview")}\n${preview}\n`);
887
+ }
888
+ printNextCommands(io.stdout, [
889
+ `sleepwalker page serialize ${shellQuote(serialization.url || url)} --json`,
890
+ `sleepwalker reports by-url ${shellQuote(serialization.url || url)}`,
891
+ ], theme);
892
+ });
893
+ }
894
+
895
+ async function handleVisibility(args, flags, io) {
896
+ const theme = io.theme;
897
+ const command = args[0];
898
+ const client = createApiClient({ env: io.env, fetchImpl: io.fetch });
899
+
900
+ if (command === "suggest-prompts") {
901
+ const url = urlArg(args.slice(1), flags, "Usage: sleepwalker visibility suggest-prompts <url> --brand <brand>");
902
+ const brand = requireArg(flagString(flags, "brand", ""), "Missing --brand <brand>.");
903
+ const payload = await client.post("/v1/visibility/prompts/suggest", {
904
+ url,
905
+ brand_name: brand,
906
+ language: flagString(flags, "language", undefined),
907
+ country: flagString(flags, "country", undefined),
908
+ });
909
+ output(io.stdout, flags, payload, (data) => {
910
+ const prompts = data.prompts || [];
911
+ printList(io.stdout, prompts, (prompt) => `${theme.accent("-")} ${sanitizeTerminalText(prompt)}`);
912
+ if (prompts.length) {
913
+ printNextCommands(io.stdout, [
914
+ `sleepwalker visibility run ${shellQuote(url)} --brand ${shellQuote(brand)} --prompt ${shellQuote(prompts[0])} --platform perplexity`,
915
+ ], theme);
916
+ }
917
+ });
918
+ return;
919
+ }
920
+
921
+ if (command === "run") {
922
+ const url = urlArg(args.slice(1), flags, "Usage: sleepwalker visibility run <url> --brand <brand> --prompt <prompt> --platform <platform>");
923
+ const prompts = promptsFromFlags(flags);
924
+ const platforms = splitCsvValues(flagList(flags, "platform"));
925
+ if (!prompts.length) {
926
+ throw makeError("Missing --prompt <prompt> or --prompt-file <path>.");
927
+ }
928
+ if (!platforms.length) {
929
+ throw makeError("Missing --platform <platform>.");
930
+ }
931
+ const payload = await client.post("/v1/visibility/runs", {
932
+ url,
933
+ target_entity: requireArg(flagString(flags, "brand", flagString(flags, "target", "")), "Missing --brand <brand>."),
934
+ prompts,
935
+ platforms,
936
+ competitors: splitCsvValues(flagList(flags, "competitor")),
937
+ language: flagString(flags, "language", "en"),
938
+ country: flagString(flags, "country", "US"),
939
+ idempotency_key: flagString(flags, "idempotency-key", undefined),
940
+ });
941
+ if (flagBool(flags, "watch")) {
942
+ const watched = await pollUntilDone({
943
+ client,
944
+ io,
945
+ stdout: io.stdout,
946
+ flags,
947
+ path: `/v1/visibility/runs/${payload.run_id}`,
948
+ query: { include_probes: true, include_results: true },
949
+ type: "Visibility",
950
+ theme,
951
+ });
952
+ output(io.stdout, flags, watched, (data) => printRunSummary(io.stdout, data, theme.accent("AI Visibility"), theme, {
953
+ nextCommands: [
954
+ data.run_id ? `sleepwalker visibility status ${shellQuote(data.run_id)} --results` : "",
955
+ ],
956
+ }));
957
+ } else {
958
+ output(io.stdout, flags, payload, (data) => printRunSummary(io.stdout, data, theme.accent("AI Visibility"), theme, {
959
+ nextCommands: [
960
+ data.run_id ? `sleepwalker visibility status ${shellQuote(data.run_id)} --results` : "",
961
+ data.run_id ? `sleepwalker visibility status ${shellQuote(data.run_id)} --results --json` : "",
962
+ ],
963
+ }));
964
+ }
965
+ return;
966
+ }
967
+
968
+ if (command === "status") {
969
+ const runId = requireArg(args[1], "Usage: sleepwalker visibility status <run_id>");
970
+ const payload = await client.get(`/v1/visibility/runs/${runId}`, {
971
+ query: {
972
+ include_probes: flagBool(flags, "probes") || flagBool(flags, "results"),
973
+ include_results: flagBool(flags, "results"),
974
+ },
975
+ });
976
+ output(io.stdout, flags, payload, (data) => printRunSummary(io.stdout, data, theme.accent("AI Visibility"), theme, {
977
+ nextCommands: [
978
+ data.run_id ? `sleepwalker visibility status ${shellQuote(data.run_id)} --results --json` : "",
979
+ ],
980
+ }));
981
+ return;
982
+ }
983
+
984
+ if (command === "list") {
985
+ const payload = await client.get("/v1/visibility/runs", {
986
+ query: {
987
+ limit: flagNumber(flags, "limit", 20),
988
+ status: flagString(flags, "status", ""),
989
+ starting_after: flagString(flags, "starting-after", ""),
990
+ },
991
+ });
992
+ output(io.stdout, flags, payload, (data) => {
993
+ printList(io.stdout, data.runs || [], (run) => `${theme.id(run.run_id || run.id)} ${styleStatus(theme, run.status)} ${theme.info(run.url || "")}`);
994
+ });
995
+ return;
996
+ }
997
+
998
+ throw makeError("Unknown visibility command. Run `sleepwalker --help`.");
999
+ }
1000
+
1001
+ async function handleCi(args, flags, io) {
1002
+ const theme = io.theme;
1003
+ const command = args[0];
1004
+ const client = createApiClient({ env: io.env, fetchImpl: io.fetch });
1005
+
1006
+ if (command === "score") {
1007
+ const url = urlArg(args.slice(1), flags, "Usage: sleepwalker ci score <url>");
1008
+ const payload = await client.post("/v1/content-intelligence/content/score", {
1009
+ url,
1010
+ extraction_mode: flagString(flags, "mode", flagString(flags, "extraction-mode", "")) || undefined,
1011
+ industry: flagString(flags, "industry", undefined),
1012
+ language: flagString(flags, "language", undefined),
1013
+ country: flagString(flags, "country", undefined),
1014
+ });
1015
+ output(io.stdout, flags, payload, (data) => {
1016
+ if (data.content_score_issue || data.blocked || data.site_error) {
1017
+ io.stdout.write(`${sanitizeTerminalText(data.issue_message || data.blocked_message || data.site_error_message || "Content could not be scored.")}\n`);
1018
+ return;
1019
+ }
1020
+ io.stdout.write(`${theme.ci("Content score")}\n\n`);
1021
+ printKeyValue(io.stdout, [
1022
+ ["Overall", theme.ci(`${data.overall_score ?? "n/a"}${data.overall_band ? ` - ${data.overall_band}` : ""}`)],
1023
+ ["Page type", data.page_type || "unknown"],
1024
+ ["Trend source", data.trend_source || "unknown"],
1025
+ ["Freshness", data.freshness_score ? `${data.freshness_score}${data.freshness_band ? ` - ${data.freshness_band}` : ""}` : ""],
1026
+ ["Depth", data.sufficiency_score ? `${data.sufficiency_score}${data.sufficiency_band ? ` - ${data.sufficiency_band}` : ""}` : ""],
1027
+ ...billingDetailRows(data, theme),
1028
+ ], theme);
1029
+ const recommendations = data.top_recommendations || [];
1030
+ if (recommendations.length) {
1031
+ io.stdout.write(`\n${theme.ci("Top recommendations")}\n`);
1032
+ printList(io.stdout, recommendations.slice(0, 3), (item) => `${theme.ci("-")} ${sanitizeTerminalText(item)}`);
1033
+ }
1034
+ printNextCommands(io.stdout, [
1035
+ `sleepwalker ci score ${shellQuote(url)} --json`,
1036
+ `sleepwalker ci run ${shellQuote(url)} --depth full`,
1037
+ ], theme);
1038
+ });
1039
+ return;
1040
+ }
1041
+
1042
+ if (command === "run") {
1043
+ const url = urlArg(args.slice(1), flags, "Usage: sleepwalker ci run <url> [--depth score|full]");
1044
+ const payload = await client.post("/v1/content-intelligence/runs", {
1045
+ url,
1046
+ analysis_depth: flagString(flags, "depth", "full"),
1047
+ language: flagString(flags, "language", "en"),
1048
+ country: flagString(flags, "country", "US"),
1049
+ idempotency_key: flagString(flags, "idempotency-key", undefined),
1050
+ });
1051
+ if (flagBool(flags, "watch")) {
1052
+ const watched = await pollUntilDone({
1053
+ client,
1054
+ io,
1055
+ stdout: io.stdout,
1056
+ flags,
1057
+ path: `/v1/content-intelligence/runs/${payload.run_id}`,
1058
+ query: { include_result: true },
1059
+ type: "Content Intelligence",
1060
+ theme,
1061
+ });
1062
+ output(io.stdout, flags, watched, (data) => printRunSummary(io.stdout, data, theme.ci("Content Intelligence"), theme, {
1063
+ nextCommands: [
1064
+ data.run_id ? `sleepwalker ci status ${shellQuote(data.run_id)} --result` : "",
1065
+ ],
1066
+ }));
1067
+ } else {
1068
+ output(io.stdout, flags, payload, (data) => printRunSummary(io.stdout, data, theme.ci("Content Intelligence"), theme, {
1069
+ nextCommands: [
1070
+ data.run_id ? `sleepwalker ci status ${shellQuote(data.run_id)} --result` : "",
1071
+ data.run_id ? `sleepwalker ci status ${shellQuote(data.run_id)} --result --json` : "",
1072
+ ],
1073
+ }));
1074
+ }
1075
+ return;
1076
+ }
1077
+
1078
+ if (command === "status") {
1079
+ const runId = requireArg(args[1], "Usage: sleepwalker ci status <run_id>");
1080
+ const payload = await client.get(`/v1/content-intelligence/runs/${runId}`, {
1081
+ query: { include_result: flagBool(flags, "result") },
1082
+ });
1083
+ output(io.stdout, flags, payload, (data) => printRunSummary(io.stdout, data, theme.ci("Content Intelligence"), theme, {
1084
+ nextCommands: [
1085
+ data.run_id ? `sleepwalker ci status ${shellQuote(data.run_id)} --result --json` : "",
1086
+ ],
1087
+ }));
1088
+ return;
1089
+ }
1090
+
1091
+ if (command === "list") {
1092
+ const payload = await client.get("/v1/content-intelligence/runs", {
1093
+ query: {
1094
+ limit: flagNumber(flags, "limit", 20),
1095
+ status: flagString(flags, "status", ""),
1096
+ starting_after: flagString(flags, "starting-after", ""),
1097
+ },
1098
+ });
1099
+ output(io.stdout, flags, payload, (data) => {
1100
+ printList(io.stdout, data.runs || [], (run) => `${theme.id(run.run_id || run.id)} ${styleStatus(theme, run.status)} ${theme.info(run.url || "")}`);
1101
+ });
1102
+ return;
1103
+ }
1104
+
1105
+ throw makeError("Unknown ci command. Run `sleepwalker --help`.");
1106
+ }
1107
+
1108
+ export async function runCli(argv, io) {
1109
+ const { positional, flags } = parseFlags(argv);
1110
+ io.theme = createTheme({ env: io.env, stdout: io.stdout });
1111
+ const command = positional[0];
1112
+ const rest = positional.slice(1);
1113
+
1114
+ if (flagBool(flags, "version") || command === "version") {
1115
+ io.stdout.write(`${VERSION}\n`);
1116
+ return;
1117
+ }
1118
+ if (command === "commands" || flagBool(flags, "help-all") || (command === "help" && rest[0] === "commands")) {
1119
+ io.stdout.write(renderCommandsHelp(io.theme));
1120
+ return;
1121
+ }
1122
+ if ((!command && isInteractiveSession(io, flags)) || command === "menu") {
1123
+ await handleInteractiveMenu(flags, io);
1124
+ return;
1125
+ }
1126
+ if (!command || flagBool(flags, "help") || command === "help") {
1127
+ io.stdout.write(renderHelp(io.theme, readSetupState(io)));
1128
+ return;
1129
+ }
1130
+
1131
+ try {
1132
+ if (command === "init") {
1133
+ await handleInit(flags, io);
1134
+ } else if (command === "config") {
1135
+ await handleConfig(rest, flags, io);
1136
+ } else if (command === "doctor") {
1137
+ await handleDoctor(flags, io);
1138
+ } else if (command === "auth") {
1139
+ await handleAuth(rest, flags, io);
1140
+ } else if (command === "credits" || command === "usage") {
1141
+ await handleUsage(flags, io);
1142
+ } else if (command === "activity") {
1143
+ await handleActivity(rest, flags, io);
1144
+ } else if (command === "tests") {
1145
+ await handleTests(rest, flags, io);
1146
+ } else if (command === "reports") {
1147
+ await handleReports(rest, flags, io);
1148
+ } else if (command === "page") {
1149
+ await handlePage(rest, flags, io);
1150
+ } else if (command === "visibility") {
1151
+ await handleVisibility(rest, flags, io);
1152
+ } else if (command === "ci" || command === "content") {
1153
+ await handleCi(rest, flags, io);
1154
+ } else {
1155
+ throw makeError(`Unknown command: ${command}. Run \`sleepwalker --help\`.`);
1156
+ }
1157
+ } catch (error) {
1158
+ if (error instanceof SleepwalkerApiError && flagBool(flags, "json")) {
1159
+ printJson(io.stdout, {
1160
+ error: error.message,
1161
+ status: error.status,
1162
+ payload: error.payload,
1163
+ });
1164
+ } else if (error && error.payload && flagBool(flags, "json")) {
1165
+ printJson(io.stdout, error.payload);
1166
+ }
1167
+ throw error;
1168
+ }
1169
+ }
1170
+
1171
+ export async function main(argv, io) {
1172
+ await runCli(argv, io);
1173
+ }