speqs 0.3.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.
package/README.md CHANGED
@@ -2,26 +2,18 @@
2
2
 
3
3
  CLI tool to expose your localhost to [Speqs](https://speqs.io) for simulation testing.
4
4
 
5
- ## Prerequisites
6
-
7
- [cloudflared](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/) must be installed:
8
-
9
- - **macOS**: `brew install cloudflare/cloudflare/cloudflared`
10
- - **Debian/Ubuntu**: `sudo apt install cloudflared`
11
- - **Windows**: `scoop install cloudflared` or [download](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/)
12
-
13
5
  ## Install
14
6
 
15
7
  ### Quick install (recommended)
16
8
 
17
9
  **macOS / Linux:**
18
10
  ```bash
19
- curl -fsSL https://raw.githubusercontent.com/speqs-io/speqs-cli/main/install.sh | sh
11
+ curl -fsSL https://speqs.io/install.sh | sh
20
12
  ```
21
13
 
22
14
  **Windows (PowerShell):**
23
15
  ```powershell
24
- irm https://raw.githubusercontent.com/speqs-io/speqs-cli/main/install.ps1 | iex
16
+ irm https://speqs.io/install.ps1 | iex
25
17
  ```
26
18
 
27
19
  ### npm (all platforms)
@@ -40,7 +32,7 @@ brew install speqs
40
32
  ## Usage
41
33
 
42
34
  ```bash
43
- speqs tunnel <port>
35
+ speqs connect <port>
44
36
  ```
45
37
 
46
38
  ### Options
@@ -57,22 +49,19 @@ The CLI resolves your auth token in this order:
57
49
 
58
50
  1. `--token` CLI argument
59
51
  2. `SPEQS_TOKEN` environment variable
60
- 3. Saved token in `~/.speqs/config.json`
61
- 4. Interactive prompt (token is saved for future use)
62
-
63
- Find your token in the Speqs app under **Settings**.
52
+ 3. Saved token from `speqs login` (stored in `~/.speqs/config.json`)
64
53
 
65
54
  ## Examples
66
55
 
67
56
  ```bash
68
57
  # Expose port 3000
69
- speqs tunnel 3000
58
+ speqs connect 3000
70
59
 
71
60
  # With explicit token
72
- speqs tunnel 3000 --token YOUR_TOKEN
61
+ speqs connect 3000 --token YOUR_TOKEN
73
62
 
74
63
  # Using environment variable
75
- SPEQS_TOKEN=YOUR_TOKEN speqs tunnel 8080
64
+ SPEQS_TOKEN=YOUR_TOKEN speqs connect 8080
76
65
  ```
77
66
 
78
67
  ## License
@@ -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;