speqs 0.4.0 → 0.5.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.
@@ -0,0 +1,5 @@
1
+ /**
2
+ * speqs config — Manage simulation configs.
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerConfigCommands(program: Command): void;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * speqs config — Manage simulation configs.
3
+ */
4
+ import { withClient, readJsonFileOrStdin } from "../lib/command-helpers.js";
5
+ import { output, formatConfigList } from "../lib/output.js";
6
+ export function registerConfigCommands(program) {
7
+ const config = program
8
+ .command("config")
9
+ .description("Manage simulation configs");
10
+ config
11
+ .command("list")
12
+ .description("List all simulation configs")
13
+ .addHelpText("after", "\nExamples:\n $ speqs config list\n $ speqs config list --json")
14
+ .action(async (_opts, cmd) => {
15
+ await withClient(cmd, async (client, globals) => {
16
+ const data = await client.get("/dev/simulation-configs");
17
+ formatConfigList(data, globals.json);
18
+ });
19
+ });
20
+ config
21
+ .command("create")
22
+ .description("Create a simulation config")
23
+ .requiredOption("--file <path>", "JSON file with config data")
24
+ .action(async (opts, cmd) => {
25
+ await withClient(cmd, async (client, globals) => {
26
+ const body = await readJsonFileOrStdin(opts.file);
27
+ const data = await client.post("/dev/simulation-configs", body);
28
+ output(data, globals.json);
29
+ });
30
+ });
31
+ config
32
+ .command("get")
33
+ .description("Get simulation config details")
34
+ .argument("<id>", "Config ID")
35
+ .action(async (id, _opts, cmd) => {
36
+ await withClient(cmd, async (client, globals) => {
37
+ const data = await client.get(`/dev/simulation-configs/${id}`);
38
+ output(data, globals.json);
39
+ });
40
+ });
41
+ config
42
+ .command("schema")
43
+ .description("Get simulation config schema with defaults")
44
+ .action(async (_opts, cmd) => {
45
+ await withClient(cmd, async (client, globals) => {
46
+ const data = await client.get("/dev/simulation-configs/schema");
47
+ output(data, globals.json);
48
+ });
49
+ });
50
+ config
51
+ .command("update")
52
+ .description("Update a simulation config")
53
+ .argument("<id>", "Config ID")
54
+ .requiredOption("--file <path>", "JSON file with update data")
55
+ .action(async (id, opts, cmd) => {
56
+ await withClient(cmd, async (client, globals) => {
57
+ const body = await readJsonFileOrStdin(opts.file);
58
+ const data = await client.put(`/dev/simulation-configs/${id}`, body);
59
+ output(data, globals.json);
60
+ });
61
+ });
62
+ config
63
+ .command("delete")
64
+ .description("Delete a simulation config")
65
+ .argument("<id>", "Config ID")
66
+ .action(async (id, _opts, cmd) => {
67
+ await withClient(cmd, async (client, globals) => {
68
+ await client.del(`/dev/simulation-configs/${id}`);
69
+ output({ message: "Config deleted" }, globals.json);
70
+ });
71
+ });
72
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * speqs iteration — Manage iterations (usually created via `simulation run`).
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerIterationCommands(program: Command): void;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * speqs iteration — Manage iterations (usually created via `simulation run`).
3
+ */
4
+ import { withClient } from "../lib/command-helpers.js";
5
+ import { output, formatIterationList } from "../lib/output.js";
6
+ export function registerIterationCommands(program) {
7
+ const iteration = program
8
+ .command("iteration")
9
+ .description("Manage iterations (usually created via `simulation run`)");
10
+ iteration
11
+ .command("list")
12
+ .description("List iterations for a study")
13
+ .requiredOption("--study <id>", "Study ID")
14
+ .addHelpText("after", "\nExamples:\n $ speqs iteration list --study <id>\n $ speqs iteration list --study <id> --json")
15
+ .action(async (opts, cmd) => {
16
+ await withClient(cmd, async (client, globals) => {
17
+ const data = await client.get(`/studies/${opts.study}/iterations`);
18
+ formatIterationList(data, globals.json);
19
+ });
20
+ });
21
+ iteration
22
+ .command("create")
23
+ .description("Create a new iteration (low-level)")
24
+ .requiredOption("--study <id>", "Study ID")
25
+ .requiredOption("--name <name>", "Iteration name")
26
+ .option("--description <description>", "Iteration description")
27
+ .option("--details-json <json>", "Iteration details as JSON string")
28
+ .action(async (opts, cmd) => {
29
+ await withClient(cmd, async (client, globals) => {
30
+ const body = {
31
+ name: opts.name,
32
+ ...(opts.description && { description: opts.description }),
33
+ ...(opts.detailsJson && { details: JSON.parse(opts.detailsJson) }),
34
+ };
35
+ const data = await client.post(`/studies/${opts.study}/iterations`, body);
36
+ output(data, globals.json);
37
+ });
38
+ });
39
+ iteration
40
+ .command("get")
41
+ .description("Get iteration details")
42
+ .argument("<id>", "Iteration ID")
43
+ .action(async (id, _opts, cmd) => {
44
+ await withClient(cmd, async (client, globals) => {
45
+ const data = await client.get(`/iterations/${id}`);
46
+ output(data, globals.json);
47
+ });
48
+ });
49
+ iteration
50
+ .command("update")
51
+ .description("Update an iteration")
52
+ .argument("<id>", "Iteration ID")
53
+ .option("--name <name>", "Iteration name")
54
+ .option("--description <description>", "Iteration description")
55
+ .option("--details-json <json>", "Iteration details as JSON string")
56
+ .option("--label <label>", "Iteration label (uppercase letters)")
57
+ .action(async (id, opts, cmd) => {
58
+ await withClient(cmd, async (client, globals) => {
59
+ const body = {};
60
+ if (opts.name !== undefined)
61
+ body.name = opts.name;
62
+ if (opts.description !== undefined)
63
+ body.description = opts.description;
64
+ if (opts.detailsJson !== undefined)
65
+ body.details = JSON.parse(opts.detailsJson);
66
+ if (opts.label !== undefined)
67
+ body.label = opts.label;
68
+ const data = await client.put(`/iterations/${id}`, body);
69
+ output(data, globals.json);
70
+ });
71
+ });
72
+ iteration
73
+ .command("delete")
74
+ .description("Delete an iteration")
75
+ .argument("<id>", "Iteration ID")
76
+ .action(async (id, _opts, cmd) => {
77
+ await withClient(cmd, async (client, globals) => {
78
+ await client.del(`/iterations/${id}`);
79
+ output({ message: "Iteration deleted" }, globals.json);
80
+ });
81
+ });
82
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * speqs simulation — Run, monitor, and cancel simulations.
3
+ *
4
+ * Primary command: `speqs simulation run` — orchestrates the full flow:
5
+ * 1. Creates iteration (if not provided)
6
+ * 2. Creates testers from profiles
7
+ * 3. Starts simulations
8
+ */
9
+ import type { Command } from "commander";
10
+ export declare function registerSimulationCommands(program: Command): void;
@@ -0,0 +1,306 @@
1
+ /**
2
+ * speqs simulation — Run, monitor, and cancel simulations.
3
+ *
4
+ * Primary command: `speqs simulation run` — orchestrates the full flow:
5
+ * 1. Creates iteration (if not provided)
6
+ * 2. Creates testers from profiles
7
+ * 3. Starts simulations
8
+ */
9
+ import * as readline from "node:readline/promises";
10
+ import { withClient, getWebUrl, terminalLink } from "../lib/command-helpers.js";
11
+ import { output, outputError, formatSimulationPoll } from "../lib/output.js";
12
+ function parseMaxInteractions(value) {
13
+ const n = parseInt(value, 10);
14
+ if (isNaN(n) || n < 1)
15
+ throw new Error(`Invalid --max-interactions value: ${value}`);
16
+ return n;
17
+ }
18
+ export function registerSimulationCommands(program) {
19
+ const sim = program
20
+ .command("simulation")
21
+ .alias("sim")
22
+ .description("Run and monitor simulations");
23
+ // --- Primary: `simulation run` ---
24
+ sim
25
+ .command("run")
26
+ .description("Run simulations (creates iteration + testers + starts simulations)")
27
+ .requiredOption("--workspace <id>", "Workspace ID")
28
+ .requiredOption("--study <id>", "Study ID")
29
+ .option("--profiles <ids>", "Comma-separated tester profile IDs (auto-selected from last iteration if omitted)")
30
+ .option("--iteration <id>", "Use existing iteration (skip creation)")
31
+ .option("--iteration-name <name>", "Name for new iteration (forces creating a new iteration)")
32
+ .option("--platform <platform>", "Platform (browser, android, figma, code)")
33
+ .option("--url <url>", "URL to test")
34
+ .option("--screen-format <format>", "Screen format (mobile_portrait, desktop)")
35
+ .option("--max-interactions <n>", "Max interactions per tester")
36
+ .option("--language <lang>", "Language code (e.g. en, sv)")
37
+ .option("--locale <locale>", "Locale code (e.g. en-US)")
38
+ .option("-y, --yes", "Skip confirmation prompt")
39
+ .addHelpText("after", `
40
+ Examples:
41
+ # Re-run with same settings and profiles as last time:
42
+ $ speqs sim run --workspace W --study S
43
+
44
+ # Re-run with different profiles:
45
+ $ speqs sim run --workspace W --study S --profiles p1,p2
46
+
47
+ # Force a new iteration:
48
+ $ speqs sim run --workspace W --study S --iteration-name "Round 2"
49
+
50
+ # First run (no previous iteration):
51
+ $ speqs sim run --workspace W --study S --profiles p1,p2 --url https://example.com`)
52
+ .action(async (opts, cmd) => {
53
+ await withClient(cmd, async (client, globals) => {
54
+ const log = (msg) => { if (!globals.quiet)
55
+ console.error(msg); };
56
+ // Step 0: Resolve defaults from latest iteration
57
+ let resolvedPlatform = opts.platform;
58
+ let resolvedUrl = opts.url;
59
+ let resolvedScreenFormat = opts.screenFormat;
60
+ let profileIds = opts.profiles
61
+ ? opts.profiles.split(",").map((s) => s.trim()).filter(Boolean)
62
+ : [];
63
+ const profileNames = new Map();
64
+ let iterationId = opts.iteration;
65
+ if (!iterationId || profileIds.length === 0 || !resolvedPlatform || !resolvedScreenFormat) {
66
+ const study = await client.get(`/studies/${opts.study}`);
67
+ const iterations = study.iterations || [];
68
+ const latest = iterations.length > 0 ? iterations[iterations.length - 1] : null;
69
+ if (latest) {
70
+ const iterLabel = latest.label || latest.name || latest.id.slice(0, 8);
71
+ log(`Using iteration "${iterLabel}" as baseline`);
72
+ // Reuse iteration if not creating a new one
73
+ if (!iterationId && !opts.iterationName) {
74
+ iterationId = latest.id;
75
+ }
76
+ // Fill platform/url/screen-format from iteration details
77
+ const details = latest.details;
78
+ if (details) {
79
+ if (!resolvedPlatform)
80
+ resolvedPlatform = details.platform;
81
+ if (!resolvedUrl)
82
+ resolvedUrl = details.url;
83
+ if (!resolvedScreenFormat)
84
+ resolvedScreenFormat = details.screen_format || details.screenFormat;
85
+ }
86
+ // Auto-select profiles from latest iteration's testers
87
+ if (profileIds.length === 0 && latest.testers && latest.testers.length > 0) {
88
+ const seen = new Set();
89
+ for (const t of latest.testers) {
90
+ const pid = t.tester_profile_id || t.tester_profile?.id;
91
+ if (pid && !seen.has(pid)) {
92
+ seen.add(pid);
93
+ profileIds.push(pid);
94
+ const name = t.tester_profile?.name;
95
+ if (name)
96
+ profileNames.set(pid, name);
97
+ }
98
+ }
99
+ }
100
+ }
101
+ }
102
+ // Apply hardcoded fallbacks
103
+ resolvedPlatform = resolvedPlatform || "browser";
104
+ resolvedScreenFormat = resolvedScreenFormat || "desktop";
105
+ if (profileIds.length === 0) {
106
+ outputError(new Error("No profiles specified and no previous iteration to copy from. Use --profiles <ids>."), globals.json);
107
+ process.exit(1);
108
+ }
109
+ // Confirmation step — show resolved settings
110
+ if (!globals.json && !opts.yes) {
111
+ log("");
112
+ log(" Simulation settings:");
113
+ log(` Platform: ${resolvedPlatform}`);
114
+ log(` Screen format: ${resolvedScreenFormat}`);
115
+ if (resolvedUrl)
116
+ log(` URL: ${resolvedUrl}`);
117
+ if (opts.language)
118
+ log(` Language: ${opts.language}`);
119
+ log(` Profiles (${profileIds.length}):`);
120
+ for (const pid of profileIds) {
121
+ const name = profileNames.get(pid);
122
+ log(` - ${name ? `${name} (${pid})` : pid}`);
123
+ }
124
+ log("");
125
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
126
+ const answer = await rl.question(" Proceed? [Y/n] ");
127
+ rl.close();
128
+ if (answer && !["y", "yes", ""].includes(answer.toLowerCase().trim())) {
129
+ log("Aborted.");
130
+ process.exit(0);
131
+ }
132
+ log("");
133
+ }
134
+ // Step 1: Create or use existing iteration
135
+ if (!iterationId) {
136
+ const iterName = opts.iterationName || `CLI ${new Date().toISOString().slice(0, 16)}`;
137
+ const iterBody = {
138
+ name: iterName,
139
+ details: {
140
+ type: "interactive",
141
+ platform: resolvedPlatform,
142
+ url: resolvedUrl || "",
143
+ screen_format: resolvedScreenFormat,
144
+ ...(opts.locale && { locale: opts.locale }),
145
+ },
146
+ };
147
+ log(`Creating iteration "${iterName}"...`);
148
+ const iter = await client.post(`/studies/${opts.study}/iterations`, iterBody);
149
+ iterationId = iter.id;
150
+ log(`Created iteration "${iterName}"`);
151
+ }
152
+ // Step 2: Create testers from profiles
153
+ const testerInputs = profileIds.map((profileId) => ({
154
+ tester_profile_id: profileId,
155
+ tester_type: "ai",
156
+ status: "draft",
157
+ ...(opts.language && { language: opts.language }),
158
+ platform: resolvedPlatform,
159
+ }));
160
+ log(`Creating ${testerInputs.length} tester${testerInputs.length > 1 ? "s" : ""}...`);
161
+ const batchResult = await client.post(`/iterations/${iterationId}/testers/batch`, { testers: testerInputs });
162
+ const createdTesters = batchResult.testers;
163
+ log(`Created ${createdTesters.length} tester${createdTesters.length > 1 ? "s" : ""}`);
164
+ // Step 3: Start simulations (batch) — config resolved from profiles by backend
165
+ const simItems = createdTesters.map((t) => ({
166
+ study_id: opts.study,
167
+ tester_id: t.id,
168
+ ...(opts.language && { language: opts.language }),
169
+ ...(opts.locale && { locale: opts.locale }),
170
+ }));
171
+ log(`Starting ${simItems.length} simulation${simItems.length > 1 ? "s" : ""}...`);
172
+ const simResult = await client.post("/simulation/interactive/start/batch", {
173
+ product_id: opts.workspace,
174
+ simulations: simItems,
175
+ platform: resolvedPlatform,
176
+ ...(resolvedUrl && { url: resolvedUrl }),
177
+ screen_format: resolvedScreenFormat,
178
+ ...(opts.maxInteractions && { max_interactions: parseMaxInteractions(opts.maxInteractions) }),
179
+ }, { timeout: 60_000 });
180
+ if (globals.json) {
181
+ output({
182
+ iteration_id: iterationId,
183
+ testers: createdTesters.map((t) => ({ id: t.id, profile_name: t.tester_profile?.name })),
184
+ simulations: simResult.results,
185
+ }, true);
186
+ }
187
+ else {
188
+ // Human-readable summary (to stderr — not data)
189
+ for (let i = 0; i < simResult.results.length; i++) {
190
+ const tester = createdTesters[i];
191
+ const profileName = tester?.tester_profile?.name || "Unknown";
192
+ log(` ${profileName.padEnd(24)} QUEUED`);
193
+ }
194
+ const url = getWebUrl(globals, `/${opts.workspace}/${opts.study}/timeline`);
195
+ log(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
196
+ log(`Run \`speqs simulation poll --study ${opts.study}\` to check progress.`);
197
+ }
198
+ });
199
+ });
200
+ // --- Poll: check simulation progress ---
201
+ sim
202
+ .command("poll")
203
+ .description("Check simulation progress")
204
+ .argument("[job_id]", "Job ID (for single simulation)")
205
+ .option("--study <id>", "Study ID (poll all simulations for study)")
206
+ .addHelpText("after", "\nExamples:\n $ speqs simulation poll --study <study_id>\n $ speqs simulation poll <job_id> --json")
207
+ .action(async (jobId, opts, cmd) => {
208
+ await withClient(cmd, async (client, globals) => {
209
+ if (jobId) {
210
+ // Single job status
211
+ const data = await client.get(`/simulation/status/${jobId}`);
212
+ output(data, globals.json);
213
+ }
214
+ else if (opts.study) {
215
+ // Get study to find all testers with running/queued simulations
216
+ const study = await client.get(`/studies/${opts.study}`);
217
+ // Collect all testers across iterations
218
+ const allTesters = [];
219
+ for (const iteration of study.iterations || []) {
220
+ for (const tester of iteration.testers || []) {
221
+ allTesters.push({
222
+ id: tester.id,
223
+ status: tester.status,
224
+ tester_name: tester.tester_profile?.name || "Unknown",
225
+ interaction_count: Array.isArray(tester.interactions) ? tester.interactions.length : 0,
226
+ });
227
+ }
228
+ }
229
+ formatSimulationPoll(allTesters, globals.json);
230
+ if (!globals.json && study.product_id) {
231
+ const url = getWebUrl(globals, `/${study.product_id}/${opts.study}/timeline`);
232
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
233
+ }
234
+ }
235
+ else {
236
+ outputError(new Error("Provide a job_id argument or --study flag"), globals.json);
237
+ process.exit(1);
238
+ }
239
+ });
240
+ });
241
+ // --- Lower-level commands ---
242
+ sim
243
+ .command("start")
244
+ .description("Start a single interactive simulation (low-level)")
245
+ .requiredOption("--workspace <id>", "Workspace ID")
246
+ .requiredOption("--study <id>", "Study ID")
247
+ .requiredOption("--tester <id>", "Tester ID")
248
+ .option("--config <id>", "Simulation config ID (resolved from profile if omitted)")
249
+ .option("--platform <platform>", "Platform (browser, android, figma, code)", "browser")
250
+ .option("--url <url>", "URL to test")
251
+ .option("--screen-format <format>", "Screen format (mobile_portrait, desktop)")
252
+ .option("--max-interactions <n>", "Max interactions")
253
+ .option("--language <lang>", "Language code")
254
+ .option("--locale <locale>", "Locale code")
255
+ .action(async (opts, cmd) => {
256
+ await withClient(cmd, async (client, globals) => {
257
+ const body = {
258
+ product_id: opts.workspace,
259
+ study_id: opts.study,
260
+ tester_id: opts.tester,
261
+ ...(opts.config && { config_id: opts.config }),
262
+ platform: opts.platform,
263
+ ...(opts.url && { url: opts.url }),
264
+ ...(opts.screenFormat && { screen_format: opts.screenFormat }),
265
+ ...(opts.maxInteractions && { max_interactions: parseMaxInteractions(opts.maxInteractions) }),
266
+ ...(opts.language && { language: opts.language }),
267
+ ...(opts.locale && { locale: opts.locale }),
268
+ };
269
+ const data = await client.post("/simulation/interactive/start", body);
270
+ output(data, globals.json);
271
+ });
272
+ });
273
+ sim
274
+ .command("start-media")
275
+ .description("Start a media simulation (low-level)")
276
+ .requiredOption("--workspace <id>", "Workspace ID")
277
+ .requiredOption("--study <id>", "Study ID")
278
+ .requiredOption("--tester <id>", "Tester ID")
279
+ .option("--config <id>", "Simulation config ID (resolved from profile if omitted)")
280
+ .option("--max-interactions <n>", "Max interactions")
281
+ .option("--language <lang>", "Language code")
282
+ .action(async (opts, cmd) => {
283
+ await withClient(cmd, async (client, globals) => {
284
+ const body = {
285
+ product_id: opts.workspace,
286
+ study_id: opts.study,
287
+ tester_id: opts.tester,
288
+ ...(opts.config && { config_id: opts.config }),
289
+ ...(opts.maxInteractions && { max_interactions: parseMaxInteractions(opts.maxInteractions) }),
290
+ ...(opts.language && { language: opts.language }),
291
+ };
292
+ const data = await client.post("/simulation/media/start", body);
293
+ output(data, globals.json);
294
+ });
295
+ });
296
+ sim
297
+ .command("cancel")
298
+ .description("Cancel a simulation")
299
+ .argument("<job_id>", "Job ID")
300
+ .action(async (jobId, _opts, cmd) => {
301
+ await withClient(cmd, async (client, globals) => {
302
+ const data = await client.post(`/simulation/cancel/${jobId}`);
303
+ output(data, globals.json);
304
+ });
305
+ });
306
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * speqs study — Manage studies.
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerStudyCommands(program: Command): void;
@@ -0,0 +1,131 @@
1
+ /**
2
+ * speqs study — Manage studies.
3
+ */
4
+ import { withClient, getWebUrl, terminalLink } from "../lib/command-helpers.js";
5
+ import { formatStudyList, formatStudyDetail, formatStudyResults, output } from "../lib/output.js";
6
+ export function registerStudyCommands(program) {
7
+ const study = program
8
+ .command("study")
9
+ .description("Manage studies");
10
+ study
11
+ .command("list")
12
+ .description("List studies for a workspace")
13
+ .requiredOption("--workspace <id>", "Workspace ID")
14
+ .addHelpText("after", "\nExamples:\n $ speqs study list --workspace <id>\n $ speqs study list --workspace <id> --json")
15
+ .action(async (opts, cmd) => {
16
+ await withClient(cmd, async (client, globals) => {
17
+ const data = await client.get(`/products/${opts.workspace}/studies`);
18
+ formatStudyList(data, globals.json);
19
+ });
20
+ });
21
+ study
22
+ .command("create")
23
+ .description("Create a new study")
24
+ .requiredOption("--workspace <id>", "Workspace ID")
25
+ .requiredOption("--name <name>", "Study name")
26
+ .option("--description <description>", "Study description")
27
+ .option("--modality <modality>", "Study modality (interactive, video, audio, text, image)")
28
+ .option("--content-type <type>", "Content type")
29
+ .addHelpText("after", "\nExamples:\n $ speqs study create --workspace <id> --name \"Onboarding UX\" --modality interactive\n $ speqs study create --workspace <id> --name \"Landing Page\" --json")
30
+ .action(async (opts, cmd) => {
31
+ await withClient(cmd, async (client, globals) => {
32
+ const body = {
33
+ product_id: opts.workspace,
34
+ name: opts.name,
35
+ ...(opts.description && { description: opts.description }),
36
+ ...(opts.modality && { modality: opts.modality }),
37
+ ...(opts.contentType && { content_type: opts.contentType }),
38
+ };
39
+ const data = await client.post(`/products/${opts.workspace}/studies`, body);
40
+ formatStudyDetail(data, globals.json);
41
+ if (!globals.json && data.id) {
42
+ const url = getWebUrl(globals, `/${opts.workspace}/${data.id}/overview`);
43
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
44
+ }
45
+ });
46
+ });
47
+ study
48
+ .command("generate")
49
+ .description("Generate a study from a problem description using AI")
50
+ .requiredOption("--workspace <id>", "Workspace ID")
51
+ .requiredOption("--problem <description>", "Problem description (what you want to understand)")
52
+ .option("--target-url <url>", "URL of the product to test")
53
+ .addHelpText("after", "\nExamples:\n $ speqs study generate --workspace <id> --problem \"How do users navigate the onboarding flow?\"\n $ speqs study generate --workspace <id> --problem \"Test checkout\" --target-url https://example.com --json")
54
+ .action(async (opts, cmd) => {
55
+ await withClient(cmd, async (client, globals) => {
56
+ const body = {
57
+ problem_description: opts.problem,
58
+ ...(opts.targetUrl && { target_url: opts.targetUrl }),
59
+ };
60
+ const data = await client.post(`/products/${opts.workspace}/studies/generate`, body);
61
+ formatStudyDetail(data, globals.json);
62
+ if (!globals.json && data.id) {
63
+ const url = getWebUrl(globals, `/${opts.workspace}/${data.id}/overview`);
64
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
65
+ }
66
+ });
67
+ });
68
+ study
69
+ .command("get")
70
+ .description("Get study overview (assignments, questions, testers)")
71
+ .argument("<id>", "Study ID")
72
+ .addHelpText("after", "\nExamples:\n $ speqs study get <id>\n $ speqs study get <id> --json")
73
+ .action(async (id, _opts, cmd) => {
74
+ await withClient(cmd, async (client, globals) => {
75
+ const data = await client.get(`/studies/${id}`);
76
+ formatStudyDetail(data, globals.json);
77
+ if (!globals.json && data.product_id) {
78
+ const url = getWebUrl(globals, `/${data.product_id}/${id}/overview`);
79
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
80
+ }
81
+ });
82
+ });
83
+ study
84
+ .command("results")
85
+ .description("View aggregated results (sentiment, interview answers)")
86
+ .argument("<id>", "Study ID")
87
+ .addHelpText("after", "\nExamples:\n $ speqs study results <id>\n $ speqs study results <id> --json")
88
+ .action(async (id, _opts, cmd) => {
89
+ await withClient(cmd, async (client, globals) => {
90
+ const data = await client.get(`/studies/${id}`);
91
+ formatStudyResults(data, globals.json);
92
+ if (!globals.json && data.product_id) {
93
+ const url = getWebUrl(globals, `/${data.product_id}/${id}/overview`);
94
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
95
+ }
96
+ });
97
+ });
98
+ study
99
+ .command("update")
100
+ .description("Update a study")
101
+ .argument("<id>", "Study ID")
102
+ .option("--name <name>", "Study name")
103
+ .option("--description <description>", "Study description")
104
+ .option("--status <status>", "Study status (draft, running, completed)")
105
+ .option("--modality <modality>", "Study modality")
106
+ .action(async (id, opts, cmd) => {
107
+ await withClient(cmd, async (client, globals) => {
108
+ const body = {};
109
+ if (opts.name !== undefined)
110
+ body.name = opts.name;
111
+ if (opts.description !== undefined)
112
+ body.description = opts.description;
113
+ if (opts.status !== undefined)
114
+ body.status = opts.status;
115
+ if (opts.modality !== undefined)
116
+ body.modality = opts.modality;
117
+ const data = await client.put(`/studies/${id}`, body);
118
+ formatStudyDetail(data, globals.json);
119
+ });
120
+ });
121
+ study
122
+ .command("delete")
123
+ .description("Delete a study")
124
+ .argument("<id>", "Study ID")
125
+ .action(async (id, _opts, cmd) => {
126
+ await withClient(cmd, async (client, globals) => {
127
+ await client.del(`/studies/${id}`);
128
+ output({ message: "Study deleted" }, globals.json);
129
+ });
130
+ });
131
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * speqs tester-profile — Manage tester profiles.
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerTesterProfileCommands(program: Command): void;