@ishlabs/cli 0.8.1 → 0.8.3
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/README.md +323 -21
- package/dist/auth.d.ts +17 -1
- package/dist/auth.js +62 -9
- package/dist/commands/ask.d.ts +5 -0
- package/dist/commands/ask.js +722 -0
- package/dist/commands/config.js +25 -1
- package/dist/commands/docs.d.ts +17 -0
- package/dist/commands/docs.js +147 -0
- package/dist/commands/init.d.ts +16 -0
- package/dist/commands/init.js +182 -0
- package/dist/commands/iteration.d.ts +5 -1
- package/dist/commands/iteration.js +243 -31
- package/dist/commands/profile.d.ts +5 -0
- package/dist/commands/profile.js +313 -0
- package/dist/commands/source.d.ts +10 -0
- package/dist/commands/source.js +78 -0
- package/dist/commands/study-run.d.ts +11 -0
- package/dist/commands/study-run.js +552 -0
- package/dist/commands/study-tester.d.ts +8 -0
- package/dist/commands/study-tester.js +149 -0
- package/dist/commands/study.js +145 -70
- package/dist/commands/workspace.js +193 -7
- package/dist/config.d.ts +3 -1
- package/dist/config.js +10 -10
- package/dist/connect.d.ts +4 -1
- package/dist/connect.js +127 -94
- package/dist/index.js +82 -34
- package/dist/lib/alias-store.d.ts +3 -0
- package/dist/lib/alias-store.js +9 -7
- package/dist/lib/api-client.d.ts +9 -6
- package/dist/lib/api-client.js +87 -26
- package/dist/lib/ask-questions.d.ts +9 -0
- package/dist/lib/ask-questions.js +35 -0
- package/dist/lib/ask-variants.d.ts +48 -0
- package/dist/lib/ask-variants.js +236 -0
- package/dist/lib/auth.d.ts +1 -1
- package/dist/lib/auth.js +24 -8
- package/dist/lib/colors.d.ts +30 -0
- package/dist/lib/colors.js +48 -0
- package/dist/lib/command-helpers.d.ts +74 -0
- package/dist/lib/command-helpers.js +232 -6
- package/dist/lib/docs.d.ts +32 -0
- package/dist/lib/docs.js +930 -0
- package/dist/lib/local-sim/browser.d.ts +0 -1
- package/dist/lib/local-sim/browser.js +0 -2
- package/dist/lib/local-sim/install.d.ts +2 -12
- package/dist/lib/local-sim/install.js +22 -30
- package/dist/lib/output.d.ts +25 -3
- package/dist/lib/output.js +465 -20
- package/dist/lib/paths.d.ts +14 -0
- package/dist/lib/paths.js +36 -0
- package/dist/lib/profile-sources.d.ts +55 -0
- package/dist/lib/profile-sources.js +157 -0
- package/dist/lib/site-access.d.ts +80 -0
- package/dist/lib/site-access.js +188 -0
- package/dist/lib/skill-content.d.ts +31 -0
- package/dist/lib/skill-content.js +462 -0
- package/dist/lib/study-inputs.d.ts +20 -0
- package/dist/lib/study-inputs.js +72 -0
- package/dist/lib/types.d.ts +207 -9
- package/dist/lib/types.js +7 -0
- package/dist/lib/upload.js +2 -2
- package/dist/upgrade.js +11 -1
- package/package.json +3 -2
- package/dist/commands/simulation.d.ts +0 -10
- package/dist/commands/simulation.js +0 -647
- package/dist/commands/tester-profile.d.ts +0 -5
- package/dist/commands/tester-profile.js +0 -109
- package/dist/commands/tester.d.ts +0 -5
- package/dist/commands/tester.js +0 -73
|
@@ -7,8 +7,160 @@ import { resolveApiUrl, resolveToken } from "./auth.js";
|
|
|
7
7
|
import { getAppUrl } from "../auth.js";
|
|
8
8
|
import { ApiClient, ApiError } from "./api-client.js";
|
|
9
9
|
import { outputError, setVerbose, setFields } from "./output.js";
|
|
10
|
+
import { setColorsEnabled, colorsEnabled } from "./colors.js";
|
|
10
11
|
import { loadConfig } from "../config.js";
|
|
11
12
|
import { resolveId } from "./alias-store.js";
|
|
13
|
+
function isSimulatable(p) {
|
|
14
|
+
return Boolean(p.simulation_config_id) || Boolean(p.simulation_config);
|
|
15
|
+
}
|
|
16
|
+
function shuffleInPlace(arr) {
|
|
17
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
18
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
19
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
20
|
+
}
|
|
21
|
+
return arr;
|
|
22
|
+
}
|
|
23
|
+
function parseSampleSize(raw) {
|
|
24
|
+
if (raw === undefined)
|
|
25
|
+
return undefined;
|
|
26
|
+
const n = parseInt(raw, 10);
|
|
27
|
+
if (Number.isNaN(n) || n <= 0) {
|
|
28
|
+
throw new Error(`--sample must be a positive integer, got "${raw}".`);
|
|
29
|
+
}
|
|
30
|
+
return n;
|
|
31
|
+
}
|
|
32
|
+
function describeFilters(flags) {
|
|
33
|
+
const parts = [];
|
|
34
|
+
if (flags.search)
|
|
35
|
+
parts.push(`--search "${flags.search}"`);
|
|
36
|
+
if (flags.gender?.length)
|
|
37
|
+
parts.push(...flags.gender.map((g) => `--gender ${g}`));
|
|
38
|
+
if (flags.country?.length)
|
|
39
|
+
parts.push(...flags.country.map((c) => `--country ${c}`));
|
|
40
|
+
if (flags.minAge)
|
|
41
|
+
parts.push(`--min-age ${flags.minAge}`);
|
|
42
|
+
if (flags.maxAge)
|
|
43
|
+
parts.push(`--max-age ${flags.maxAge}`);
|
|
44
|
+
if (flags.visibility)
|
|
45
|
+
parts.push(`--visibility ${flags.visibility}`);
|
|
46
|
+
return parts.join(" ");
|
|
47
|
+
}
|
|
48
|
+
function hasFilterFlag(flags) {
|
|
49
|
+
return Boolean(flags.search
|
|
50
|
+
|| (flags.gender && flags.gender.length > 0)
|
|
51
|
+
|| (flags.country && flags.country.length > 0)
|
|
52
|
+
|| flags.minAge
|
|
53
|
+
|| flags.maxAge
|
|
54
|
+
|| flags.visibility);
|
|
55
|
+
}
|
|
56
|
+
/** True when any audience-selection flag is set on the command. */
|
|
57
|
+
export function hasAudienceFlags(flags) {
|
|
58
|
+
return Boolean((flags.profile && flags.profile.length > 0)
|
|
59
|
+
|| flags.sample !== undefined
|
|
60
|
+
|| flags.all
|
|
61
|
+
|| hasFilterFlag(flags));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a tester-profile audience from CLI flags.
|
|
65
|
+
*
|
|
66
|
+
* Modes (mutually exclusive):
|
|
67
|
+
* - explicit: `--profile <id>` (repeatable) — returns those IDs verbatim.
|
|
68
|
+
* - sample: `--sample N` + optional filter flags — fetches the matching
|
|
69
|
+
* pool, randomly samples N.
|
|
70
|
+
* - all: caller's "all" flag + optional filter flags — returns every
|
|
71
|
+
* matching profile.
|
|
72
|
+
* - filtered: filter flags only — treated as "all matching".
|
|
73
|
+
*
|
|
74
|
+
* Always sends `type=ai` to the backend (simulations are AI-driven).
|
|
75
|
+
* If `requireSimulatable`, applies a client-side filter for profiles that
|
|
76
|
+
* have a simulation config attached.
|
|
77
|
+
*/
|
|
78
|
+
export async function resolveAudienceProfileIds(client, workspace, flags, opts = {}) {
|
|
79
|
+
const allFlagName = opts.allFlagName ?? "--all";
|
|
80
|
+
const explicit = (flags.profile ?? []).map(resolveId);
|
|
81
|
+
const sampleN = parseSampleSize(flags.sample);
|
|
82
|
+
const filtersUsed = hasFilterFlag(flags);
|
|
83
|
+
if (explicit.length > 0) {
|
|
84
|
+
if (sampleN !== undefined || flags.all || filtersUsed) {
|
|
85
|
+
throw new Error(`Use either explicit --profile flags or --sample/${allFlagName}/filter flags (--country, --gender, --min-age, --max-age, --search, --visibility), not both.`);
|
|
86
|
+
}
|
|
87
|
+
return explicit;
|
|
88
|
+
}
|
|
89
|
+
if (sampleN === undefined && !flags.all && !filtersUsed) {
|
|
90
|
+
throw new Error(`Pick an audience: pass --profile <id> (repeatable), --sample <N>, ${allFlagName}, or filter flags (--country, --gender, --min-age, --max-age, --search, --visibility).`);
|
|
91
|
+
}
|
|
92
|
+
const params = {
|
|
93
|
+
product_id: workspace,
|
|
94
|
+
type: "ai",
|
|
95
|
+
limit: "200",
|
|
96
|
+
offset: "0",
|
|
97
|
+
};
|
|
98
|
+
if (flags.search)
|
|
99
|
+
params.search = flags.search;
|
|
100
|
+
if (flags.gender && flags.gender.length > 0)
|
|
101
|
+
params.gender = flags.gender;
|
|
102
|
+
if (flags.country && flags.country.length > 0)
|
|
103
|
+
params.country = flags.country;
|
|
104
|
+
if (flags.minAge)
|
|
105
|
+
params.min_age = flags.minAge;
|
|
106
|
+
if (flags.maxAge)
|
|
107
|
+
params.max_age = flags.maxAge;
|
|
108
|
+
if (flags.visibility)
|
|
109
|
+
params.visibility = flags.visibility;
|
|
110
|
+
const data = await client.get("/tester-profiles", params);
|
|
111
|
+
const items = Array.isArray(data)
|
|
112
|
+
? data
|
|
113
|
+
: Array.isArray(data?.items)
|
|
114
|
+
? data.items
|
|
115
|
+
: [];
|
|
116
|
+
let pool = items;
|
|
117
|
+
if (opts.requireSimulatable) {
|
|
118
|
+
pool = pool.filter(isSimulatable);
|
|
119
|
+
}
|
|
120
|
+
if (opts.excludeProfileIds && opts.excludeProfileIds.size > 0) {
|
|
121
|
+
pool = pool.filter((p) => !opts.excludeProfileIds.has(p.id));
|
|
122
|
+
}
|
|
123
|
+
if (pool.length === 0) {
|
|
124
|
+
const filterDesc = describeFilters(flags);
|
|
125
|
+
const sim = opts.requireSimulatable ? "simulatable AI " : "AI ";
|
|
126
|
+
if (opts.excludeProfileIds && opts.excludeProfileIds.size > 0 && !filterDesc) {
|
|
127
|
+
throw new Error("All matching profiles are already in this audience.");
|
|
128
|
+
}
|
|
129
|
+
if (filterDesc) {
|
|
130
|
+
throw new Error(`No ${sim}tester profiles in workspace ${workspace} match: ${filterDesc}. Broaden your filters or run \`ish profile list\` to inspect the pool.`);
|
|
131
|
+
}
|
|
132
|
+
throw new Error(`No ${sim}tester profiles found in workspace ${workspace}.${opts.requireSimulatable ? " Create profiles with simulation configs first." : ""}`);
|
|
133
|
+
}
|
|
134
|
+
if (flags.all)
|
|
135
|
+
return pool.map((p) => p.id);
|
|
136
|
+
if (sampleN !== undefined) {
|
|
137
|
+
if (sampleN > pool.length) {
|
|
138
|
+
throw new Error(`--sample ${sampleN} requested but only ${pool.length} matching profile${pool.length === 1 ? "" : "s"} available.`);
|
|
139
|
+
}
|
|
140
|
+
return shuffleInPlace([...pool]).slice(0, sampleN).map((p) => p.id);
|
|
141
|
+
}
|
|
142
|
+
// Filters only, no --sample/--all → return every match.
|
|
143
|
+
return pool.map((p) => p.id);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Attach the audience-selection flag set to a Commander command.
|
|
147
|
+
* Pass `allFlagName: "--all-simulatable"` for ask commands to keep the
|
|
148
|
+
* established flag name; defaults to `--all` for study run.
|
|
149
|
+
*/
|
|
150
|
+
export function addAudienceFilterFlags(cmd, opts = {}) {
|
|
151
|
+
const allFlag = opts.allFlagName ?? "--all";
|
|
152
|
+
const allDesc = opts.allFlagDescription ?? "Use every profile matching the filters";
|
|
153
|
+
return cmd
|
|
154
|
+
.option("--profile <ids>", "Tester profile IDs/aliases (comma-separated or repeatable)", collectIds, [])
|
|
155
|
+
.option("--sample <N>", "Randomly sample N profiles from the matching pool")
|
|
156
|
+
.option(allFlag, allDesc)
|
|
157
|
+
.option("--search <text>", "Free-text search (matches profile name and bio)")
|
|
158
|
+
.option("--gender <gender>", "Filter by gender (repeatable)", collectRepeatable, [])
|
|
159
|
+
.option("--country <code>", "Filter by 2-letter country code (repeatable)", collectRepeatable, [])
|
|
160
|
+
.option("--min-age <n>", "Minimum age (inclusive)")
|
|
161
|
+
.option("--max-age <n>", "Maximum age (inclusive)")
|
|
162
|
+
.option("--visibility <v>", "Filter by visibility (private|public)");
|
|
163
|
+
}
|
|
12
164
|
export function getGlobals(cmd) {
|
|
13
165
|
const opts = cmd.optsWithGlobals();
|
|
14
166
|
// Auto-switch to JSON when stdout is piped (non-TTY)
|
|
@@ -17,16 +169,28 @@ export function getGlobals(cmd) {
|
|
|
17
169
|
const fields = opts.fields
|
|
18
170
|
? String(opts.fields).split(",").map((f) => f.trim()).filter(Boolean)
|
|
19
171
|
: undefined;
|
|
172
|
+
// Commander's negatable boolean: --no-color sets color=false, default is undefined.
|
|
173
|
+
// Fall back to the env-aware default from colors.ts.
|
|
174
|
+
const color = opts.color === false ? false : (json ? false : colorsEnabled());
|
|
20
175
|
return {
|
|
21
176
|
token: opts.token,
|
|
177
|
+
tokenFile: opts.tokenFile,
|
|
22
178
|
apiUrl: opts.apiUrl,
|
|
23
179
|
dev: opts.dev,
|
|
24
180
|
json,
|
|
25
181
|
verbose: opts.verbose ?? false,
|
|
26
182
|
quiet: opts.quiet ?? (json && !opts.json), // auto-quiet when auto-json via pipe
|
|
183
|
+
color,
|
|
27
184
|
fields,
|
|
28
185
|
};
|
|
29
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* Semantic exit codes used across the CLI.
|
|
189
|
+
* 0=success, 1=general error, 2=usage/validation, 3=auth, 4=not found, 5=transient.
|
|
190
|
+
* `EXIT_USAGE` matches the POSIX convention for argv/usage errors and is
|
|
191
|
+
* what `program.exitOverride()` returns from Commander-level failures.
|
|
192
|
+
*/
|
|
193
|
+
export const EXIT_USAGE = 2;
|
|
30
194
|
/**
|
|
31
195
|
* Map errors to semantic exit codes.
|
|
32
196
|
* 0=success, 1=general, 2=usage/validation, 3=auth, 4=not found, 5=transient
|
|
@@ -42,20 +206,29 @@ export function exitCodeFromError(err) {
|
|
|
42
206
|
if (err.retryable)
|
|
43
207
|
return 5;
|
|
44
208
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
209
|
+
if (err instanceof Error) {
|
|
210
|
+
// Auth-related client errors (e.g. missing token)
|
|
211
|
+
if (/no auth token found|run "ish login"|saved token is invalid|session expired/i.test(err.message))
|
|
212
|
+
return 3;
|
|
213
|
+
// Client-side validation failures
|
|
214
|
+
if (err.name === "ValidationError" || /^invalid |^cannot read |is empty:|--\w[\w-]* must be|pick an audience|use either /i.test(err.message))
|
|
215
|
+
return 2;
|
|
216
|
+
}
|
|
48
217
|
return 1;
|
|
49
218
|
}
|
|
50
219
|
export async function createClient(globals) {
|
|
51
220
|
const apiUrl = resolveApiUrl(globals.apiUrl, globals.dev);
|
|
52
|
-
const token = await resolveToken(globals.token, apiUrl);
|
|
221
|
+
const token = await resolveToken(globals.token, apiUrl, globals.tokenFile);
|
|
53
222
|
return new ApiClient({ apiUrl, token });
|
|
54
223
|
}
|
|
55
|
-
|
|
56
|
-
const globals = getGlobals(cmd);
|
|
224
|
+
function applyGlobals(globals) {
|
|
57
225
|
setVerbose(globals.verbose);
|
|
58
226
|
setFields(globals.fields);
|
|
227
|
+
setColorsEnabled(globals.color);
|
|
228
|
+
}
|
|
229
|
+
export async function withClient(cmd, fn) {
|
|
230
|
+
const globals = getGlobals(cmd);
|
|
231
|
+
applyGlobals(globals);
|
|
59
232
|
try {
|
|
60
233
|
const client = await createClient(globals);
|
|
61
234
|
await fn(client, globals);
|
|
@@ -65,6 +238,22 @@ export async function withClient(cmd, fn) {
|
|
|
65
238
|
process.exit(exitCodeFromError(err));
|
|
66
239
|
}
|
|
67
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Wrap an inline (non-API) command action with the same uniform error
|
|
243
|
+
* formatting + exit codes used by withClient. Use this for actions that
|
|
244
|
+
* don't need an ApiClient — login, logout, connect, etc.
|
|
245
|
+
*/
|
|
246
|
+
export async function runInline(cmd, fn) {
|
|
247
|
+
const globals = getGlobals(cmd);
|
|
248
|
+
applyGlobals(globals);
|
|
249
|
+
try {
|
|
250
|
+
await fn(globals);
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
outputError(err, globals.json);
|
|
254
|
+
process.exit(exitCodeFromError(err));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
68
257
|
export function getWebUrl(globals, path) {
|
|
69
258
|
const base = globals.dev ? "http://localhost:3000" : getAppUrl();
|
|
70
259
|
return `${base}${path}`;
|
|
@@ -129,3 +318,40 @@ export function resolveStudy(explicit) {
|
|
|
129
318
|
return config.study;
|
|
130
319
|
throw new Error('No study set. Use `ish study use <alias>` or pass --study.');
|
|
131
320
|
}
|
|
321
|
+
export function resolveAsk(explicit) {
|
|
322
|
+
if (explicit)
|
|
323
|
+
return resolveId(explicit);
|
|
324
|
+
const env = process.env.ISH_ASK;
|
|
325
|
+
if (env)
|
|
326
|
+
return resolveId(env);
|
|
327
|
+
const config = loadConfig();
|
|
328
|
+
if (config.ask)
|
|
329
|
+
return config.ask;
|
|
330
|
+
throw new Error('No ask set. Use `ish ask use <alias>` or pass the ask ID as an argument.');
|
|
331
|
+
}
|
|
332
|
+
/** Commander option-collector for repeatable flags (e.g. `--variant text:"..."` repeated). */
|
|
333
|
+
export function collectRepeatable(value, prev = []) {
|
|
334
|
+
return prev.concat([value]);
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Commander collector for ID-list flags. Accepts a comma-separated value
|
|
338
|
+
* (`--profile a,b,c`) and also concatenates across repeated flags
|
|
339
|
+
* (`--profile a --profile b`). Trims whitespace and drops empty entries.
|
|
340
|
+
*
|
|
341
|
+
* Use this for flags whose values are IDs/aliases — never for free-form
|
|
342
|
+
* strings that may legitimately contain commas (those should use
|
|
343
|
+
* `collectRepeatable`).
|
|
344
|
+
*/
|
|
345
|
+
export function collectIds(value, prev = []) {
|
|
346
|
+
return prev.concat(value.split(",").map((s) => s.trim()).filter(Boolean));
|
|
347
|
+
}
|
|
348
|
+
/** Parse a `--timeout <seconds>` flag into milliseconds, with validation. */
|
|
349
|
+
export function parseWaitTimeout(raw, defaultMs = 5 * 60 * 1000) {
|
|
350
|
+
if (raw === undefined)
|
|
351
|
+
return defaultMs;
|
|
352
|
+
const n = parseInt(raw, 10);
|
|
353
|
+
if (Number.isNaN(n) || n <= 0) {
|
|
354
|
+
throw new Error(`--timeout must be a positive integer (seconds), got "${raw}".`);
|
|
355
|
+
}
|
|
356
|
+
return n * 1000;
|
|
357
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ish docs — In-binary documentation aimed at AI coding agents.
|
|
3
|
+
*
|
|
4
|
+
* Pages ship as TypeScript string constants so they survive both the `tsc`
|
|
5
|
+
* build and the `bun build --compile` single-binary build. Slugs use
|
|
6
|
+
* forward-slashes so the surface mirrors a doc tree (`concepts/study`).
|
|
7
|
+
*/
|
|
8
|
+
export interface DocPage {
|
|
9
|
+
slug: string;
|
|
10
|
+
title: string;
|
|
11
|
+
/** One-line description shown in `ish docs list` and search. */
|
|
12
|
+
description: string;
|
|
13
|
+
body: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function listPages(): DocPage[];
|
|
16
|
+
export declare function getPage(slug: string): DocPage | undefined;
|
|
17
|
+
/** Concise pointer printed at the bottom of `--help` outputs. */
|
|
18
|
+
export declare const AGENT_HELP_FOOTER: string;
|
|
19
|
+
export interface SearchHit {
|
|
20
|
+
slug: string;
|
|
21
|
+
title: string;
|
|
22
|
+
description: string;
|
|
23
|
+
/** Highest-scoring match snippet, max ~200 chars. */
|
|
24
|
+
snippet: string;
|
|
25
|
+
score: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Substring search over title + description + body. Title hits score
|
|
29
|
+
* highest, then description, then body. Returns at most `limit` hits
|
|
30
|
+
* sorted by score desc.
|
|
31
|
+
*/
|
|
32
|
+
export declare function searchPages(query: string, limit?: number): SearchHit[];
|