@ishlabs/cli 0.8.1

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 (57) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +69 -0
  3. package/dist/auth.d.ts +17 -0
  4. package/dist/auth.js +102 -0
  5. package/dist/commands/config.d.ts +5 -0
  6. package/dist/commands/config.js +82 -0
  7. package/dist/commands/iteration.d.ts +5 -0
  8. package/dist/commands/iteration.js +134 -0
  9. package/dist/commands/simulation.d.ts +10 -0
  10. package/dist/commands/simulation.js +647 -0
  11. package/dist/commands/study.d.ts +5 -0
  12. package/dist/commands/study.js +283 -0
  13. package/dist/commands/tester-profile.d.ts +5 -0
  14. package/dist/commands/tester-profile.js +109 -0
  15. package/dist/commands/tester.d.ts +5 -0
  16. package/dist/commands/tester.js +73 -0
  17. package/dist/commands/workspace.d.ts +5 -0
  18. package/dist/commands/workspace.js +133 -0
  19. package/dist/config.d.ts +13 -0
  20. package/dist/config.js +25 -0
  21. package/dist/connect.d.ts +4 -0
  22. package/dist/connect.js +573 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +89 -0
  25. package/dist/lib/alias-store.d.ts +49 -0
  26. package/dist/lib/alias-store.js +138 -0
  27. package/dist/lib/api-client.d.ts +58 -0
  28. package/dist/lib/api-client.js +177 -0
  29. package/dist/lib/auth.d.ts +8 -0
  30. package/dist/lib/auth.js +73 -0
  31. package/dist/lib/command-helpers.d.ts +28 -0
  32. package/dist/lib/command-helpers.js +131 -0
  33. package/dist/lib/local-sim/actions.d.ts +22 -0
  34. package/dist/lib/local-sim/actions.js +379 -0
  35. package/dist/lib/local-sim/browser.d.ts +63 -0
  36. package/dist/lib/local-sim/browser.js +332 -0
  37. package/dist/lib/local-sim/debug-report.d.ts +21 -0
  38. package/dist/lib/local-sim/debug-report.js +186 -0
  39. package/dist/lib/local-sim/debug.d.ts +44 -0
  40. package/dist/lib/local-sim/debug.js +103 -0
  41. package/dist/lib/local-sim/install.d.ts +25 -0
  42. package/dist/lib/local-sim/install.js +72 -0
  43. package/dist/lib/local-sim/loop.d.ts +60 -0
  44. package/dist/lib/local-sim/loop.js +526 -0
  45. package/dist/lib/local-sim/types.d.ts +232 -0
  46. package/dist/lib/local-sim/types.js +8 -0
  47. package/dist/lib/local-sim/upload.d.ts +6 -0
  48. package/dist/lib/local-sim/upload.js +24 -0
  49. package/dist/lib/output.d.ts +34 -0
  50. package/dist/lib/output.js +675 -0
  51. package/dist/lib/types.d.ts +179 -0
  52. package/dist/lib/types.js +12 -0
  53. package/dist/lib/upload.d.ts +47 -0
  54. package/dist/lib/upload.js +178 -0
  55. package/dist/upgrade.d.ts +1 -0
  56. package/dist/upgrade.js +94 -0
  57. package/package.json +43 -0
@@ -0,0 +1,283 @@
1
+ /**
2
+ * ish study — Manage studies.
3
+ */
4
+ import { withClient, getWebUrl, terminalLink, resolveWorkspace } from "../lib/command-helpers.js";
5
+ import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
6
+ import { loadConfig, saveConfig } from "../config.js";
7
+ import { formatStudyList, formatStudyDetail, formatStudyResults, output, ValidationError } from "../lib/output.js";
8
+ import { VALID_CONTENT_TYPES } from "../lib/types.js";
9
+ export function registerStudyCommands(program) {
10
+ const study = program
11
+ .command("study")
12
+ .description("Manage studies");
13
+ study
14
+ .command("list")
15
+ .description("List studies for a workspace")
16
+ .option("--workspace <id>", "Workspace ID")
17
+ .addHelpText("after", "\nExamples:\n $ ish study list --workspace <id>\n $ ish study list --workspace <id> --json")
18
+ .action(async (opts, cmd) => {
19
+ await withClient(cmd, async (client, globals) => {
20
+ const data = await client.get(`/products/${resolveWorkspace(opts.workspace)}/studies`);
21
+ formatStudyList(data, globals.json);
22
+ });
23
+ });
24
+ study
25
+ .command("create")
26
+ .description("Create a new study")
27
+ .option("--workspace <id>", "Workspace ID")
28
+ .requiredOption("--name <name>", "Study name")
29
+ .option("--description <description>", "Study description")
30
+ .option("--modality <modality>", "Study modality (interactive, video, audio, text, image, document)")
31
+ .option("--content-type <type>", "Content type (varies by modality — see examples below)")
32
+ .option("--assignments <json>", "JSON array of assignments, e.g. '[{\"name\":\"Task\",\"instructions\":\"Do something\"}]'")
33
+ .option("--questions <json>", "JSON array of interview questions, e.g. '[{\"question\":\"How was it?\",\"type\":\"text\",\"timing\":\"after\"}]'")
34
+ .addHelpText("after", `
35
+ Note: --workspace is optional if set via \`ish workspace use <alias>\`.
36
+
37
+ Examples:
38
+ # Interactive study:
39
+ $ ish study create --name "Onboarding UX" --modality interactive \\
40
+ --assignments '[{"name":"Sign up","instructions":"Complete the signup flow"}]'
41
+
42
+ # Text/email study:
43
+ $ ish study create --name "Newsletter" --modality text --content-type email \\
44
+ --assignments '[{"name":"Read","instructions":"Read this email naturally"}]'
45
+
46
+ # Audio conversation study:
47
+ $ ish study create --name "Episode Review" --modality audio --content-type conversation \\
48
+ --assignments '[{"name":"Listen","instructions":"Listen and react naturally"}]'
49
+
50
+ # Video ad study:
51
+ $ ish study create --name "Product Ad" --modality video --content-type ad \\
52
+ --assignments '[{"name":"Watch","instructions":"Watch this ad and share your reaction"}]'
53
+
54
+ # Image social post study:
55
+ $ ish study create --name "Instagram Post" --modality image --content-type social_post \\
56
+ --assignments '[{"name":"View","instructions":"Look at this post naturally"}]'
57
+
58
+ # With interview questions:
59
+ $ ish study create --name "Checkout" --modality interactive \\
60
+ --assignments '[{"name":"Buy","instructions":"Add to cart and checkout"}]' \\
61
+ --questions '[{"question":"How easy was it?","type":"slider","timing":"after","min":0,"max":10}]'
62
+
63
+ Content types by modality:
64
+ text: narrative, informational, commercial, editorial, reference, email, news
65
+ video: tutorial, documentary, entertainment, review, lifestyle, news, social_post, ad
66
+ audio: music, narration, conversation, speech, soundscape, news, ad
67
+ image: product, photography, infographic, artwork, interface, social_post, ad
68
+ document: deck, presentation, report, brochure, guide`)
69
+ .action(async (opts, cmd) => {
70
+ await withClient(cmd, async (client, globals) => {
71
+ let assignments;
72
+ let interviewQuestions;
73
+ if (opts.assignments) {
74
+ try {
75
+ assignments = JSON.parse(opts.assignments);
76
+ }
77
+ catch {
78
+ throw new Error("Invalid --assignments JSON");
79
+ }
80
+ }
81
+ if (opts.questions) {
82
+ try {
83
+ interviewQuestions = JSON.parse(opts.questions);
84
+ }
85
+ catch {
86
+ throw new Error("Invalid --questions JSON");
87
+ }
88
+ }
89
+ // Validate content_type against modality
90
+ if (opts.contentType && opts.modality) {
91
+ const validTypes = VALID_CONTENT_TYPES[opts.modality];
92
+ if (validTypes && !validTypes.includes(opts.contentType)) {
93
+ throw new ValidationError(`Invalid content type "${opts.contentType}" for modality "${opts.modality}".`, validTypes);
94
+ }
95
+ }
96
+ const resolvedWs = resolveWorkspace(opts.workspace);
97
+ const body = {
98
+ product_id: resolvedWs,
99
+ name: opts.name,
100
+ ...(opts.description !== undefined && { description: opts.description }),
101
+ ...(opts.modality !== undefined && { modality: opts.modality }),
102
+ ...(opts.contentType !== undefined && { content_type: opts.contentType }),
103
+ ...(assignments && { assignments }),
104
+ ...(interviewQuestions && { interview_questions: interviewQuestions }),
105
+ };
106
+ const data = await client.post(`/products/${resolvedWs}/studies`, body);
107
+ if (data.id) {
108
+ const config = loadConfig();
109
+ config.study = data.id;
110
+ saveConfig(config);
111
+ }
112
+ const result = data;
113
+ if (result.id)
114
+ result.alias = tagAlias(ALIAS_PREFIX.study, String(result.id));
115
+ formatStudyDetail(result, globals.json);
116
+ if (!globals.json && data.id) {
117
+ const url = getWebUrl(globals, `/${resolvedWs}/${data.id}/overview`);
118
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
119
+ }
120
+ });
121
+ });
122
+ study
123
+ .command("generate")
124
+ .description("Generate a study from a problem description using AI")
125
+ .option("--workspace <id>", "Workspace ID")
126
+ .requiredOption("--problem <description>", "Problem description (what you want to understand)")
127
+ .option("--target-url <url>", "URL of the product to test")
128
+ .addHelpText("after", "\nExamples:\n $ ish study generate --workspace <id> --problem \"How do users navigate the onboarding flow?\"\n $ ish study generate --workspace <id> --problem \"Test checkout\" --target-url https://example.com --json")
129
+ .action(async (opts, cmd) => {
130
+ await withClient(cmd, async (client, globals) => {
131
+ const body = {
132
+ problem_description: opts.problem,
133
+ ...(opts.targetUrl && { target_url: opts.targetUrl }),
134
+ };
135
+ const resolvedWs = resolveWorkspace(opts.workspace);
136
+ const data = await client.post(`/products/${resolvedWs}/studies/generate`, body);
137
+ const result = data;
138
+ if (result.id)
139
+ result.alias = tagAlias(ALIAS_PREFIX.study, String(result.id));
140
+ formatStudyDetail(result, globals.json);
141
+ if (!globals.json && data.id) {
142
+ const url = getWebUrl(globals, `/${resolvedWs}/${data.id}/overview`);
143
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
144
+ }
145
+ });
146
+ });
147
+ study
148
+ .command("get")
149
+ .description("Get study overview (assignments, questions, testers)")
150
+ .argument("<id>", "Study ID")
151
+ .addHelpText("after", "\nExamples:\n $ ish study get <id>\n $ ish study get <id> --json")
152
+ .action(async (id, _opts, cmd) => {
153
+ await withClient(cmd, async (client, globals) => {
154
+ const rid = resolveId(id);
155
+ const data = await client.get(`/studies/${rid}`);
156
+ const result = data;
157
+ if (result.id)
158
+ result.alias = tagAlias(ALIAS_PREFIX.study, String(result.id));
159
+ formatStudyDetail(result, globals.json);
160
+ if (!globals.json && data.product_id) {
161
+ const url = getWebUrl(globals, `/${data.product_id}/${rid}/overview`);
162
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
163
+ }
164
+ });
165
+ });
166
+ study
167
+ .command("results")
168
+ .description("View aggregated results (sentiment, interview answers)")
169
+ .argument("<id>", "Study ID")
170
+ .addHelpText("after", "\nExamples:\n $ ish study results <id>\n $ ish study results <id> --json")
171
+ .action(async (id, _opts, cmd) => {
172
+ await withClient(cmd, async (client, globals) => {
173
+ const rid = resolveId(id);
174
+ const data = await client.get(`/studies/${rid}`);
175
+ formatStudyResults(data, globals.json);
176
+ if (!globals.json && data.product_id) {
177
+ const url = getWebUrl(globals, `/${data.product_id}/${rid}/overview`);
178
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
179
+ }
180
+ });
181
+ });
182
+ study
183
+ .command("update")
184
+ .description("Update a study")
185
+ .argument("<id>", "Study ID")
186
+ .option("--name <name>", "Study name")
187
+ .option("--description <description>", "Study description")
188
+ .option("--status <status>", "Study status (draft, running, completed)")
189
+ .option("--modality <modality>", "Study modality (interactive, video, audio, text, image, document)")
190
+ .option("--content-type <type>", "Content type")
191
+ .option("--assignments <json>", "JSON array of assignments, e.g. '[{\"name\":\"Task\",\"instructions\":\"Do something\"}]'")
192
+ .option("--questions <json>", "JSON array of interview questions")
193
+ .addHelpText("after", "\nExamples:\n $ ish study update <id> --name \"Updated Name\"\n $ ish study update <id> --status running --json")
194
+ .action(async (id, opts, cmd) => {
195
+ await withClient(cmd, async (client, globals) => {
196
+ let assignments;
197
+ let interviewQuestions;
198
+ if (opts.assignments) {
199
+ try {
200
+ assignments = JSON.parse(opts.assignments);
201
+ }
202
+ catch {
203
+ throw new Error("Invalid --assignments JSON");
204
+ }
205
+ }
206
+ if (opts.questions) {
207
+ try {
208
+ interviewQuestions = JSON.parse(opts.questions);
209
+ }
210
+ catch {
211
+ throw new Error("Invalid --questions JSON");
212
+ }
213
+ }
214
+ if (opts.contentType && opts.modality) {
215
+ const validTypes = VALID_CONTENT_TYPES[opts.modality];
216
+ if (validTypes && !validTypes.includes(opts.contentType)) {
217
+ throw new ValidationError(`Invalid content type "${opts.contentType}" for modality "${opts.modality}".`, validTypes);
218
+ }
219
+ }
220
+ const body = {};
221
+ if (opts.name !== undefined)
222
+ body.name = opts.name;
223
+ if (opts.description !== undefined)
224
+ body.description = opts.description;
225
+ if (opts.status !== undefined)
226
+ body.status = opts.status;
227
+ if (opts.modality !== undefined)
228
+ body.modality = opts.modality;
229
+ if (opts.contentType !== undefined)
230
+ body.content_type = opts.contentType;
231
+ if (assignments)
232
+ body.assignments = assignments;
233
+ if (interviewQuestions)
234
+ body.interview_questions = interviewQuestions;
235
+ if (Object.keys(body).length === 0) {
236
+ console.error("No update flags provided. Run `ish study update --help` for options.");
237
+ return;
238
+ }
239
+ const data = await client.put(`/studies/${resolveId(id)}`, body);
240
+ const result = data;
241
+ if (result.id)
242
+ result.alias = tagAlias(ALIAS_PREFIX.study, String(result.id));
243
+ formatStudyDetail(result, globals.json);
244
+ });
245
+ });
246
+ study
247
+ .command("delete")
248
+ .description("Delete a study")
249
+ .argument("<id>", "Study ID")
250
+ .addHelpText("after", "\nExamples:\n $ ish study delete <id>")
251
+ .action(async (id, _opts, cmd) => {
252
+ await withClient(cmd, async (client, globals) => {
253
+ await client.del(`/studies/${resolveId(id)}`);
254
+ output({ message: "Study deleted" }, globals.json);
255
+ });
256
+ });
257
+ study
258
+ .command("use")
259
+ .description("Set the active study (saved to ~/.ish/config.json)")
260
+ .argument("[id]", "Study alias or UUID")
261
+ .option("--clear", "Remove the active study from config")
262
+ .addHelpText("after", "\nExamples:\n $ ish study use s-b2c\n $ ish study use <uuid>\n $ ish study use --clear")
263
+ .action(async (id, opts, cmd) => {
264
+ if (opts.clear) {
265
+ const config = loadConfig();
266
+ delete config.study;
267
+ saveConfig(config);
268
+ console.error("Cleared active study.");
269
+ return;
270
+ }
271
+ if (!id) {
272
+ throw new Error("Provide a study alias or UUID, or use --clear.");
273
+ }
274
+ await withClient(cmd, async (client) => {
275
+ const rid = resolveId(id);
276
+ const data = await client.get(`/studies/${rid}`);
277
+ const config = loadConfig();
278
+ config.study = rid;
279
+ saveConfig(config);
280
+ console.error(`Active study set to "${data.name || rid}".`);
281
+ });
282
+ });
283
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * ish tester-profile — Manage tester profiles.
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerTesterProfileCommands(program: Command): void;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * ish tester-profile — Manage tester profiles.
3
+ */
4
+ import { withClient, readJsonFileOrStdin } from "../lib/command-helpers.js";
5
+ import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
6
+ import { formatTesterProfileList, output } from "../lib/output.js";
7
+ export function registerTesterProfileCommands(program) {
8
+ const profile = program
9
+ .command("tester-profile")
10
+ .description("Manage tester profiles");
11
+ profile
12
+ .command("list")
13
+ .description("List tester profiles (defaults to simulatable AI profiles)")
14
+ .option("--workspace <id>", "Filter by workspace ID")
15
+ .option("--search <query>", "Search by name or bio")
16
+ .option("--type <type>", "Profile type: ai, human, all (default: ai)", "ai")
17
+ .option("--gender <gender>", "Filter by gender")
18
+ .option("--country <country>", "Filter by country code (e.g. US, GB, SE)")
19
+ .option("--min-age <n>", "Minimum age")
20
+ .option("--max-age <n>", "Maximum age")
21
+ .option("--limit <n>", "Max results (default 50)", "50")
22
+ .option("--offset <n>", "Offset for pagination", "0")
23
+ .addHelpText("after", `
24
+ Examples:
25
+ $ ish tester-profile list
26
+ $ ish tester-profile list --search "engineer" --country US
27
+ $ ish tester-profile list --gender female --min-age 25 --max-age 40
28
+ $ ish tester-profile list --type all --json`)
29
+ .action(async (opts, cmd) => {
30
+ await withClient(cmd, async (client, globals) => {
31
+ const params = {
32
+ limit: opts.limit,
33
+ offset: opts.offset,
34
+ };
35
+ if (opts.workspace)
36
+ params.product_id = opts.workspace;
37
+ if (opts.search)
38
+ params.search = opts.search;
39
+ if (opts.type !== "all")
40
+ params.type = opts.type;
41
+ if (opts.gender)
42
+ params.gender = opts.gender;
43
+ if (opts.country)
44
+ params.country = opts.country;
45
+ if (opts.minAge)
46
+ params.min_age = opts.minAge;
47
+ if (opts.maxAge)
48
+ params.max_age = opts.maxAge;
49
+ const data = await client.get("/tester-profiles", params);
50
+ formatTesterProfileList(data, globals.json, parseInt(opts.limit, 10));
51
+ });
52
+ });
53
+ profile
54
+ .command("create")
55
+ .description("Create a tester profile")
56
+ .requiredOption("--file <path>", "JSON file with profile data")
57
+ .addHelpText("after", "\nExamples:\n $ ish tester-profile create --file profile.json\n\n Expected JSON: { \"name\": \"...\", \"age\": 30, \"gender\": \"...\", \"country\": \"US\", \"occupation\": \"...\", \"bio\": \"...\" }")
58
+ .action(async (opts, cmd) => {
59
+ await withClient(cmd, async (client, globals) => {
60
+ const body = await readJsonFileOrStdin(opts.file);
61
+ const data = await client.post("/tester-profiles", body);
62
+ const result = data;
63
+ if (result.id)
64
+ result.alias = tagAlias(ALIAS_PREFIX.testerProfile, String(result.id));
65
+ output(result, globals.json);
66
+ });
67
+ });
68
+ profile
69
+ .command("get")
70
+ .description("Get tester profile details")
71
+ .argument("<id>", "Tester profile ID")
72
+ .addHelpText("after", "\nExamples:\n $ ish tester-profile get <id>\n $ ish tester-profile get <id> --json")
73
+ .action(async (id, _opts, cmd) => {
74
+ await withClient(cmd, async (client, globals) => {
75
+ const data = await client.get(`/tester-profiles/${resolveId(id)}`);
76
+ const result = data;
77
+ if (result.id)
78
+ result.alias = tagAlias(ALIAS_PREFIX.testerProfile, String(result.id));
79
+ output(result, globals.json);
80
+ });
81
+ });
82
+ profile
83
+ .command("update")
84
+ .description("Update a tester profile")
85
+ .argument("<id>", "Tester profile ID")
86
+ .requiredOption("--file <path>", "JSON file with update data")
87
+ .addHelpText("after", "\nExamples:\n $ ish tester-profile update <id> --file updates.json")
88
+ .action(async (id, opts, cmd) => {
89
+ await withClient(cmd, async (client, globals) => {
90
+ const body = await readJsonFileOrStdin(opts.file);
91
+ const data = await client.put(`/tester-profiles/${resolveId(id)}`, body);
92
+ const result = data;
93
+ if (result.id)
94
+ result.alias = tagAlias(ALIAS_PREFIX.testerProfile, String(result.id));
95
+ output(result, globals.json);
96
+ });
97
+ });
98
+ profile
99
+ .command("delete")
100
+ .description("Delete a tester profile")
101
+ .argument("<id>", "Tester profile ID")
102
+ .addHelpText("after", "\nExamples:\n $ ish tester-profile delete <id>")
103
+ .action(async (id, _opts, cmd) => {
104
+ await withClient(cmd, async (client, globals) => {
105
+ await client.del(`/tester-profiles/${resolveId(id)}`);
106
+ output({ message: "Tester profile deleted" }, globals.json);
107
+ });
108
+ });
109
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * ish tester — Manage testers (usually created via `simulation run`).
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerTesterCommands(program: Command): void;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * ish tester — Manage testers (usually created via `simulation run`).
3
+ */
4
+ import { withClient, readJsonFileOrStdin } from "../lib/command-helpers.js";
5
+ import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
6
+ import { formatTesterDetail, output } from "../lib/output.js";
7
+ export function registerTesterCommands(program) {
8
+ const tester = program
9
+ .command("tester")
10
+ .description("Manage testers (usually created via `simulation run`)");
11
+ tester
12
+ .command("create")
13
+ .description("Create a tester (low-level)")
14
+ .requiredOption("--iteration <id>", "Iteration ID")
15
+ .requiredOption("--profile <id>", "Tester profile ID")
16
+ .option("--language <lang>", "Language code (e.g. en, sv)")
17
+ .option("--platform <platform>", "Platform (browser, android, figma, code)")
18
+ .option("--tester-type <type>", "Tester type (ai, human)", "ai")
19
+ .addHelpText("after", "\nExamples:\n $ ish tester create --iteration <id> --profile <id>\n $ ish tester create --iteration <id> --profile <id> --platform android --json")
20
+ .action(async (opts, cmd) => {
21
+ await withClient(cmd, async (client, globals) => {
22
+ const body = {
23
+ tester_profile_id: resolveId(opts.profile),
24
+ tester_type: opts.testerType,
25
+ ...(opts.language && { language: opts.language }),
26
+ ...(opts.platform && { platform: opts.platform }),
27
+ };
28
+ const data = await client.post(`/iterations/${resolveId(opts.iteration)}/testers`, body);
29
+ const result = data;
30
+ if (result.id)
31
+ result.alias = tagAlias(ALIAS_PREFIX.tester, String(result.id));
32
+ output(result, globals.json);
33
+ });
34
+ });
35
+ tester
36
+ .command("batch-create")
37
+ .description("Create multiple testers from a JSON file (low-level)")
38
+ .requiredOption("--iteration <id>", "Iteration ID")
39
+ .requiredOption("--file <path>", "JSON file with testers array")
40
+ .addHelpText("after", "\nExamples:\n $ ish tester batch-create --iteration <id> --file testers.json\n\n Expected JSON: [{ \"tester_profile_id\": \"<id>\", \"platform\": \"browser\" }, ...]")
41
+ .action(async (opts, cmd) => {
42
+ await withClient(cmd, async (client, globals) => {
43
+ const body = await readJsonFileOrStdin(opts.file);
44
+ const data = await client.post(`/iterations/${resolveId(opts.iteration)}/testers/batch`, body);
45
+ output(data, globals.json);
46
+ });
47
+ });
48
+ tester
49
+ .command("get")
50
+ .description("Get tester details and results")
51
+ .argument("<id>", "Tester ID")
52
+ .addHelpText("after", "\nExamples:\n $ ish tester get <id>\n $ ish tester get <id> --json")
53
+ .action(async (id, _opts, cmd) => {
54
+ await withClient(cmd, async (client, globals) => {
55
+ const data = await client.get(`/testers/${resolveId(id)}`);
56
+ const result = data;
57
+ if (result.id)
58
+ result.alias = tagAlias(ALIAS_PREFIX.tester, String(result.id));
59
+ formatTesterDetail(result, globals.json);
60
+ });
61
+ });
62
+ tester
63
+ .command("delete")
64
+ .description("Delete a tester")
65
+ .argument("<id>", "Tester ID")
66
+ .addHelpText("after", "\nExamples:\n $ ish tester delete <id>")
67
+ .action(async (id, _opts, cmd) => {
68
+ await withClient(cmd, async (client, globals) => {
69
+ await client.del(`/testers/${resolveId(id)}`);
70
+ output({ message: "Tester deleted" }, globals.json);
71
+ });
72
+ });
73
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * ish workspace — Manage workspaces (API: /products).
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerWorkspaceCommands(program: Command): void;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * ish workspace — Manage workspaces (API: /products).
3
+ */
4
+ import { withClient, getWebUrl, terminalLink } from "../lib/command-helpers.js";
5
+ import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
6
+ import { loadConfig, saveConfig } from "../config.js";
7
+ import { formatWorkspaceList, formatWorkspaceDetail, output } from "../lib/output.js";
8
+ export function registerWorkspaceCommands(program) {
9
+ const workspace = program
10
+ .command("workspace")
11
+ .description("Manage workspaces");
12
+ workspace
13
+ .command("list")
14
+ .description("List all workspaces")
15
+ .addHelpText("after", "\nExamples:\n $ ish workspace list\n $ ish workspace list --json")
16
+ .action(async (_opts, cmd) => {
17
+ await withClient(cmd, async (client, globals) => {
18
+ const data = await client.get("/products");
19
+ formatWorkspaceList(data, globals.json);
20
+ });
21
+ });
22
+ workspace
23
+ .command("create")
24
+ .description("Create a new workspace")
25
+ .requiredOption("--name <name>", "Workspace name")
26
+ .option("--description <description>", "Workspace description")
27
+ .option("--base-url <url>", "Default base URL")
28
+ .addHelpText("after", "\nExamples:\n $ ish workspace create --name \"My App\" --base-url https://example.com\n $ ish workspace create --name \"My App\" --json")
29
+ .action(async (opts, cmd) => {
30
+ await withClient(cmd, async (client, globals) => {
31
+ const body = {
32
+ name: opts.name,
33
+ ...(opts.description !== undefined && { description: opts.description }),
34
+ ...(opts.baseUrl !== undefined && { base_url: opts.baseUrl }),
35
+ };
36
+ const data = await client.post("/products", body);
37
+ const result = data;
38
+ if (result.id)
39
+ result.alias = tagAlias(ALIAS_PREFIX.workspace, String(result.id));
40
+ formatWorkspaceDetail(result, globals.json);
41
+ if (!globals.json && data.id) {
42
+ const url = getWebUrl(globals, `/${data.id}`);
43
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
44
+ }
45
+ });
46
+ });
47
+ workspace
48
+ .command("get")
49
+ .description("Get workspace details")
50
+ .argument("<id>", "Workspace ID")
51
+ .addHelpText("after", "\nExamples:\n $ ish workspace get <id>\n $ ish workspace get <id> --json")
52
+ .action(async (id, _opts, cmd) => {
53
+ await withClient(cmd, async (client, globals) => {
54
+ const rid = resolveId(id);
55
+ const data = await client.get(`/products/${rid}`);
56
+ const result = data;
57
+ if (result.id)
58
+ result.alias = tagAlias(ALIAS_PREFIX.workspace, String(result.id));
59
+ formatWorkspaceDetail(result, globals.json);
60
+ if (!globals.json) {
61
+ const url = getWebUrl(globals, `/${rid}`);
62
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
63
+ }
64
+ });
65
+ });
66
+ workspace
67
+ .command("update")
68
+ .description("Update a workspace")
69
+ .argument("<id>", "Workspace ID")
70
+ .option("--name <name>", "Workspace name")
71
+ .option("--description <description>", "Workspace description")
72
+ .option("--base-url <url>", "Default base URL")
73
+ .addHelpText("after", "\nExamples:\n $ ish workspace update <id> --name \"New Name\"\n $ ish workspace update <id> --base-url https://example.com --json")
74
+ .action(async (id, opts, cmd) => {
75
+ await withClient(cmd, async (client, globals) => {
76
+ const body = {};
77
+ if (opts.name !== undefined)
78
+ body.name = opts.name;
79
+ if (opts.description !== undefined)
80
+ body.description = opts.description;
81
+ if (opts.baseUrl !== undefined)
82
+ body.base_url = opts.baseUrl;
83
+ if (Object.keys(body).length === 0) {
84
+ console.error("No update flags provided. Run `ish workspace update --help` for options.");
85
+ return;
86
+ }
87
+ const data = await client.put(`/products/${resolveId(id)}`, body);
88
+ const result = data;
89
+ if (result.id)
90
+ result.alias = tagAlias(ALIAS_PREFIX.workspace, String(result.id));
91
+ formatWorkspaceDetail(result, globals.json);
92
+ });
93
+ });
94
+ workspace
95
+ .command("delete")
96
+ .description("Delete a workspace")
97
+ .argument("<id>", "Workspace ID")
98
+ .addHelpText("after", "\nExamples:\n $ ish workspace delete <id>")
99
+ .action(async (id, _opts, cmd) => {
100
+ await withClient(cmd, async (client, globals) => {
101
+ await client.del(`/products/${resolveId(id)}`);
102
+ output({ message: "Workspace deleted" }, globals.json);
103
+ });
104
+ });
105
+ workspace
106
+ .command("use")
107
+ .description("Set the active workspace (saved to ~/.ish/config.json)")
108
+ .argument("[id]", "Workspace alias or UUID")
109
+ .option("--clear", "Remove the active workspace from config")
110
+ .addHelpText("after", "\nExamples:\n $ ish workspace use w-6ec\n $ ish workspace use <uuid>\n $ ish workspace use --clear")
111
+ .action(async (id, opts, cmd) => {
112
+ if (opts.clear) {
113
+ const config = loadConfig();
114
+ delete config.workspace;
115
+ delete config.study;
116
+ saveConfig(config);
117
+ console.error("Cleared active workspace (and study).");
118
+ return;
119
+ }
120
+ if (!id) {
121
+ throw new Error("Provide a workspace alias or UUID, or use --clear.");
122
+ }
123
+ await withClient(cmd, async (client) => {
124
+ const rid = resolveId(id);
125
+ const data = await client.get(`/products/${rid}`);
126
+ const config = loadConfig();
127
+ config.workspace = rid;
128
+ delete config.study; // study belongs to workspace
129
+ saveConfig(config);
130
+ console.error(`Active workspace set to "${data.name || rid}".`);
131
+ });
132
+ });
133
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Config persistence for ~/.ish/config.json
3
+ */
4
+ export interface IshConfig {
5
+ access_token?: string;
6
+ refresh_token?: string;
7
+ token?: string;
8
+ workspace?: string;
9
+ study?: string;
10
+ [key: string]: string | undefined;
11
+ }
12
+ export declare function loadConfig(): IshConfig;
13
+ export declare function saveConfig(config: IshConfig): void;
package/dist/config.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Config persistence for ~/.ish/config.json
3
+ */
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import * as os from "node:os";
7
+ const CONFIG_DIR = path.join(os.homedir(), ".ish");
8
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
9
+ export function loadConfig() {
10
+ try {
11
+ if (fs.existsSync(CONFIG_FILE)) {
12
+ return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
13
+ }
14
+ }
15
+ catch {
16
+ // Corrupted config — ignore
17
+ }
18
+ return {};
19
+ }
20
+ export function saveConfig(config) {
21
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
22
+ const tmp = CONFIG_FILE + ".tmp";
23
+ fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
24
+ fs.renameSync(tmp, CONFIG_FILE);
25
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Localhost connect CLI — wraps cloudflared and registers with Ish backend.
3
+ */
4
+ export declare function runTunnel(port: number, tokenArg?: string, apiUrlArg?: string): Promise<void>;