@ishlabs/cli 0.8.1 → 0.8.2

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.
Files changed (70) hide show
  1. package/README.md +323 -21
  2. package/dist/auth.d.ts +17 -1
  3. package/dist/auth.js +62 -9
  4. package/dist/commands/ask.d.ts +5 -0
  5. package/dist/commands/ask.js +722 -0
  6. package/dist/commands/config.js +25 -1
  7. package/dist/commands/docs.d.ts +17 -0
  8. package/dist/commands/docs.js +147 -0
  9. package/dist/commands/init.d.ts +16 -0
  10. package/dist/commands/init.js +182 -0
  11. package/dist/commands/iteration.d.ts +5 -1
  12. package/dist/commands/iteration.js +243 -31
  13. package/dist/commands/profile.d.ts +5 -0
  14. package/dist/commands/profile.js +313 -0
  15. package/dist/commands/source.d.ts +10 -0
  16. package/dist/commands/source.js +78 -0
  17. package/dist/commands/study-run.d.ts +11 -0
  18. package/dist/commands/study-run.js +552 -0
  19. package/dist/commands/study-tester.d.ts +8 -0
  20. package/dist/commands/study-tester.js +149 -0
  21. package/dist/commands/study.js +145 -70
  22. package/dist/commands/workspace.js +193 -7
  23. package/dist/config.d.ts +3 -1
  24. package/dist/config.js +10 -10
  25. package/dist/connect.d.ts +4 -1
  26. package/dist/connect.js +127 -94
  27. package/dist/index.js +82 -34
  28. package/dist/lib/alias-store.d.ts +3 -0
  29. package/dist/lib/alias-store.js +9 -7
  30. package/dist/lib/api-client.d.ts +9 -6
  31. package/dist/lib/api-client.js +87 -26
  32. package/dist/lib/ask-questions.d.ts +9 -0
  33. package/dist/lib/ask-questions.js +35 -0
  34. package/dist/lib/ask-variants.d.ts +48 -0
  35. package/dist/lib/ask-variants.js +236 -0
  36. package/dist/lib/auth.d.ts +1 -1
  37. package/dist/lib/auth.js +24 -8
  38. package/dist/lib/colors.d.ts +30 -0
  39. package/dist/lib/colors.js +48 -0
  40. package/dist/lib/command-helpers.d.ts +74 -0
  41. package/dist/lib/command-helpers.js +232 -6
  42. package/dist/lib/docs.d.ts +32 -0
  43. package/dist/lib/docs.js +930 -0
  44. package/dist/lib/local-sim/browser.d.ts +0 -1
  45. package/dist/lib/local-sim/browser.js +0 -2
  46. package/dist/lib/local-sim/install.d.ts +4 -7
  47. package/dist/lib/local-sim/install.js +6 -21
  48. package/dist/lib/output.d.ts +25 -3
  49. package/dist/lib/output.js +465 -20
  50. package/dist/lib/paths.d.ts +14 -0
  51. package/dist/lib/paths.js +36 -0
  52. package/dist/lib/profile-sources.d.ts +55 -0
  53. package/dist/lib/profile-sources.js +157 -0
  54. package/dist/lib/site-access.d.ts +80 -0
  55. package/dist/lib/site-access.js +188 -0
  56. package/dist/lib/skill-content.d.ts +31 -0
  57. package/dist/lib/skill-content.js +462 -0
  58. package/dist/lib/study-inputs.d.ts +20 -0
  59. package/dist/lib/study-inputs.js +72 -0
  60. package/dist/lib/types.d.ts +207 -9
  61. package/dist/lib/types.js +7 -0
  62. package/dist/lib/upload.js +2 -2
  63. package/dist/upgrade.js +11 -1
  64. package/package.json +1 -1
  65. package/dist/commands/simulation.d.ts +0 -10
  66. package/dist/commands/simulation.js +0 -647
  67. package/dist/commands/tester-profile.d.ts +0 -5
  68. package/dist/commands/tester-profile.js +0 -109
  69. package/dist/commands/tester.d.ts +0 -5
  70. package/dist/commands/tester.js +0 -73
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Parse `--variant` flags and `--variants <manifest.json>` into AskVariantInput[],
3
+ * uploading any local files to the asks variant-upload endpoint along the way.
4
+ *
5
+ * Flag form: `--variant <kind>:<value>[::label=<label>]`
6
+ * text:"hello" → literal text
7
+ * text:@./body.md → text loaded from file
8
+ * ./logo.png → media; kind inferred from MIME
9
+ * image:./logo.png → media; kind override
10
+ * ./hero.png::label=B → media with label
11
+ *
12
+ * Manifest form (--variants ./manifest.json): JSON array of
13
+ * { kind: "text"|"image"|..., label?: string, content?: string, file?: string }
14
+ * - kind="text": `content` holds the literal text (or `file` to load text from disk).
15
+ * - kind=media: `file` is a local path to upload, or `content` is an already-uploaded file_path.
16
+ */
17
+ import { readFile, readFileSync } from "node:fs";
18
+ import { promisify } from "node:util";
19
+ import { basename, resolve as resolvePath } from "node:path";
20
+ import { detectMimeType, validateFile } from "./upload.js";
21
+ import { ASK_VARIANT_KINDS } from "./types.js";
22
+ const readFileAsync = promisify(readFile);
23
+ const KIND_BY_MIME_PREFIX = [
24
+ [/^image\//, "image"],
25
+ [/^audio\//, "audio"],
26
+ [/^video\//, "video"],
27
+ [/^application\/pdf$/, "document"],
28
+ [/^application\/(msword|vnd\.ms-|vnd\.openxmlformats-)/, "document"],
29
+ [/^text\//, "document"],
30
+ ];
31
+ function inferKindFromMime(mime) {
32
+ for (const [re, kind] of KIND_BY_MIME_PREFIX) {
33
+ if (re.test(mime))
34
+ return kind;
35
+ }
36
+ return undefined;
37
+ }
38
+ /** Strip a trailing `::label=...` suffix off a flag value, returning {value, label?}. */
39
+ function splitLabelSuffix(raw) {
40
+ const idx = raw.lastIndexOf("::");
41
+ if (idx === -1)
42
+ return { value: raw };
43
+ const tail = raw.slice(idx + 2);
44
+ const m = /^label=(.*)$/i.exec(tail);
45
+ if (!m)
46
+ return { value: raw };
47
+ return { value: raw.slice(0, idx), label: m[1] };
48
+ }
49
+ const KIND_PREFIXES = ASK_VARIANT_KINDS;
50
+ /** Parse a single `--variant` flag value. */
51
+ export function parseVariantFlag(raw) {
52
+ if (!raw || !raw.trim()) {
53
+ throw new Error("Empty --variant value.");
54
+ }
55
+ const { value, label } = splitLabelSuffix(raw);
56
+ // Check for explicit kind prefix `text:`, `image:`, etc.
57
+ let explicitKind;
58
+ let body = value;
59
+ for (const k of KIND_PREFIXES) {
60
+ if (value.startsWith(`${k}:`)) {
61
+ explicitKind = k;
62
+ body = value.slice(k.length + 1);
63
+ break;
64
+ }
65
+ }
66
+ if (explicitKind === "text") {
67
+ if (body.startsWith("@")) {
68
+ const fp = body.slice(1);
69
+ if (!fp)
70
+ throw new Error("Missing file path after `text:@`.");
71
+ let text;
72
+ try {
73
+ text = readFileSync(resolvePath(fp), "utf-8");
74
+ }
75
+ catch {
76
+ throw new Error(`Cannot read text file: ${fp}`);
77
+ }
78
+ return { kind: "text", label, source: text, needsUpload: false };
79
+ }
80
+ return { kind: "text", label, source: body, needsUpload: false };
81
+ }
82
+ // Media (explicit or inferred). Body is a file path.
83
+ const path = body;
84
+ if (!path)
85
+ throw new Error(`--variant ${value}: missing file path.`);
86
+ // Resolve kind: explicit override wins, else infer from MIME.
87
+ let kind = explicitKind;
88
+ if (!kind) {
89
+ const mime = detectMimeType(path);
90
+ kind = inferKindFromMime(mime);
91
+ if (!kind) {
92
+ throw new Error(`Cannot infer variant kind from file: ${path}. ` +
93
+ `Use an explicit prefix like image:${path} or document:${path}.`);
94
+ }
95
+ }
96
+ return { kind, label, source: path, needsUpload: true };
97
+ }
98
+ export function parseVariantFlags(flags) {
99
+ return flags.map(parseVariantFlag);
100
+ }
101
+ /** Load a JSON manifest (an array of variant entries) from disk. */
102
+ export function loadVariantManifest(filePath) {
103
+ let raw;
104
+ try {
105
+ raw = readFileSync(resolvePath(filePath), "utf-8");
106
+ }
107
+ catch {
108
+ throw new Error(`Cannot read variants manifest: ${filePath}`);
109
+ }
110
+ let parsed;
111
+ try {
112
+ parsed = JSON.parse(raw);
113
+ }
114
+ catch {
115
+ throw new Error(`Invalid JSON in variants manifest: ${filePath}`);
116
+ }
117
+ if (!Array.isArray(parsed) || parsed.length === 0) {
118
+ throw new Error(`Variants manifest must be a non-empty JSON array: ${filePath}`);
119
+ }
120
+ const out = [];
121
+ for (let i = 0; i < parsed.length; i++) {
122
+ const entry = parsed[i];
123
+ const kind = entry.kind;
124
+ if (!kind || !ASK_VARIANT_KINDS.includes(kind)) {
125
+ throw new Error(`variants[${i}].kind must be one of ${ASK_VARIANT_KINDS.join(", ")}.`);
126
+ }
127
+ if (kind === "text") {
128
+ let text = entry.content;
129
+ if (entry.file) {
130
+ try {
131
+ text = readFileSync(resolvePath(entry.file), "utf-8");
132
+ }
133
+ catch {
134
+ throw new Error(`Cannot read text file referenced in variants[${i}].file: ${entry.file}`);
135
+ }
136
+ }
137
+ if (text === undefined || text === "") {
138
+ throw new Error(`variants[${i}] (text): provide either "content" or "file".`);
139
+ }
140
+ out.push({ kind: "text", label: entry.label, source: text, needsUpload: false });
141
+ continue;
142
+ }
143
+ if (entry.file) {
144
+ out.push({ kind, label: entry.label, source: entry.file, needsUpload: true });
145
+ continue;
146
+ }
147
+ if (entry.content) {
148
+ // Treat as already-uploaded file_path or content URL — pass through.
149
+ out.push({ kind, label: entry.label, source: entry.content, needsUpload: false });
150
+ continue;
151
+ }
152
+ throw new Error(`variants[${i}] (${kind}): provide "file" (to upload) or "content" (existing file_path/URL).`);
153
+ }
154
+ return out;
155
+ }
156
+ /**
157
+ * Walk the parsed variants, request signed upload URLs for the ones that need
158
+ * uploading, PUT each file to its signed URL, then return an AskVariantInput[]
159
+ * with the right `content` value (file_path for media, literal for text).
160
+ */
161
+ export async function uploadAndBuildVariants(client, productId, parsed, opts) {
162
+ if (parsed.length === 0)
163
+ return [];
164
+ const log = (msg) => { if (!opts?.quiet)
165
+ process.stderr.write(msg); };
166
+ // Validate all media files up front so we fail fast before requesting URLs.
167
+ const uploads = [];
168
+ for (let i = 0; i < parsed.length; i++) {
169
+ const v = parsed[i];
170
+ if (!v.needsUpload)
171
+ continue;
172
+ const { size, mime } = await validateFile(v.source);
173
+ uploads.push({ index: i, mime, size, path: v.source });
174
+ }
175
+ // Map index → resolved content_string (file_path for media, literal for text).
176
+ const resolved = new Map();
177
+ for (let i = 0; i < parsed.length; i++) {
178
+ const v = parsed[i];
179
+ if (!v.needsUpload)
180
+ resolved.set(i, v.source);
181
+ }
182
+ if (uploads.length > 0) {
183
+ const body = {
184
+ items: uploads.map((u) => ({ content_type: u.mime, file_size_bytes: u.size })),
185
+ };
186
+ const resp = await client.post(`/products/${productId}/asks/variants/upload-urls`, body);
187
+ if (!resp.items || resp.items.length !== uploads.length) {
188
+ throw new Error("Variant upload response did not match request size.");
189
+ }
190
+ for (let i = 0; i < uploads.length; i++) {
191
+ const u = uploads[i];
192
+ const slot = resp.items[i];
193
+ const sizeMB = (u.size / (1024 * 1024)).toFixed(2);
194
+ log(`Uploading ${basename(u.path)} (${sizeMB} MB)...`);
195
+ const buf = await readFileAsync(resolvePath(u.path));
196
+ const putResp = await fetch(slot.upload_info.signed_upload_url, {
197
+ method: "PUT",
198
+ headers: {
199
+ "Content-Type": u.mime,
200
+ "Content-Length": String(buf.byteLength),
201
+ },
202
+ body: buf,
203
+ signal: AbortSignal.timeout(300_000),
204
+ });
205
+ if (!putResp.ok) {
206
+ const errText = await putResp.text().catch(() => "");
207
+ throw new Error(`Upload failed for ${u.path} (HTTP ${putResp.status}): ${errText}`);
208
+ }
209
+ log(" done.\n");
210
+ resolved.set(u.index, slot.upload_info.file_path);
211
+ }
212
+ }
213
+ return parsed.map((v, i) => {
214
+ const content = resolved.get(i);
215
+ const out = { kind: v.kind, content };
216
+ if (v.label !== undefined && v.label !== "")
217
+ out.label = v.label;
218
+ return out;
219
+ });
220
+ }
221
+ /**
222
+ * Convenience: parse flags OR manifest, validate that they're not both set, and
223
+ * return the parsed list. Caller still has to call `uploadAndBuildVariants`.
224
+ */
225
+ export function parseVariantInputs(opts) {
226
+ const hasFlags = opts.variant && opts.variant.length > 0;
227
+ const hasManifest = !!opts.variants;
228
+ if (hasFlags && hasManifest) {
229
+ throw new Error("Use either --variant flags or --variants <manifest.json>, not both.");
230
+ }
231
+ if (hasManifest)
232
+ return loadVariantManifest(opts.variants);
233
+ if (hasFlags)
234
+ return parseVariantFlags(opts.variant);
235
+ return [];
236
+ }
@@ -5,4 +5,4 @@
5
5
  export declare const DEFAULT_API_URL = "https://api.ishlabs.io";
6
6
  export declare const API_BASE = "/api/v1";
7
7
  export declare function resolveApiUrl(apiUrlArg?: string, dev?: boolean): string;
8
- export declare function resolveToken(tokenArg: string | undefined, apiUrl: string): Promise<string>;
8
+ export declare function resolveToken(tokenArg: string | undefined, apiUrl: string, tokenFileArg?: string): Promise<string>;
package/dist/lib/auth.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * Shared authentication and configuration utilities for CLI commands.
3
3
  * Uses the canonical config from src/config.ts and OAuth refresh from src/auth.ts.
4
4
  */
5
+ import * as fs from "node:fs";
5
6
  import { loadConfig, saveConfig } from "../config.js";
6
7
  import { refreshTokens, isTokenExpired } from "../auth.js";
7
8
  export const DEFAULT_API_URL = "https://api.ishlabs.io";
@@ -28,22 +29,36 @@ async function verifyToken(token, apiUrl) {
28
29
  return true;
29
30
  }
30
31
  }
31
- export async function resolveToken(tokenArg, apiUrl) {
32
+ export async function resolveToken(tokenArg, apiUrl, tokenFileArg) {
32
33
  // 1. Explicit token argument
33
34
  if (tokenArg)
34
35
  return tokenArg;
35
- // 2. Environment variable
36
+ // 2. Token file
37
+ if (tokenFileArg) {
38
+ let content;
39
+ try {
40
+ content = fs.readFileSync(tokenFileArg, "utf-8");
41
+ }
42
+ catch {
43
+ throw new Error(`Cannot read --token-file: ${tokenFileArg}`);
44
+ }
45
+ const token = content.trim();
46
+ if (!token)
47
+ throw new Error(`--token-file is empty: ${tokenFileArg}`);
48
+ return token;
49
+ }
50
+ // 3. Environment variable
36
51
  const envToken = process.env.ISH_TOKEN;
37
52
  if (envToken)
38
53
  return envToken;
39
- // 3. Saved config with OAuth tokens (access_token + refresh_token)
54
+ // 4. Saved config with OAuth tokens (access_token + refresh_token)
40
55
  const config = loadConfig();
41
56
  if (config.access_token && config.refresh_token) {
42
57
  let accessToken = config.access_token;
43
58
  // Refresh if expired or close to expiry
44
59
  if (isTokenExpired(accessToken)) {
45
60
  try {
46
- const tokens = await refreshTokens(config.refresh_token);
61
+ const tokens = await refreshTokens(config.refresh_token, { accessToken });
47
62
  accessToken = tokens.accessToken;
48
63
  config.access_token = tokens.accessToken;
49
64
  config.refresh_token = tokens.refreshToken;
@@ -53,7 +68,8 @@ export async function resolveToken(tokenArg, apiUrl) {
53
68
  if (err instanceof TypeError || (err instanceof Error && err.message.includes('fetch'))) {
54
69
  throw new Error('Could not refresh session (network error). Check your connection or run "ish login".');
55
70
  }
56
- throw new Error('Session expired. Run "ish login" to re-authenticate.');
71
+ const detail = err instanceof Error && err.message.startsWith("Token refresh failed") ? ` (${err.message})` : "";
72
+ throw new Error(`Session expired. Run "ish login" to re-authenticate.${detail}`);
57
73
  }
58
74
  }
59
75
  if (await verifyToken(accessToken, apiUrl)) {
@@ -61,13 +77,13 @@ export async function resolveToken(tokenArg, apiUrl) {
61
77
  }
62
78
  throw new Error('Saved token is invalid. Run "ish login" to re-authenticate.');
63
79
  }
64
- // 4. Legacy saved token (no refresh token)
80
+ // 5. Legacy saved token (no refresh token)
65
81
  if (config.token) {
66
82
  if (await verifyToken(config.token, apiUrl)) {
67
83
  return config.token;
68
84
  }
69
85
  throw new Error('Saved token is invalid. Run "ish login" to re-authenticate.');
70
86
  }
71
- // 5. No token found
72
- throw new Error('No auth token found. Run "ish login" or set ISH_TOKEN environment variable or pass --token <token>.');
87
+ // 6. No token found
88
+ throw new Error('No auth token found. Run "ish login", set ISH_TOKEN, or pass --token <token> / --token-file <path>.');
73
89
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Color/escape-sequence handling.
3
+ *
4
+ * Honors the NO_COLOR (https://no-color.org) and FORCE_COLOR conventions
5
+ * plus a runtime `--no-color` flag. Defaults to enabled when stdout is a
6
+ * TTY and NO_COLOR is unset.
7
+ *
8
+ * Use the `c` object: every property is a getter that returns the literal
9
+ * escape sequence when colors are enabled and an empty string when they
10
+ * aren't, so concatenating them is always safe.
11
+ */
12
+ /** Override the default. Wired up in withClient/runInline based on --no-color. */
13
+ export declare function setColorsEnabled(enabled: boolean): void;
14
+ export declare function colorsEnabled(): boolean;
15
+ declare const SEQUENCES: {
16
+ readonly reset: "\u001B[0m";
17
+ readonly bold: "\u001B[1m";
18
+ readonly dim: "\u001B[2m";
19
+ readonly green: "\u001B[32m";
20
+ readonly red: "\u001B[31m";
21
+ readonly yellow: "\u001B[33m";
22
+ readonly cyan: "\u001B[36m";
23
+ readonly orange: "\u001B[38;2;212;117;78m";
24
+ };
25
+ type ColorMap = {
26
+ readonly [K in keyof typeof SEQUENCES]: string;
27
+ };
28
+ /** Color sequences. Reflect the current setColorsEnabled() state on each access. */
29
+ export declare const c: ColorMap;
30
+ export {};
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Color/escape-sequence handling.
3
+ *
4
+ * Honors the NO_COLOR (https://no-color.org) and FORCE_COLOR conventions
5
+ * plus a runtime `--no-color` flag. Defaults to enabled when stdout is a
6
+ * TTY and NO_COLOR is unset.
7
+ *
8
+ * Use the `c` object: every property is a getter that returns the literal
9
+ * escape sequence when colors are enabled and an empty string when they
10
+ * aren't, so concatenating them is always safe.
11
+ */
12
+ let _enabled = computeDefault();
13
+ function computeDefault() {
14
+ if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "")
15
+ return false;
16
+ const force = process.env.FORCE_COLOR;
17
+ if (force === "0" || force === "false")
18
+ return false;
19
+ if (force !== undefined && force !== "")
20
+ return true;
21
+ return Boolean(process.stdout.isTTY);
22
+ }
23
+ /** Override the default. Wired up in withClient/runInline based on --no-color. */
24
+ export function setColorsEnabled(enabled) {
25
+ _enabled = enabled;
26
+ }
27
+ export function colorsEnabled() {
28
+ return _enabled;
29
+ }
30
+ const SEQUENCES = {
31
+ reset: "\x1b[0m",
32
+ bold: "\x1b[1m",
33
+ dim: "\x1b[2m",
34
+ green: "\x1b[32m",
35
+ red: "\x1b[31m",
36
+ yellow: "\x1b[33m",
37
+ cyan: "\x1b[36m",
38
+ orange: "\x1b[38;2;212;117;78m",
39
+ };
40
+ const colorObj = Object.create(null);
41
+ for (const [key, value] of Object.entries(SEQUENCES)) {
42
+ Object.defineProperty(colorObj, key, {
43
+ get: () => (_enabled ? value : ""),
44
+ enumerable: true,
45
+ });
46
+ }
47
+ /** Color sequences. Reflect the current setColorsEnabled() state on each access. */
48
+ export const c = colorObj;
@@ -4,16 +4,69 @@
4
4
  */
5
5
  import type { Command } from "commander";
6
6
  import { ApiClient } from "./api-client.js";
7
+ export interface AudienceFilterOpts {
8
+ profile?: string[];
9
+ sample?: string;
10
+ all?: boolean;
11
+ search?: string;
12
+ gender?: string[];
13
+ country?: string[];
14
+ minAge?: string;
15
+ maxAge?: string;
16
+ visibility?: string;
17
+ }
18
+ export interface AudienceResolveOpts {
19
+ requireSimulatable?: boolean;
20
+ allFlagName?: string;
21
+ /** Profile IDs to exclude from the fetched pool (e.g. testers already on an ask). */
22
+ excludeProfileIds?: Set<string>;
23
+ }
24
+ /** True when any audience-selection flag is set on the command. */
25
+ export declare function hasAudienceFlags(flags: AudienceFilterOpts): boolean;
26
+ /**
27
+ * Resolve a tester-profile audience from CLI flags.
28
+ *
29
+ * Modes (mutually exclusive):
30
+ * - explicit: `--profile <id>` (repeatable) — returns those IDs verbatim.
31
+ * - sample: `--sample N` + optional filter flags — fetches the matching
32
+ * pool, randomly samples N.
33
+ * - all: caller's "all" flag + optional filter flags — returns every
34
+ * matching profile.
35
+ * - filtered: filter flags only — treated as "all matching".
36
+ *
37
+ * Always sends `type=ai` to the backend (simulations are AI-driven).
38
+ * If `requireSimulatable`, applies a client-side filter for profiles that
39
+ * have a simulation config attached.
40
+ */
41
+ export declare function resolveAudienceProfileIds(client: ApiClient, workspace: string, flags: AudienceFilterOpts, opts?: AudienceResolveOpts): Promise<string[]>;
42
+ /**
43
+ * Attach the audience-selection flag set to a Commander command.
44
+ * Pass `allFlagName: "--all-simulatable"` for ask commands to keep the
45
+ * established flag name; defaults to `--all` for study run.
46
+ */
47
+ export declare function addAudienceFilterFlags(cmd: Command, opts?: {
48
+ allFlagName?: string;
49
+ allFlagDescription?: string;
50
+ }): Command;
7
51
  export interface GlobalOpts {
8
52
  token?: string;
53
+ tokenFile?: string;
9
54
  apiUrl?: string;
10
55
  dev?: boolean;
11
56
  json: boolean;
12
57
  verbose: boolean;
13
58
  quiet: boolean;
59
+ color: boolean;
14
60
  fields?: string[];
15
61
  }
16
62
  export declare function getGlobals(cmd: Command): GlobalOpts;
63
+ /**
64
+ * Semantic exit codes used across the CLI.
65
+ * 0=success, 1=general error, 2=usage/validation, 3=auth, 4=not found, 5=transient.
66
+ * `EXIT_USAGE` matches the POSIX convention for argv/usage errors and is
67
+ * what `program.exitOverride()` returns from Commander-level failures.
68
+ */
69
+ export declare const EXIT_USAGE = 2;
17
70
  /**
18
71
  * Map errors to semantic exit codes.
19
72
  * 0=success, 1=general, 2=usage/validation, 3=auth, 4=not found, 5=transient
@@ -21,8 +74,29 @@ export declare function getGlobals(cmd: Command): GlobalOpts;
21
74
  export declare function exitCodeFromError(err: unknown): number;
22
75
  export declare function createClient(globals: GlobalOpts): Promise<ApiClient>;
23
76
  export declare function withClient(cmd: Command, fn: (client: ApiClient, globals: GlobalOpts) => Promise<void>): Promise<void>;
77
+ /**
78
+ * Wrap an inline (non-API) command action with the same uniform error
79
+ * formatting + exit codes used by withClient. Use this for actions that
80
+ * don't need an ApiClient — login, logout, connect, etc.
81
+ */
82
+ export declare function runInline(cmd: Command, fn: (globals: GlobalOpts) => Promise<void>): Promise<void>;
24
83
  export declare function getWebUrl(globals: GlobalOpts, path: string): string;
25
84
  export declare function terminalLink(url: string, text: string): string;
26
85
  export declare function readJsonFileOrStdin(filePath?: string): Promise<unknown>;
27
86
  export declare function resolveWorkspace(explicit?: string): string;
28
87
  export declare function resolveStudy(explicit?: string): string;
88
+ export declare function resolveAsk(explicit?: string): string;
89
+ /** Commander option-collector for repeatable flags (e.g. `--variant text:"..."` repeated). */
90
+ export declare function collectRepeatable(value: string, prev?: string[]): string[];
91
+ /**
92
+ * Commander collector for ID-list flags. Accepts a comma-separated value
93
+ * (`--profile a,b,c`) and also concatenates across repeated flags
94
+ * (`--profile a --profile b`). Trims whitespace and drops empty entries.
95
+ *
96
+ * Use this for flags whose values are IDs/aliases — never for free-form
97
+ * strings that may legitimately contain commas (those should use
98
+ * `collectRepeatable`).
99
+ */
100
+ export declare function collectIds(value: string, prev?: string[]): string[];
101
+ /** Parse a `--timeout <seconds>` flag into milliseconds, with validation. */
102
+ export declare function parseWaitTimeout(raw: string | undefined, defaultMs?: number): number;