speqs 0.4.0 → 0.5.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.
@@ -0,0 +1,170 @@
1
+ /**
2
+ * speqs study — Manage studies.
3
+ */
4
+ import { withClient, getWebUrl, terminalLink } from "../lib/command-helpers.js";
5
+ import { resolveId } from "../lib/alias-store.js";
6
+ import { formatStudyList, formatStudyDetail, formatStudyResults, output } from "../lib/output.js";
7
+ export function registerStudyCommands(program) {
8
+ const study = program
9
+ .command("study")
10
+ .description("Manage studies");
11
+ study
12
+ .command("list")
13
+ .description("List studies for a workspace")
14
+ .requiredOption("--workspace <id>", "Workspace ID")
15
+ .addHelpText("after", "\nExamples:\n $ speqs study list --workspace <id>\n $ speqs study list --workspace <id> --json")
16
+ .action(async (opts, cmd) => {
17
+ await withClient(cmd, async (client, globals) => {
18
+ const data = await client.get(`/products/${resolveId(opts.workspace)}/studies`);
19
+ formatStudyList(data, globals.json);
20
+ });
21
+ });
22
+ study
23
+ .command("create")
24
+ .description("Create a new study")
25
+ .requiredOption("--workspace <id>", "Workspace ID")
26
+ .requiredOption("--name <name>", "Study name")
27
+ .option("--description <description>", "Study description")
28
+ .option("--modality <modality>", "Study modality (interactive, video, audio, text, image)")
29
+ .option("--content-type <type>", "Content type")
30
+ .option("--assignments <json>", "JSON array of assignments, e.g. '[{\"name\":\"Task\",\"instructions\":\"Do something\"}]'")
31
+ .option("--questions <json>", "JSON array of interview questions, e.g. '[{\"question\":\"How was it?\",\"type\":\"text\",\"timing\":\"after\"}]'")
32
+ .addHelpText("after", `
33
+ Examples:
34
+ # Create with assignments (required for simulations):
35
+ $ speqs study create --workspace <id> --name "Onboarding UX" --modality interactive \\
36
+ --assignments '[{"name":"Sign up","instructions":"Complete the signup flow"}]'
37
+
38
+ # Create with assignments and interview questions:
39
+ $ speqs study create --workspace <id> --name "Checkout" --modality interactive \\
40
+ --assignments '[{"name":"Buy item","instructions":"Add an item to cart and checkout"}]' \\
41
+ --questions '[{"question":"How easy was checkout?","type":"slider","timing":"after","min":0,"max":10}]'
42
+
43
+ # Minimal (note: assignments are needed before running simulations):
44
+ $ speqs study create --workspace <id> --name "Landing Page" --json`)
45
+ .action(async (opts, cmd) => {
46
+ await withClient(cmd, async (client, globals) => {
47
+ let assignments;
48
+ let interviewQuestions;
49
+ if (opts.assignments) {
50
+ try {
51
+ assignments = JSON.parse(opts.assignments);
52
+ }
53
+ catch {
54
+ throw new Error("Invalid --assignments JSON");
55
+ }
56
+ }
57
+ if (opts.questions) {
58
+ try {
59
+ interviewQuestions = JSON.parse(opts.questions);
60
+ }
61
+ catch {
62
+ throw new Error("Invalid --questions JSON");
63
+ }
64
+ }
65
+ const resolvedWs = resolveId(opts.workspace);
66
+ const body = {
67
+ product_id: resolvedWs,
68
+ name: opts.name,
69
+ ...(opts.description && { description: opts.description }),
70
+ ...(opts.modality && { modality: opts.modality }),
71
+ ...(opts.contentType && { content_type: opts.contentType }),
72
+ ...(assignments && { assignments }),
73
+ ...(interviewQuestions && { interview_questions: interviewQuestions }),
74
+ };
75
+ const data = await client.post(`/products/${resolvedWs}/studies`, body);
76
+ formatStudyDetail(data, globals.json);
77
+ if (!globals.json && data.id) {
78
+ const url = getWebUrl(globals, `/${resolvedWs}/${data.id}/overview`);
79
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
80
+ }
81
+ });
82
+ });
83
+ study
84
+ .command("generate")
85
+ .description("Generate a study from a problem description using AI")
86
+ .requiredOption("--workspace <id>", "Workspace ID")
87
+ .requiredOption("--problem <description>", "Problem description (what you want to understand)")
88
+ .option("--target-url <url>", "URL of the product to test")
89
+ .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")
90
+ .action(async (opts, cmd) => {
91
+ await withClient(cmd, async (client, globals) => {
92
+ const body = {
93
+ problem_description: opts.problem,
94
+ ...(opts.targetUrl && { target_url: opts.targetUrl }),
95
+ };
96
+ const resolvedWs = resolveId(opts.workspace);
97
+ const data = await client.post(`/products/${resolvedWs}/studies/generate`, body);
98
+ formatStudyDetail(data, globals.json);
99
+ if (!globals.json && data.id) {
100
+ const url = getWebUrl(globals, `/${resolvedWs}/${data.id}/overview`);
101
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
102
+ }
103
+ });
104
+ });
105
+ study
106
+ .command("get")
107
+ .description("Get study overview (assignments, questions, testers)")
108
+ .argument("<id>", "Study ID")
109
+ .addHelpText("after", "\nExamples:\n $ speqs study get <id>\n $ speqs study get <id> --json")
110
+ .action(async (id, _opts, cmd) => {
111
+ await withClient(cmd, async (client, globals) => {
112
+ const rid = resolveId(id);
113
+ const data = await client.get(`/studies/${rid}`);
114
+ formatStudyDetail(data, globals.json);
115
+ if (!globals.json && data.product_id) {
116
+ const url = getWebUrl(globals, `/${data.product_id}/${rid}/overview`);
117
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
118
+ }
119
+ });
120
+ });
121
+ study
122
+ .command("results")
123
+ .description("View aggregated results (sentiment, interview answers)")
124
+ .argument("<id>", "Study ID")
125
+ .addHelpText("after", "\nExamples:\n $ speqs study results <id>\n $ speqs study results <id> --json")
126
+ .action(async (id, _opts, cmd) => {
127
+ await withClient(cmd, async (client, globals) => {
128
+ const rid = resolveId(id);
129
+ const data = await client.get(`/studies/${rid}`);
130
+ formatStudyResults(data, globals.json);
131
+ if (!globals.json && data.product_id) {
132
+ const url = getWebUrl(globals, `/${data.product_id}/${rid}/overview`);
133
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
134
+ }
135
+ });
136
+ });
137
+ study
138
+ .command("update")
139
+ .description("Update a study")
140
+ .argument("<id>", "Study ID")
141
+ .option("--name <name>", "Study name")
142
+ .option("--description <description>", "Study description")
143
+ .option("--status <status>", "Study status (draft, running, completed)")
144
+ .option("--modality <modality>", "Study modality")
145
+ .action(async (id, opts, cmd) => {
146
+ await withClient(cmd, async (client, globals) => {
147
+ const body = {};
148
+ if (opts.name !== undefined)
149
+ body.name = opts.name;
150
+ if (opts.description !== undefined)
151
+ body.description = opts.description;
152
+ if (opts.status !== undefined)
153
+ body.status = opts.status;
154
+ if (opts.modality !== undefined)
155
+ body.modality = opts.modality;
156
+ const data = await client.put(`/studies/${resolveId(id)}`, body);
157
+ formatStudyDetail(data, globals.json);
158
+ });
159
+ });
160
+ study
161
+ .command("delete")
162
+ .description("Delete a study")
163
+ .argument("<id>", "Study ID")
164
+ .action(async (id, _opts, cmd) => {
165
+ await withClient(cmd, async (client, globals) => {
166
+ await client.del(`/studies/${resolveId(id)}`);
167
+ output({ message: "Study deleted" }, globals.json);
168
+ });
169
+ });
170
+ }
@@ -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;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * speqs tester-profile — Manage tester profiles.
3
+ */
4
+ import { withClient, readJsonFileOrStdin } from "../lib/command-helpers.js";
5
+ import { resolveId } 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
+ $ speqs tester-profile list
26
+ $ speqs tester-profile list --search "engineer" --country US
27
+ $ speqs tester-profile list --gender female --min-age 25 --max-age 40
28
+ $ speqs 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
+ .action(async (opts, cmd) => {
58
+ await withClient(cmd, async (client, globals) => {
59
+ const body = await readJsonFileOrStdin(opts.file);
60
+ const data = await client.post("/tester-profiles", body);
61
+ output(data, globals.json);
62
+ });
63
+ });
64
+ profile
65
+ .command("get")
66
+ .description("Get tester profile details")
67
+ .argument("<id>", "Tester profile ID")
68
+ .action(async (id, _opts, cmd) => {
69
+ await withClient(cmd, async (client, globals) => {
70
+ const data = await client.get(`/tester-profiles/${resolveId(id)}`);
71
+ output(data, globals.json);
72
+ });
73
+ });
74
+ profile
75
+ .command("update")
76
+ .description("Update a tester profile")
77
+ .argument("<id>", "Tester profile ID")
78
+ .requiredOption("--file <path>", "JSON file with update data")
79
+ .action(async (id, opts, cmd) => {
80
+ await withClient(cmd, async (client, globals) => {
81
+ const body = await readJsonFileOrStdin(opts.file);
82
+ const data = await client.put(`/tester-profiles/${resolveId(id)}`, body);
83
+ output(data, globals.json);
84
+ });
85
+ });
86
+ profile
87
+ .command("delete")
88
+ .description("Delete a tester profile")
89
+ .argument("<id>", "Tester profile ID")
90
+ .action(async (id, _opts, cmd) => {
91
+ await withClient(cmd, async (client, globals) => {
92
+ await client.del(`/tester-profiles/${resolveId(id)}`);
93
+ output({ message: "Tester profile deleted" }, globals.json);
94
+ });
95
+ });
96
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * speqs 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,64 @@
1
+ /**
2
+ * speqs tester — Manage testers (usually created via `simulation run`).
3
+ */
4
+ import { withClient, readJsonFileOrStdin } from "../lib/command-helpers.js";
5
+ import { resolveId } 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
+ .action(async (opts, cmd) => {
20
+ await withClient(cmd, async (client, globals) => {
21
+ const body = {
22
+ tester_profile_id: resolveId(opts.profile),
23
+ tester_type: opts.testerType,
24
+ ...(opts.language && { language: opts.language }),
25
+ ...(opts.platform && { platform: opts.platform }),
26
+ };
27
+ const data = await client.post(`/iterations/${resolveId(opts.iteration)}/testers`, body);
28
+ output(data, globals.json);
29
+ });
30
+ });
31
+ tester
32
+ .command("batch-create")
33
+ .description("Create multiple testers from a JSON file (low-level)")
34
+ .requiredOption("--iteration <id>", "Iteration ID")
35
+ .requiredOption("--file <path>", "JSON file with testers array")
36
+ .action(async (opts, cmd) => {
37
+ await withClient(cmd, async (client, globals) => {
38
+ const body = await readJsonFileOrStdin(opts.file);
39
+ const data = await client.post(`/iterations/${resolveId(opts.iteration)}/testers/batch`, body);
40
+ output(data, globals.json);
41
+ });
42
+ });
43
+ tester
44
+ .command("get")
45
+ .description("Get tester details and results")
46
+ .argument("<id>", "Tester ID")
47
+ .addHelpText("after", "\nExamples:\n $ speqs tester get <id>\n $ speqs tester get <id> --json")
48
+ .action(async (id, _opts, cmd) => {
49
+ await withClient(cmd, async (client, globals) => {
50
+ const data = await client.get(`/testers/${resolveId(id)}`);
51
+ formatTesterDetail(data, globals.json);
52
+ });
53
+ });
54
+ tester
55
+ .command("delete")
56
+ .description("Delete a tester")
57
+ .argument("<id>", "Tester ID")
58
+ .action(async (id, _opts, cmd) => {
59
+ await withClient(cmd, async (client, globals) => {
60
+ await client.del(`/testers/${resolveId(id)}`);
61
+ output({ message: "Tester deleted" }, globals.json);
62
+ });
63
+ });
64
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * speqs workspace — Manage workspaces (API: /products).
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerWorkspaceCommands(program: Command): void;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * speqs workspace — Manage workspaces (API: /products).
3
+ */
4
+ import { withClient, getWebUrl, terminalLink } from "../lib/command-helpers.js";
5
+ import { resolveId } from "../lib/alias-store.js";
6
+ import { formatWorkspaceList, formatWorkspaceDetail, output } from "../lib/output.js";
7
+ export function registerWorkspaceCommands(program) {
8
+ const workspace = program
9
+ .command("workspace")
10
+ .description("Manage workspaces");
11
+ workspace
12
+ .command("list")
13
+ .description("List all workspaces")
14
+ .addHelpText("after", "\nExamples:\n $ speqs workspace list\n $ speqs workspace list --json")
15
+ .action(async (_opts, cmd) => {
16
+ await withClient(cmd, async (client, globals) => {
17
+ const data = await client.get("/products");
18
+ formatWorkspaceList(data, globals.json);
19
+ });
20
+ });
21
+ workspace
22
+ .command("create")
23
+ .description("Create a new workspace")
24
+ .requiredOption("--name <name>", "Workspace name")
25
+ .option("--description <description>", "Workspace description")
26
+ .option("--base-url <url>", "Default base URL")
27
+ .addHelpText("after", "\nExamples:\n $ speqs workspace create --name \"My App\" --base-url https://example.com\n $ speqs workspace create --name \"My App\" --json")
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.baseUrl && { base_url: opts.baseUrl }),
34
+ };
35
+ const data = await client.post("/products", body);
36
+ formatWorkspaceDetail(data, globals.json);
37
+ if (!globals.json && data.id) {
38
+ const url = getWebUrl(globals, `/${data.id}`);
39
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
40
+ }
41
+ });
42
+ });
43
+ workspace
44
+ .command("get")
45
+ .description("Get workspace details")
46
+ .argument("<id>", "Workspace ID")
47
+ .action(async (id, _opts, cmd) => {
48
+ await withClient(cmd, async (client, globals) => {
49
+ const rid = resolveId(id);
50
+ const data = await client.get(`/products/${rid}`);
51
+ formatWorkspaceDetail(data, globals.json);
52
+ if (!globals.json) {
53
+ const url = getWebUrl(globals, `/${rid}`);
54
+ console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
55
+ }
56
+ });
57
+ });
58
+ workspace
59
+ .command("update")
60
+ .description("Update a workspace")
61
+ .argument("<id>", "Workspace ID")
62
+ .option("--name <name>", "Workspace name")
63
+ .option("--description <description>", "Workspace description")
64
+ .option("--base-url <url>", "Default base URL")
65
+ .action(async (id, opts, cmd) => {
66
+ await withClient(cmd, async (client, globals) => {
67
+ const body = {};
68
+ if (opts.name !== undefined)
69
+ body.name = opts.name;
70
+ if (opts.description !== undefined)
71
+ body.description = opts.description;
72
+ if (opts.baseUrl !== undefined)
73
+ body.base_url = opts.baseUrl;
74
+ const data = await client.put(`/products/${resolveId(id)}`, body);
75
+ formatWorkspaceDetail(data, globals.json);
76
+ });
77
+ });
78
+ workspace
79
+ .command("delete")
80
+ .description("Delete a workspace")
81
+ .argument("<id>", "Workspace ID")
82
+ .action(async (id, _opts, cmd) => {
83
+ await withClient(cmd, async (client, globals) => {
84
+ await client.del(`/products/${resolveId(id)}`);
85
+ output({ message: "Workspace deleted" }, globals.json);
86
+ });
87
+ });
88
+ }
package/dist/index.js CHANGED
@@ -4,18 +4,35 @@ import { runTunnel } from "./connect.js";
4
4
  import { login, getAppUrl } from "./auth.js";
5
5
  import { loadConfig, saveConfig } from "./config.js";
6
6
  import { upgrade } from "./upgrade.js";
7
+ import { registerWorkspaceCommands } from "./commands/workspace.js";
8
+ import { registerStudyCommands } from "./commands/study.js";
9
+ import { registerIterationCommands } from "./commands/iteration.js";
10
+ import { registerTesterProfileCommands } from "./commands/tester-profile.js";
11
+ import { registerTesterCommands } from "./commands/tester.js";
12
+ import { registerSimulationCommands } from "./commands/simulation.js";
13
+ import { registerConfigCommands } from "./commands/config.js";
7
14
  import pkg from "../package.json" with { type: "json" };
8
15
  const { version } = pkg;
9
16
  program
10
17
  .name("speqs")
11
- .description("Speqs CLI tools")
18
+ .description("Speqs CLI — manage workspaces, studies, simulations, and more")
12
19
  .version(version);
20
+ // Global options
21
+ program
22
+ .option("-t, --token <token>", "Auth token (or set SPEQS_TOKEN env var)")
23
+ .option("--api-url <url>", "Backend API URL (default: SPEQS_API_URL or https://api.speqs.io)")
24
+ .addOption(new Option("--dev", "Use local dev API (http://localhost:8000)").hideHelp())
25
+ .option("--json", "Output as JSON (for programmatic use)")
26
+ .option("-q, --quiet", "Suppress progress messages on stderr");
27
+ // --- Inline commands (from upstream) ---
13
28
  program
14
29
  .command("login")
15
30
  .description("Authenticate with Speqs via your browser")
16
- .action(async () => {
31
+ .action(async (_opts, cmd) => {
17
32
  try {
18
- const tokens = await login(getAppUrl());
33
+ const globals = cmd.optsWithGlobals();
34
+ const appUrl = globals.dev ? "http://localhost:3000" : getAppUrl();
35
+ const tokens = await login(appUrl);
19
36
  const config = loadConfig();
20
37
  config.access_token = tokens.accessToken;
21
38
  config.refresh_token = tokens.refreshToken;
@@ -42,18 +59,24 @@ program
42
59
  .command("connect")
43
60
  .description("Expose your localhost to Speqs via a Cloudflare tunnel")
44
61
  .argument("<port>", "Local port to connect (e.g. 3000)")
45
- .option("-t, --token <token>", "Auth token (or set SPEQS_TOKEN, or save via interactive prompt)")
46
- .option("--api-url <url>", "Backend API URL (default: SPEQS_API_URL or https://api.speqs.io)")
47
- .addOption(new Option("--dev", "Use local dev API (http://localhost:8000)").hideHelp())
48
- .action(async (port, options) => {
62
+ .action(async (port, _opts, cmd) => {
49
63
  const portNum = parseInt(port, 10);
50
64
  if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
51
65
  console.error(`Invalid port: ${port}`);
52
66
  process.exit(1);
53
67
  }
54
- const apiUrl = options.dev ? "http://localhost:8000" : options.apiUrl;
55
- await runTunnel(portNum, options.token, apiUrl);
68
+ const globals = cmd.optsWithGlobals();
69
+ const apiUrl = globals.dev ? "http://localhost:8000" : globals.apiUrl;
70
+ await runTunnel(portNum, globals.token, apiUrl);
56
71
  });
72
+ // --- Modular command groups ---
73
+ registerWorkspaceCommands(program);
74
+ registerStudyCommands(program);
75
+ registerIterationCommands(program);
76
+ registerTesterProfileCommands(program);
77
+ registerTesterCommands(program);
78
+ registerSimulationCommands(program);
79
+ registerConfigCommands(program);
57
80
  program
58
81
  .command("upgrade")
59
82
  .description("Update speqs to the latest version")
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Short alias system for entity IDs.
3
+ *
4
+ * Maps short aliases like tp1, w2, s3 to full UUIDs.
5
+ * Aliases are regenerated on every list call and persisted to ~/.speqs/aliases.json.
6
+ */
7
+ /** Entity type → alias prefix */
8
+ export declare const ALIAS_PREFIX: {
9
+ readonly workspace: "w";
10
+ readonly study: "s";
11
+ readonly iteration: "i";
12
+ readonly testerProfile: "tp";
13
+ readonly tester: "t";
14
+ readonly config: "c";
15
+ readonly job: "j";
16
+ };
17
+ /**
18
+ * Save aliases for a list of IDs under the given prefix.
19
+ * Clears all existing aliases for that prefix before saving.
20
+ * Numbers start at `startIndex` (default 1) to support pagination.
21
+ */
22
+ export declare function saveAliases(prefix: string, ids: string[], startIndex?: number): void;
23
+ /**
24
+ * Build a uuid→alias map for a given prefix (used by formatters to display aliases).
25
+ */
26
+ export declare function getAliasMap(prefix: string): Map<string, string>;
27
+ /**
28
+ * Resolve a short alias to a full UUID, or return the input as-is if it's not an alias.
29
+ */
30
+ export declare function resolveId(input: string): string;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Short alias system for entity IDs.
3
+ *
4
+ * Maps short aliases like tp1, w2, s3 to full UUIDs.
5
+ * Aliases are regenerated on every list call and persisted to ~/.speqs/aliases.json.
6
+ */
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import * as os from "node:os";
10
+ const ALIASES_FILE = path.join(os.homedir(), ".speqs", "aliases.json");
11
+ /** Entity type → alias prefix */
12
+ export const ALIAS_PREFIX = {
13
+ workspace: "w",
14
+ study: "s",
15
+ iteration: "i",
16
+ testerProfile: "tp",
17
+ tester: "t",
18
+ config: "c",
19
+ job: "j",
20
+ };
21
+ function loadAliases() {
22
+ try {
23
+ if (fs.existsSync(ALIASES_FILE)) {
24
+ return JSON.parse(fs.readFileSync(ALIASES_FILE, "utf-8"));
25
+ }
26
+ }
27
+ catch {
28
+ // Corrupted — start fresh
29
+ }
30
+ return {};
31
+ }
32
+ function persistAliases(aliases) {
33
+ const dir = path.dirname(ALIASES_FILE);
34
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
35
+ const tmp = ALIASES_FILE + ".tmp";
36
+ fs.writeFileSync(tmp, JSON.stringify(aliases, null, 2) + "\n", { mode: 0o600 });
37
+ fs.renameSync(tmp, ALIASES_FILE);
38
+ }
39
+ /**
40
+ * Save aliases for a list of IDs under the given prefix.
41
+ * Clears all existing aliases for that prefix before saving.
42
+ * Numbers start at `startIndex` (default 1) to support pagination.
43
+ */
44
+ export function saveAliases(prefix, ids, startIndex = 1) {
45
+ const aliases = loadAliases();
46
+ // Remove existing entries for this prefix
47
+ const prefixPattern = new RegExp(`^${prefix}\\d+$`);
48
+ for (const key of Object.keys(aliases)) {
49
+ if (prefixPattern.test(key)) {
50
+ delete aliases[key];
51
+ }
52
+ }
53
+ // Save new mappings
54
+ for (let i = 0; i < ids.length; i++) {
55
+ aliases[`${prefix}${startIndex + i}`] = ids[i];
56
+ }
57
+ persistAliases(aliases);
58
+ }
59
+ /**
60
+ * Build a uuid→alias map for a given prefix (used by formatters to display aliases).
61
+ */
62
+ export function getAliasMap(prefix) {
63
+ const aliases = loadAliases();
64
+ const map = new Map();
65
+ const prefixPattern = new RegExp(`^${prefix}\\d+$`);
66
+ for (const [alias, uuid] of Object.entries(aliases)) {
67
+ if (prefixPattern.test(alias)) {
68
+ map.set(uuid, alias);
69
+ }
70
+ }
71
+ return map;
72
+ }
73
+ /**
74
+ * Resolve a short alias to a full UUID, or return the input as-is if it's not an alias.
75
+ */
76
+ export function resolveId(input) {
77
+ // If it looks like a UUID or contains hyphens, pass through
78
+ if (input.includes("-") || input.length > 20) {
79
+ return input;
80
+ }
81
+ // Check if it matches an alias pattern (letters + digits)
82
+ if (/^[a-z]+\d+$/.test(input)) {
83
+ const aliases = loadAliases();
84
+ const uuid = aliases[input];
85
+ if (uuid)
86
+ return uuid;
87
+ throw new Error(`Unknown alias "${input}". Run a list command first to generate aliases.`);
88
+ }
89
+ return input;
90
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared HTTP API client for the Speqs backend.
3
+ */
4
+ export declare class ApiError extends Error {
5
+ status: number;
6
+ statusText: string;
7
+ body: unknown;
8
+ error_code: string;
9
+ retryable: boolean;
10
+ constructor(status: number, statusText: string, body: unknown);
11
+ }
12
+ export declare class ApiClient {
13
+ private baseUrl;
14
+ private token;
15
+ constructor(opts: {
16
+ apiUrl: string;
17
+ token: string;
18
+ });
19
+ private headers;
20
+ get<T = unknown>(path: string, params?: Record<string, string>): Promise<T>;
21
+ post<T = unknown>(path: string, body?: unknown, opts?: {
22
+ timeout?: number;
23
+ }): Promise<T>;
24
+ put<T = unknown>(path: string, body?: unknown): Promise<T>;
25
+ del(path: string): Promise<void>;
26
+ private handleResponse;
27
+ }