@ishlabs/cli 0.8.1 → 0.8.2
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 +323 -21
- package/dist/auth.d.ts +17 -1
- package/dist/auth.js +62 -9
- package/dist/commands/ask.d.ts +5 -0
- package/dist/commands/ask.js +722 -0
- package/dist/commands/config.js +25 -1
- package/dist/commands/docs.d.ts +17 -0
- package/dist/commands/docs.js +147 -0
- package/dist/commands/init.d.ts +16 -0
- package/dist/commands/init.js +182 -0
- package/dist/commands/iteration.d.ts +5 -1
- package/dist/commands/iteration.js +243 -31
- package/dist/commands/profile.d.ts +5 -0
- package/dist/commands/profile.js +313 -0
- package/dist/commands/source.d.ts +10 -0
- package/dist/commands/source.js +78 -0
- package/dist/commands/study-run.d.ts +11 -0
- package/dist/commands/study-run.js +552 -0
- package/dist/commands/study-tester.d.ts +8 -0
- package/dist/commands/study-tester.js +149 -0
- package/dist/commands/study.js +145 -70
- package/dist/commands/workspace.js +193 -7
- package/dist/config.d.ts +3 -1
- package/dist/config.js +10 -10
- package/dist/connect.d.ts +4 -1
- package/dist/connect.js +127 -94
- package/dist/index.js +82 -34
- package/dist/lib/alias-store.d.ts +3 -0
- package/dist/lib/alias-store.js +9 -7
- package/dist/lib/api-client.d.ts +9 -6
- package/dist/lib/api-client.js +87 -26
- package/dist/lib/ask-questions.d.ts +9 -0
- package/dist/lib/ask-questions.js +35 -0
- package/dist/lib/ask-variants.d.ts +48 -0
- package/dist/lib/ask-variants.js +236 -0
- package/dist/lib/auth.d.ts +1 -1
- package/dist/lib/auth.js +24 -8
- package/dist/lib/colors.d.ts +30 -0
- package/dist/lib/colors.js +48 -0
- package/dist/lib/command-helpers.d.ts +74 -0
- package/dist/lib/command-helpers.js +232 -6
- package/dist/lib/docs.d.ts +32 -0
- package/dist/lib/docs.js +930 -0
- package/dist/lib/local-sim/browser.d.ts +0 -1
- package/dist/lib/local-sim/browser.js +0 -2
- package/dist/lib/local-sim/install.d.ts +4 -7
- package/dist/lib/local-sim/install.js +6 -21
- package/dist/lib/output.d.ts +25 -3
- package/dist/lib/output.js +465 -20
- package/dist/lib/paths.d.ts +14 -0
- package/dist/lib/paths.js +36 -0
- package/dist/lib/profile-sources.d.ts +55 -0
- package/dist/lib/profile-sources.js +157 -0
- package/dist/lib/site-access.d.ts +80 -0
- package/dist/lib/site-access.js +188 -0
- package/dist/lib/skill-content.d.ts +31 -0
- package/dist/lib/skill-content.js +462 -0
- package/dist/lib/study-inputs.d.ts +20 -0
- package/dist/lib/study-inputs.js +72 -0
- package/dist/lib/types.d.ts +207 -9
- package/dist/lib/types.js +7 -0
- package/dist/lib/upload.js +2 -2
- package/dist/upgrade.js +11 -1
- package/package.json +1 -1
- package/dist/commands/simulation.d.ts +0 -10
- package/dist/commands/simulation.js +0 -647
- package/dist/commands/tester-profile.d.ts +0 -5
- package/dist/commands/tester-profile.js +0 -109
- package/dist/commands/tester.d.ts +0 -5
- package/dist/commands/tester.js +0 -73
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ish study tester — Inspect and manage testers (low-level; usually
|
|
3
|
+
* created via `ish study run`).
|
|
4
|
+
*
|
|
5
|
+
* Default action: `ish study tester <id>` shows tester details and results.
|
|
6
|
+
*/
|
|
7
|
+
import { withClient, readJsonFileOrStdin } from "../lib/command-helpers.js";
|
|
8
|
+
import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
|
|
9
|
+
import { formatTesterDetail, output } from "../lib/output.js";
|
|
10
|
+
/** Pick the latest iteration on a study (highest order_index, falling back to last). */
|
|
11
|
+
async function latestIterationForStudy(client, studyId) {
|
|
12
|
+
const study = await client.get(`/studies/${studyId}`);
|
|
13
|
+
const iterations = study.iterations ?? [];
|
|
14
|
+
if (iterations.length === 0) {
|
|
15
|
+
throw new Error(`Study ${studyId} has no iterations. Create one first with \`ish iteration create\`.`);
|
|
16
|
+
}
|
|
17
|
+
const sorted = [...iterations].sort((a, b) => (b.order_index ?? 0) - (a.order_index ?? 0));
|
|
18
|
+
return sorted[0].id;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Walk a parsed JSON body and resolve any alias-shaped strings (tp-..., i-..., s-...)
|
|
22
|
+
* appearing in well-known id fields. Pure client-side: avoids a backend round-trip
|
|
23
|
+
* when the body uses local aliases that the backend wouldn't recognise.
|
|
24
|
+
*/
|
|
25
|
+
const ALIAS_FIELDS = new Set(["tester_profile_id", "iteration_id", "study_id"]);
|
|
26
|
+
const ALIAS_RE = /^(tp|i|s)-[0-9a-f]{3,}$/i;
|
|
27
|
+
function prewalkAliases(value) {
|
|
28
|
+
if (Array.isArray(value)) {
|
|
29
|
+
return value.map(prewalkAliases);
|
|
30
|
+
}
|
|
31
|
+
if (value && typeof value === "object") {
|
|
32
|
+
const out = {};
|
|
33
|
+
for (const [k, v] of Object.entries(value)) {
|
|
34
|
+
if (ALIAS_FIELDS.has(k) && typeof v === "string" && ALIAS_RE.test(v)) {
|
|
35
|
+
try {
|
|
36
|
+
out[k] = resolveId(v);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
out[k] = v;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
out[k] = prewalkAliases(v);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
export function attachStudyTesterCommands(study) {
|
|
51
|
+
const tester = study
|
|
52
|
+
.command("tester")
|
|
53
|
+
.description("Inspect or manage testers (low-level; usually created via `study run`)")
|
|
54
|
+
.argument("[id]", "Tester ID — pass directly to view tester details and results")
|
|
55
|
+
.addHelpText("after", "\nExamples:\n $ ish study tester t-d4e # show tester details\n $ ish study tester create --iteration <id> --profile <id>\n $ ish study tester batch-create --iteration <id> --file testers.json\n $ ish study tester delete t-d4e")
|
|
56
|
+
.action(async (id, _opts, cmd) => {
|
|
57
|
+
if (!id) {
|
|
58
|
+
cmd.help();
|
|
59
|
+
}
|
|
60
|
+
await withClient(cmd, async (client, globals) => {
|
|
61
|
+
const data = await client.get(`/testers/${resolveId(id)}`);
|
|
62
|
+
const result = data;
|
|
63
|
+
if (result.id)
|
|
64
|
+
result.alias = tagAlias(ALIAS_PREFIX.tester, String(result.id));
|
|
65
|
+
formatTesterDetail(result, globals.json);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
tester
|
|
69
|
+
.command("create")
|
|
70
|
+
.description("Create a tester (low-level)")
|
|
71
|
+
.option("--iteration <id>", "Iteration ID (or use --study to pick the latest iteration on a study)")
|
|
72
|
+
.option("--study <id>", "Study ID; resolves to the latest iteration on that study (alternative to --iteration)")
|
|
73
|
+
.requiredOption("--profile <id>", "Tester profile ID")
|
|
74
|
+
.option("--language <lang>", "Language code (e.g. en, sv)")
|
|
75
|
+
.option("--platform <platform>", "Platform (browser, android, figma, code)")
|
|
76
|
+
.option("--tester-type <type>", "Tester type (ai, human)", "ai")
|
|
77
|
+
.addHelpText("after", "\nExamples:\n $ ish study tester create --iteration <id> --profile <id>\n $ ish study tester create --study s-XXX --profile tp-XXX\n $ ish study tester create --iteration <id> --profile <id> --platform android --json")
|
|
78
|
+
.action(async (opts, cmd) => {
|
|
79
|
+
await withClient(cmd, async (client, globals) => {
|
|
80
|
+
if (!opts.iteration && !opts.study) {
|
|
81
|
+
throw new Error("Provide --iteration <id> or --study <id> (the latest iteration on that study will be used).");
|
|
82
|
+
}
|
|
83
|
+
if (opts.iteration && opts.study) {
|
|
84
|
+
throw new Error("Pass either --iteration or --study, not both.");
|
|
85
|
+
}
|
|
86
|
+
const iterationId = opts.iteration
|
|
87
|
+
? resolveId(opts.iteration)
|
|
88
|
+
: await latestIterationForStudy(client, resolveId(opts.study));
|
|
89
|
+
const body = {
|
|
90
|
+
tester_profile_id: resolveId(opts.profile),
|
|
91
|
+
tester_type: opts.testerType,
|
|
92
|
+
...(opts.language && { language: opts.language }),
|
|
93
|
+
...(opts.platform && { platform: opts.platform }),
|
|
94
|
+
};
|
|
95
|
+
const data = await client.post(`/iterations/${iterationId}/testers`, body);
|
|
96
|
+
const result = data;
|
|
97
|
+
if (result.id)
|
|
98
|
+
result.alias = tagAlias(ALIAS_PREFIX.tester, String(result.id));
|
|
99
|
+
output(result, globals.json);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
tester
|
|
103
|
+
.command("batch-create")
|
|
104
|
+
.description("Create multiple testers (low-level)")
|
|
105
|
+
.requiredOption("--iteration <id>", "Iteration ID")
|
|
106
|
+
.option("--file <path>", "JSON file with testers array (alternative to --profiles)")
|
|
107
|
+
.option("--profiles <ids>", "Comma-separated profile IDs/aliases; shortcut for --file with one tester per profile (mutually exclusive with --file)")
|
|
108
|
+
.addHelpText("after", "\nExamples:\n $ ish study tester batch-create --iteration <id> --file testers.json\n $ ish study tester batch-create --iteration <id> --profiles tp-eba,tp-289,tp-913\n\n Expected JSON: [{ \"tester_profile_id\": \"<id>\", \"platform\": \"browser\" }, ...]\n Profile-id strings inside --file (tester_profile_id, iteration_id, study_id) are resolved client-side via the local alias store before submitting.")
|
|
109
|
+
.action(async (opts, cmd) => {
|
|
110
|
+
await withClient(cmd, async (client, globals) => {
|
|
111
|
+
if (!opts.file && !opts.profiles) {
|
|
112
|
+
throw new Error("Provide --file <path> or --profiles <ids>.");
|
|
113
|
+
}
|
|
114
|
+
if (opts.file && opts.profiles) {
|
|
115
|
+
throw new Error("Pass either --file or --profiles, not both.");
|
|
116
|
+
}
|
|
117
|
+
const iterationId = resolveId(opts.iteration);
|
|
118
|
+
let body;
|
|
119
|
+
if (opts.profiles) {
|
|
120
|
+
const ids = opts.profiles
|
|
121
|
+
.split(",")
|
|
122
|
+
.map((s) => s.trim())
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.map((s) => resolveId(s));
|
|
125
|
+
if (ids.length === 0) {
|
|
126
|
+
throw new Error("--profiles must contain at least one profile id/alias.");
|
|
127
|
+
}
|
|
128
|
+
body = { testers: ids.map((pid) => ({ tester_profile_id: pid, tester_type: "ai" })) };
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
const parsed = await readJsonFileOrStdin(opts.file);
|
|
132
|
+
body = prewalkAliases(parsed);
|
|
133
|
+
}
|
|
134
|
+
const data = await client.post(`/iterations/${iterationId}/testers/batch`, body);
|
|
135
|
+
output(data, globals.json);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
tester
|
|
139
|
+
.command("delete")
|
|
140
|
+
.description("Delete a tester")
|
|
141
|
+
.argument("<id>", "Tester ID")
|
|
142
|
+
.addHelpText("after", "\nExamples:\n $ ish study tester delete <id>")
|
|
143
|
+
.action(async (id, _opts, cmd) => {
|
|
144
|
+
await withClient(cmd, async (client, globals) => {
|
|
145
|
+
await client.del(`/testers/${resolveId(id)}`);
|
|
146
|
+
output({ message: "Tester deleted" }, globals.json);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
package/dist/commands/study.js
CHANGED
|
@@ -6,10 +6,63 @@ import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
|
|
|
6
6
|
import { loadConfig, saveConfig } from "../config.js";
|
|
7
7
|
import { formatStudyList, formatStudyDetail, formatStudyResults, output, ValidationError } from "../lib/output.js";
|
|
8
8
|
import { VALID_CONTENT_TYPES } from "../lib/types.js";
|
|
9
|
+
import { parseAssignment, loadAssignmentsFile, parseQuestion } from "../lib/study-inputs.js";
|
|
10
|
+
import { loadQuestionsManifest } from "../lib/ask-questions.js";
|
|
11
|
+
import { attachStudyRunCommands } from "./study-run.js";
|
|
12
|
+
import { attachStudyTesterCommands } from "./study-tester.js";
|
|
13
|
+
function collectRepeatable(value, prev = []) {
|
|
14
|
+
return prev.concat([value]);
|
|
15
|
+
}
|
|
16
|
+
function resolveAssignments(opts) {
|
|
17
|
+
const sources = [];
|
|
18
|
+
if (opts.assignment && opts.assignment.length > 0)
|
|
19
|
+
sources.push("--assignment");
|
|
20
|
+
if (opts.assignmentsFile)
|
|
21
|
+
sources.push("--assignments-file");
|
|
22
|
+
if (opts.assignments)
|
|
23
|
+
sources.push("--assignments");
|
|
24
|
+
if (sources.length > 1) {
|
|
25
|
+
throw new Error(`Pass only one of: ${sources.join(", ")}.`);
|
|
26
|
+
}
|
|
27
|
+
if (opts.assignment && opts.assignment.length > 0) {
|
|
28
|
+
return opts.assignment.map(parseAssignment);
|
|
29
|
+
}
|
|
30
|
+
if (opts.assignmentsFile) {
|
|
31
|
+
return loadAssignmentsFile(opts.assignmentsFile);
|
|
32
|
+
}
|
|
33
|
+
if (opts.assignments) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(opts.assignments);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error("Invalid --assignments JSON");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
function resolveQuestionnaire(opts) {
|
|
44
|
+
if (opts.question && opts.question.length > 0 && opts.questionnaire) {
|
|
45
|
+
throw new Error("Pass only one of: --question, --questionnaire.");
|
|
46
|
+
}
|
|
47
|
+
if (opts.question && opts.question.length > 0) {
|
|
48
|
+
return opts.question.map(parseQuestion);
|
|
49
|
+
}
|
|
50
|
+
if (opts.questionnaire) {
|
|
51
|
+
return loadQuestionsManifest(opts.questionnaire);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
9
55
|
export function registerStudyCommands(program) {
|
|
10
56
|
const study = program
|
|
11
57
|
.command("study")
|
|
12
|
-
.description("Manage studies")
|
|
58
|
+
.description("Manage and run studies")
|
|
59
|
+
.addHelpText("after", `
|
|
60
|
+
A study is the persistent research artifact: modality + assignments + questionnaire.
|
|
61
|
+
Iterations carry the URL or media. \`ish study run\` dispatches simulations on the latest iteration.
|
|
62
|
+
|
|
63
|
+
Concept pages: ish docs get-page concepts/study
|
|
64
|
+
ish docs get-page concepts/run-verbs
|
|
65
|
+
ish docs get-page concepts/assignment`);
|
|
13
66
|
study
|
|
14
67
|
.command("list")
|
|
15
68
|
.description("List studies for a workspace")
|
|
@@ -23,69 +76,68 @@ export function registerStudyCommands(program) {
|
|
|
23
76
|
});
|
|
24
77
|
study
|
|
25
78
|
.command("create")
|
|
26
|
-
.description("Create a new study")
|
|
79
|
+
.description("Create a new study (the persistent shape: modality, tasks, questionnaire)")
|
|
27
80
|
.option("--workspace <id>", "Workspace ID")
|
|
28
81
|
.requiredOption("--name <name>", "Study name")
|
|
29
82
|
.option("--description <description>", "Study description")
|
|
30
83
|
.option("--modality <modality>", "Study modality (interactive, video, audio, text, image, document)")
|
|
31
84
|
.option("--content-type <type>", "Content type (varies by modality — see examples below)")
|
|
32
|
-
.option("--
|
|
33
|
-
.option("--
|
|
85
|
+
.option("--assignment <name:instructions>", "Assignment as 'Name:Instructions' (repeatable)", collectRepeatable, [])
|
|
86
|
+
.option("--assignments-file <path>", "JSON file with assignments array")
|
|
87
|
+
.option("--assignments <json>", "Inline JSON array of assignments (escape hatch)")
|
|
88
|
+
.option("--question <text>", "Add a text question to the questionnaire (repeatable; type=text, timing=after)", collectRepeatable, [])
|
|
89
|
+
.option("--questionnaire <path>", "JSON file defining the questionnaire (supports text, slider, likert, single-choice, multiple-choice, number; timing=before|after)")
|
|
34
90
|
.addHelpText("after", `
|
|
35
91
|
Note: --workspace is optional if set via \`ish workspace use <alias>\`.
|
|
36
92
|
|
|
93
|
+
The questionnaire is the set of questions testers answer. Use \`--question\` to
|
|
94
|
+
quickly add simple text questions, or \`--questionnaire <file.json>\` for richer
|
|
95
|
+
types (slider, likert, choice) and custom timing. The two forms are mutually
|
|
96
|
+
exclusive — pick one.
|
|
97
|
+
|
|
37
98
|
Examples:
|
|
38
|
-
# Interactive study:
|
|
99
|
+
# Interactive study with one assignment and a single-question questionnaire:
|
|
39
100
|
$ ish study create --name "Onboarding UX" --modality interactive \\
|
|
40
|
-
|
|
101
|
+
--assignment "Sign up:Complete the signup flow" \\
|
|
102
|
+
--question "How easy was it?"
|
|
41
103
|
|
|
42
|
-
#
|
|
43
|
-
$ ish study create --name "
|
|
44
|
-
|
|
104
|
+
# Multiple assignments + a richer questionnaire from a file:
|
|
105
|
+
$ ish study create --name "Checkout" --modality interactive \\
|
|
106
|
+
--assignment "Browse:Find a product you like" \\
|
|
107
|
+
--assignment "Buy:Add to cart and checkout" \\
|
|
108
|
+
--questionnaire ./questionnaire.json
|
|
45
109
|
|
|
46
|
-
#
|
|
47
|
-
$ ish study create --name "
|
|
48
|
-
|
|
110
|
+
# Text/email study, all assignments from a file:
|
|
111
|
+
$ ish study create --name "Newsletter" --modality text --content-type email \\
|
|
112
|
+
--assignments-file ./assignments.json
|
|
49
113
|
|
|
50
|
-
# Video ad study:
|
|
114
|
+
# Video ad study with a quick two-question questionnaire:
|
|
51
115
|
$ ish study create --name "Product Ad" --modality video --content-type ad \\
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
$ ish study create --name "Instagram Post" --modality image --content-type social_post \\
|
|
56
|
-
--assignments '[{"name":"View","instructions":"Look at this post naturally"}]'
|
|
116
|
+
--assignment "Watch:Watch this ad and share your reaction" \\
|
|
117
|
+
--question "What stuck with you?" \\
|
|
118
|
+
--question "Would you click through?"
|
|
57
119
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
120
|
+
# Sample questionnaire.json (full InterviewQuestion shape):
|
|
121
|
+
# [
|
|
122
|
+
# { "question": "How easy?", "type": "slider", "timing": "after",
|
|
123
|
+
# "min": 0, "max": 10 },
|
|
124
|
+
# { "question": "Which fits best?", "type": "single-choice",
|
|
125
|
+
# "options": ["A","B","C"] }
|
|
126
|
+
# ]
|
|
62
127
|
|
|
63
128
|
Content types by modality:
|
|
64
129
|
text: narrative, informational, commercial, editorial, reference, email, news
|
|
65
130
|
video: tutorial, documentary, entertainment, review, lifestyle, news, social_post, ad
|
|
66
131
|
audio: music, narration, conversation, speech, soundscape, news, ad
|
|
67
132
|
image: product, photography, infographic, artwork, interface, social_post, ad
|
|
68
|
-
document: deck, presentation, report, brochure, guide
|
|
133
|
+
document: deck, presentation, report, brochure, guide
|
|
134
|
+
|
|
135
|
+
Next: configure a run with \`ish iteration create --study <id>\`,
|
|
136
|
+
then dispatch with \`ish study run\`.`)
|
|
69
137
|
.action(async (opts, cmd) => {
|
|
70
138
|
await withClient(cmd, async (client, globals) => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (opts.assignments) {
|
|
74
|
-
try {
|
|
75
|
-
assignments = JSON.parse(opts.assignments);
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
throw new Error("Invalid --assignments JSON");
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (opts.questions) {
|
|
82
|
-
try {
|
|
83
|
-
interviewQuestions = JSON.parse(opts.questions);
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
throw new Error("Invalid --questions JSON");
|
|
87
|
-
}
|
|
88
|
-
}
|
|
139
|
+
const assignments = resolveAssignments(opts);
|
|
140
|
+
const interviewQuestions = resolveQuestionnaire(opts);
|
|
89
141
|
// Validate content_type against modality
|
|
90
142
|
if (opts.contentType && opts.modality) {
|
|
91
143
|
const validTypes = VALID_CONTENT_TYPES[opts.modality];
|
|
@@ -112,7 +164,7 @@ Content types by modality:
|
|
|
112
164
|
const result = data;
|
|
113
165
|
if (result.id)
|
|
114
166
|
result.alias = tagAlias(ALIAS_PREFIX.study, String(result.id));
|
|
115
|
-
formatStudyDetail(result, globals.json);
|
|
167
|
+
formatStudyDetail(result, globals.json, { writePath: true });
|
|
116
168
|
if (!globals.json && data.id) {
|
|
117
169
|
const url = getWebUrl(globals, `/${resolvedWs}/${data.id}/overview`);
|
|
118
170
|
console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
|
|
@@ -133,11 +185,11 @@ Content types by modality:
|
|
|
133
185
|
...(opts.targetUrl && { target_url: opts.targetUrl }),
|
|
134
186
|
};
|
|
135
187
|
const resolvedWs = resolveWorkspace(opts.workspace);
|
|
136
|
-
const data = await client.post(`/products/${resolvedWs}/studies/generate`, body);
|
|
188
|
+
const data = await client.post(`/products/${resolvedWs}/studies/generate`, body, { timeout: 120_000 });
|
|
137
189
|
const result = data;
|
|
138
190
|
if (result.id)
|
|
139
191
|
result.alias = tagAlias(ALIAS_PREFIX.study, String(result.id));
|
|
140
|
-
formatStudyDetail(result, globals.json);
|
|
192
|
+
formatStudyDetail(result, globals.json, { writePath: true });
|
|
141
193
|
if (!globals.json && data.id) {
|
|
142
194
|
const url = getWebUrl(globals, `/${resolvedWs}/${data.id}/overview`);
|
|
143
195
|
console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
|
|
@@ -165,9 +217,31 @@ Content types by modality:
|
|
|
165
217
|
});
|
|
166
218
|
study
|
|
167
219
|
.command("results")
|
|
168
|
-
.description("View aggregated results
|
|
220
|
+
.description("View aggregated results: tester counts, sentiment, interview answers. Returns a stable envelope with empty fields when no runs have completed.")
|
|
169
221
|
.argument("<id>", "Study ID")
|
|
170
|
-
.addHelpText("after",
|
|
222
|
+
.addHelpText("after", `
|
|
223
|
+
Examples:
|
|
224
|
+
$ ish study results <id>
|
|
225
|
+
$ ish study results <id> --json
|
|
226
|
+
|
|
227
|
+
Example response (--json):
|
|
228
|
+
{
|
|
229
|
+
"study_id": "<uuid>",
|
|
230
|
+
"alias": "s-...",
|
|
231
|
+
"name": "...",
|
|
232
|
+
"tester_count": 12,
|
|
233
|
+
"completed_count": 8,
|
|
234
|
+
"errored_count": 0,
|
|
235
|
+
"total_interactions": 142,
|
|
236
|
+
"sentiment": { "Satisfied": 5, "Frustrated": 2, "Neutral": 1 },
|
|
237
|
+
"interview_answers": [
|
|
238
|
+
{ "question_id": "...", "question": "...", "type": "text",
|
|
239
|
+
"answers": [ { "tester_id": "...", "tester_alias": "t-...", "answer": "..." } ] }
|
|
240
|
+
],
|
|
241
|
+
"testers": [ { "id": "...", "alias": "t-...", "name": "...", "status": "completed", "interaction_count": 12 } ]
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
When no runs have completed, the same envelope is returned with zero counts and empty arrays.`)
|
|
171
245
|
.action(async (id, _opts, cmd) => {
|
|
172
246
|
await withClient(cmd, async (client, globals) => {
|
|
173
247
|
const rid = resolveId(id);
|
|
@@ -188,29 +262,27 @@ Content types by modality:
|
|
|
188
262
|
.option("--status <status>", "Study status (draft, running, completed)")
|
|
189
263
|
.option("--modality <modality>", "Study modality (interactive, video, audio, text, image, document)")
|
|
190
264
|
.option("--content-type <type>", "Content type")
|
|
191
|
-
.option("--
|
|
192
|
-
.option("--
|
|
193
|
-
.
|
|
265
|
+
.option("--assignment <name:instructions>", "Replace all assignments with these (repeatable)", collectRepeatable, [])
|
|
266
|
+
.option("--assignments-file <path>", "JSON file with assignments array")
|
|
267
|
+
.option("--assignments <json>", "Inline JSON array of assignments (escape hatch)")
|
|
268
|
+
.option("--question <text>", "Replace the questionnaire with these text questions (repeatable)", collectRepeatable, [])
|
|
269
|
+
.option("--questionnaire <path>", "Replace the questionnaire from a JSON file (full InterviewQuestion shape)")
|
|
270
|
+
.addHelpText("after", `
|
|
271
|
+
Replacing the questionnaire: pass either \`--question\` (one or more text
|
|
272
|
+
questions) or \`--questionnaire <file.json>\` (full shape: slider, likert,
|
|
273
|
+
choice, custom timing). The two are mutually exclusive.
|
|
274
|
+
|
|
275
|
+
Examples:
|
|
276
|
+
$ ish study update <id> --name "Updated Name"
|
|
277
|
+
$ ish study update <id> --status running --json
|
|
278
|
+
$ ish study update <id> --assignment "Sign up:Complete the signup flow" \\
|
|
279
|
+
--question "How easy was it?"
|
|
280
|
+
$ ish study update <id> --questionnaire ./questionnaire.json
|
|
281
|
+
$ ish study update <id> --assignments-file ./assignments.json`)
|
|
194
282
|
.action(async (id, opts, cmd) => {
|
|
195
283
|
await withClient(cmd, async (client, globals) => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
if (opts.assignments) {
|
|
199
|
-
try {
|
|
200
|
-
assignments = JSON.parse(opts.assignments);
|
|
201
|
-
}
|
|
202
|
-
catch {
|
|
203
|
-
throw new Error("Invalid --assignments JSON");
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
if (opts.questions) {
|
|
207
|
-
try {
|
|
208
|
-
interviewQuestions = JSON.parse(opts.questions);
|
|
209
|
-
}
|
|
210
|
-
catch {
|
|
211
|
-
throw new Error("Invalid --questions JSON");
|
|
212
|
-
}
|
|
213
|
-
}
|
|
284
|
+
const assignments = resolveAssignments(opts);
|
|
285
|
+
const interviewQuestions = resolveQuestionnaire(opts);
|
|
214
286
|
if (opts.contentType && opts.modality) {
|
|
215
287
|
const validTypes = VALID_CONTENT_TYPES[opts.modality];
|
|
216
288
|
if (validTypes && !validTypes.includes(opts.contentType)) {
|
|
@@ -240,7 +312,7 @@ Content types by modality:
|
|
|
240
312
|
const result = data;
|
|
241
313
|
if (result.id)
|
|
242
314
|
result.alias = tagAlias(ALIAS_PREFIX.study, String(result.id));
|
|
243
|
-
formatStudyDetail(result, globals.json);
|
|
315
|
+
formatStudyDetail(result, globals.json, { writePath: true });
|
|
244
316
|
});
|
|
245
317
|
});
|
|
246
318
|
study
|
|
@@ -250,8 +322,9 @@ Content types by modality:
|
|
|
250
322
|
.addHelpText("after", "\nExamples:\n $ ish study delete <id>")
|
|
251
323
|
.action(async (id, _opts, cmd) => {
|
|
252
324
|
await withClient(cmd, async (client, globals) => {
|
|
253
|
-
|
|
254
|
-
|
|
325
|
+
const rid = resolveId(id);
|
|
326
|
+
await client.del(`/studies/${rid}`);
|
|
327
|
+
output({ id: rid, alias: tagAlias(ALIAS_PREFIX.study, rid), message: "Study deleted" }, globals.json, { writePath: true });
|
|
255
328
|
});
|
|
256
329
|
});
|
|
257
330
|
study
|
|
@@ -280,4 +353,6 @@ Content types by modality:
|
|
|
280
353
|
console.error(`Active study set to "${data.name || rid}".`);
|
|
281
354
|
});
|
|
282
355
|
});
|
|
356
|
+
attachStudyRunCommands(study);
|
|
357
|
+
attachStudyTesterCommands(study);
|
|
283
358
|
}
|