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.
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +73 -0
- package/dist/commands/iteration.d.ts +5 -0
- package/dist/commands/iteration.js +83 -0
- package/dist/commands/simulation.d.ts +10 -0
- package/dist/commands/simulation.js +314 -0
- package/dist/commands/study.d.ts +5 -0
- package/dist/commands/study.js +170 -0
- package/dist/commands/tester-profile.d.ts +5 -0
- package/dist/commands/tester-profile.js +96 -0
- package/dist/commands/tester.d.ts +5 -0
- package/dist/commands/tester.js +64 -0
- package/dist/commands/workspace.d.ts +5 -0
- package/dist/commands/workspace.js +88 -0
- package/dist/index.js +32 -9
- package/dist/lib/alias-store.d.ts +30 -0
- package/dist/lib/alias-store.js +90 -0
- package/dist/lib/api-client.d.ts +27 -0
- package/dist/lib/api-client.js +121 -0
- package/dist/lib/auth.d.ts +8 -0
- package/dist/lib/auth.js +70 -0
- package/dist/lib/command-helpers.d.ts +24 -0
- package/dist/lib/command-helpers.js +94 -0
- package/dist/lib/output.d.ts +19 -0
- package/dist/lib/output.js +482 -0
- package/dist/lib/types.d.ts +202 -0
- package/dist/lib/types.js +4 -0
- package/package.json +1 -1
|
@@ -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,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,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,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
|
|
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
|
|
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
|
-
.
|
|
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
|
|
55
|
-
|
|
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
|
+
}
|