@ishlabs/cli 0.23.1 → 0.24.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/dist/commands/ask.js +4 -4
- package/dist/commands/iteration.js +25 -3
- package/dist/commands/study-share.d.ts +18 -0
- package/dist/commands/study-share.js +117 -0
- package/dist/commands/study.js +54 -7
- package/dist/commands/workspace.js +4 -1
- package/dist/connect.d.ts +4 -2
- package/dist/connect.js +151 -11
- package/dist/index.js +63 -6
- package/dist/lib/ask-questions.d.ts +15 -5
- package/dist/lib/ask-questions.js +34 -11
- package/dist/lib/auth.d.ts +1 -0
- package/dist/lib/auth.js +7 -1
- package/dist/lib/command-helpers.js +33 -5
- package/dist/lib/docs.js +140 -8
- package/dist/lib/output.js +8 -1
- package/dist/lib/reverse-proxy.d.ts +19 -0
- package/dist/lib/reverse-proxy.js +87 -0
- package/dist/lib/reverse-proxy.test.d.ts +10 -0
- package/dist/lib/reverse-proxy.test.js +149 -0
- package/dist/lib/segmentation.d.ts +31 -0
- package/dist/lib/segmentation.js +105 -0
- package/dist/lib/skill-content.js +76 -4
- package/dist/lib/types.d.ts +2 -0
- package/package.json +3 -1
package/dist/commands/ask.js
CHANGED
|
@@ -168,7 +168,7 @@ Concept pages: ish docs get-page concepts/ask
|
|
|
168
168
|
.option("--variants <file.json>", "JSON manifest of variants (alternative to --variant)")
|
|
169
169
|
.option("--wants-pick", "Each participant picks a favourite variant (compatible with --wants-ratings; can be set together).")
|
|
170
170
|
.option("--wants-ratings", "Each participant rates every variant 1–5 (compatible with --wants-pick; can be set together). If neither is set, participants leave a free-form comment only.")
|
|
171
|
-
.option("--questions <file
|
|
171
|
+
.option("--questions <json|@file|path>", `Questions as inline JSON, @file, or a JSON file path: [{"question":"...","type":"text"|"slider"|"likert"|"single-choice"|"multiple-choice"|"number"}]`)
|
|
172
172
|
.option("--language <code>", "2-letter language code (with --new only)", "en");
|
|
173
173
|
addPersonFilterFlags(askRun, {
|
|
174
174
|
allFlagName: "--all-simulatable",
|
|
@@ -343,7 +343,7 @@ Examples:
|
|
|
343
343
|
.option("--variants <file.json>", "JSON manifest of variants (alternative to --variant)")
|
|
344
344
|
.option("--wants-pick", "Each participant picks a favourite variant (compatible with --wants-ratings; can be set together).")
|
|
345
345
|
.option("--wants-ratings", "Each participant rates every variant 1–5 (compatible with --wants-pick; can be set together). If neither is set, participants leave a free-form comment only.")
|
|
346
|
-
.option("--questions <file
|
|
346
|
+
.option("--questions <json|@file|path>", `Questions as inline JSON, @file, or a JSON file path: [{"question":"...","type":"text"|"slider"|"likert"|"single-choice"|"multiple-choice"|"number"}]`)
|
|
347
347
|
.option("--language <code>", "2-letter language code", "en");
|
|
348
348
|
addPersonFilterFlags(askCreate, {
|
|
349
349
|
allFlagName: "--all-simulatable",
|
|
@@ -628,7 +628,7 @@ the model's self-reported confidence in its variant choice. See
|
|
|
628
628
|
.option("--variants <file.json>", "JSON manifest of variants")
|
|
629
629
|
.option("--wants-pick", "Each participant picks a favourite variant (compatible with --wants-ratings; can be set together).")
|
|
630
630
|
.option("--wants-ratings", "Each participant rates every variant 1–5 (compatible with --wants-pick; can be set together). If neither is set, participants leave a free-form comment only.")
|
|
631
|
-
.option("--questions <file
|
|
631
|
+
.option("--questions <json|@file|path>", `Questions as inline JSON, @file, or a JSON file path: [{"question":"...","type":"text"|"slider"|"likert"|"single-choice"|"multiple-choice"|"number"}]`)
|
|
632
632
|
.option("--subset-round <n>", "Drill-in subset (Pattern B) — 1-indexed prior round to filter against. Pair with --subset-variant. The new round dispatches only to participants who picked --subset-variant on round N.")
|
|
633
633
|
.option("--subset-variant <variant_id>", "Drill-in subset (Pattern B) — variant id (UUID) on the prior round whose pickers should inherit. Pair with --subset-round. Read from `aggregates.pick_buckets` or `variants[*].id` on the prior round.")
|
|
634
634
|
.option("--wait", "Wait until the new round completes")
|
|
@@ -676,7 +676,7 @@ error_kind: "participant_subset_invalid".`)
|
|
|
676
676
|
.argument("[id]", "Ask alias or UUID (defaults to active ask)")
|
|
677
677
|
.option("--ask <id>", "Ask ID; alternative to positional argument")
|
|
678
678
|
.requiredOption("--round <n|round-id>", "Round number (1-indexed) or round id/alias")
|
|
679
|
-
.requiredOption("--questions <file
|
|
679
|
+
.requiredOption("--questions <json|@file|path>", `Questions as inline JSON, @file, or a JSON file path: [{"question":"...","type":"text"|"slider"|"likert"|"single-choice"|"multiple-choice"|"number"}]`)
|
|
680
680
|
.option("--redispatch-all", "Clear prior phase-1 outputs (comment, pick, ratings) and re-run the entire round from scratch (legacy behavior). Default is additive — only the new questions are answered.", false)
|
|
681
681
|
.option("--wait", "Wait until the round completes (or errors)")
|
|
682
682
|
.option("--timeout <s>", "Wait timeout in seconds (default 300)")
|
|
@@ -11,6 +11,7 @@ import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
|
|
|
11
11
|
import { output, formatIterationList, ValidationError } from "../lib/output.js";
|
|
12
12
|
import { resolveContentUrl, resolveContentUrls, resolveTextContent } from "../lib/upload.js";
|
|
13
13
|
import { isMediaModality, validateIterationDetails, normalizeChatMode, validateRoleCriteria } from "../lib/modality.js";
|
|
14
|
+
import { validateSegmentation, warnIfOverSegmented } from "../lib/segmentation.js";
|
|
14
15
|
import { normalizeEnumValue, SCREEN_FORMATS } from "../lib/enums.js";
|
|
15
16
|
/**
|
|
16
17
|
* Read text inline or from a file when prefixed with `@/path/to/file`.
|
|
@@ -361,7 +362,7 @@ Concept pages: ish docs get-page concepts/iteration
|
|
|
361
362
|
.description("Create a new iteration with run-time content/URL")
|
|
362
363
|
.option("--study <id>", "Study ID (or set via `ish study use`)")
|
|
363
364
|
.option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from the study)")
|
|
364
|
-
.option("--name <name>", "Iteration name (
|
|
365
|
+
.option("--name <name>", "Iteration name (defaults to the next position letter A/B/C… if omitted)")
|
|
365
366
|
.option("--description <description>", "Iteration description")
|
|
366
367
|
// Interactive
|
|
367
368
|
.option("--platform <platform>", "Platform (browser, android, figma, code) — interactive only")
|
|
@@ -390,7 +391,7 @@ Concept pages: ish docs get-page concepts/iteration
|
|
|
390
391
|
.option("--social-platform <platform>", "Social platform (instagram, tiktok, facebook, linkedin, x)")
|
|
391
392
|
.option("--copy-position <pos>", "Copy position relative to media (before, after)", "after")
|
|
392
393
|
// Segmentation / per-iteration evaluation config (media modalities)
|
|
393
|
-
.option("--segmentation-json <json>", "Segmentation JSON — time_based {intervals_seconds, labels?}, section_based {sections[{name,label,...}]}, or page_based {} — media modalities")
|
|
394
|
+
.option("--segmentation-json <json>", "Segmentation JSON — time_based {intervals_seconds, labels?}, section_based {sections[{name,label,...}]}, or page_based {} — media modalities. section_based sections are SEMANTIC: group related paragraphs into a few coherent sections (a long article is usually 3-6 sections, not one per paragraph; paragraph_start/end just mark where each section begins and ends).")
|
|
394
395
|
.option("--content-config-json <json>", "Content config JSON — {early_termination, selected_segment_indices?} — media modalities")
|
|
395
396
|
// Chat modality
|
|
396
397
|
.option("--chat-mode <mode>", "Chat mode: external_chatbot (default; probe a customer chatbot) or participant_pair (two AI people talk to each other)")
|
|
@@ -671,8 +672,29 @@ Next: \`ish study run\` to dispatch simulations against this iteration.`)
|
|
|
671
672
|
details = buildIterationDetails(modality, resolved);
|
|
672
673
|
}
|
|
673
674
|
}
|
|
675
|
+
// Segments are semantic sections, not paragraphs: reject a malformed
|
|
676
|
+
// shape (e.g. a missing label) before the network call, and nudge when
|
|
677
|
+
// it looks like one-section-per-paragraph.
|
|
678
|
+
if (details && typeof details === "object") {
|
|
679
|
+
validateSegmentation(details.segmentation);
|
|
680
|
+
warnIfOverSegmented(details.segmentation, { quiet: globals.quietExplicit });
|
|
681
|
+
}
|
|
682
|
+
// Default name = the iteration's position letter (A, B, C…), matching how
|
|
683
|
+
// `study create` names the inline iteration "A". Never an opaque
|
|
684
|
+
// "CLI <timestamp>" — iterations should read as what they are.
|
|
685
|
+
let iterationName = opts.name;
|
|
686
|
+
if (!iterationName) {
|
|
687
|
+
try {
|
|
688
|
+
const study = await client.get(`/studies/${studyId}`);
|
|
689
|
+
const n = Array.isArray(study.iterations) ? study.iterations.length : 0;
|
|
690
|
+
iterationName = n < 26 ? String.fromCharCode(65 + n) : `Iteration ${n + 1}`;
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
iterationName = "A";
|
|
694
|
+
}
|
|
695
|
+
}
|
|
674
696
|
const body = {
|
|
675
|
-
name:
|
|
697
|
+
name: iterationName,
|
|
676
698
|
...(opts.description !== undefined && { description: opts.description }),
|
|
677
699
|
...(details && { details }),
|
|
678
700
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ish study share / ish study unshare — public, no-login share links for a
|
|
3
|
+
* study's results, so a salesperson can send a prospect a URL they open in a
|
|
4
|
+
* browser without an account.
|
|
5
|
+
*
|
|
6
|
+
* Wraps backend endpoints already shipped server-side and used by the web
|
|
7
|
+
* share viewer (`/share/study/[token]`):
|
|
8
|
+
*
|
|
9
|
+
* POST /share/study — create a link → { id, token, share_url, expires_at, created_at }
|
|
10
|
+
* GET /share/study/links — list the current user's study share links
|
|
11
|
+
* DELETE /share/study/{token} — revoke a link by its raw token (204)
|
|
12
|
+
*
|
|
13
|
+
* The backend builds `share_url` from its own `frontend_url` setting, which is
|
|
14
|
+
* a DIFFERENT host than the CLI's app URL (getAppUrl → app.ishlabs.io). Always
|
|
15
|
+
* print the backend's `share_url` verbatim; never reconstruct it client-side.
|
|
16
|
+
*/
|
|
17
|
+
import type { Command } from "commander";
|
|
18
|
+
export declare function attachStudyShareCommands(study: Command): void;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ish study share / ish study unshare — public, no-login share links for a
|
|
3
|
+
* study's results, so a salesperson can send a prospect a URL they open in a
|
|
4
|
+
* browser without an account.
|
|
5
|
+
*
|
|
6
|
+
* Wraps backend endpoints already shipped server-side and used by the web
|
|
7
|
+
* share viewer (`/share/study/[token]`):
|
|
8
|
+
*
|
|
9
|
+
* POST /share/study — create a link → { id, token, share_url, expires_at, created_at }
|
|
10
|
+
* GET /share/study/links — list the current user's study share links
|
|
11
|
+
* DELETE /share/study/{token} — revoke a link by its raw token (204)
|
|
12
|
+
*
|
|
13
|
+
* The backend builds `share_url` from its own `frontend_url` setting, which is
|
|
14
|
+
* a DIFFERENT host than the CLI's app URL (getAppUrl → app.ishlabs.io). Always
|
|
15
|
+
* print the backend's `share_url` verbatim; never reconstruct it client-side.
|
|
16
|
+
*/
|
|
17
|
+
import { withClient, resolveStudy, confirmDestructive } from "../lib/command-helpers.js";
|
|
18
|
+
import { tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
|
|
19
|
+
import { output, printTable, ValidationError } from "../lib/output.js";
|
|
20
|
+
import { c } from "../lib/colors.js";
|
|
21
|
+
/** Convert a positive-integer `--expires <days>` into an ISO-8601 UTC datetime. */
|
|
22
|
+
function expiresAtFromDays(raw) {
|
|
23
|
+
const days = Number(raw);
|
|
24
|
+
if (!Number.isInteger(days) || days <= 0) {
|
|
25
|
+
throw new ValidationError(`--expires must be a positive integer number of days, got "${raw}".`, []);
|
|
26
|
+
}
|
|
27
|
+
return new Date(Date.now() + days * 86_400_000).toISOString();
|
|
28
|
+
}
|
|
29
|
+
export function attachStudyShareCommands(study) {
|
|
30
|
+
study
|
|
31
|
+
.command("share")
|
|
32
|
+
.description("Create a public, no-login link to a study's results (summary, frames, " +
|
|
33
|
+
"journeys, segments). Anyone with the link can view it — share with prospects.")
|
|
34
|
+
.argument("[id]", "Study ID (defaults to active study)")
|
|
35
|
+
.option("--workspace <id>", "Workspace ID; accepted for consistency (workspace is inferred from the study)")
|
|
36
|
+
.option("--expires <days>", "Link expires after N days (default: never)")
|
|
37
|
+
.option("--list", "List all your study share links instead of creating one")
|
|
38
|
+
.addHelpText("after", `
|
|
39
|
+
Examples:
|
|
40
|
+
$ ish study share # share the active study
|
|
41
|
+
$ ish study share <study-id>
|
|
42
|
+
$ ish study share <study-id> --expires 30 # auto-expire in 30 days
|
|
43
|
+
$ ish study share <study-id> --json # { token, share_url, expires_at, ... }
|
|
44
|
+
$ ish study share --list # all your share links
|
|
45
|
+
|
|
46
|
+
The printed share_url is a no-login public URL — anyone with it can view the
|
|
47
|
+
study results. Revoke with \`ish study unshare <token>\`.`)
|
|
48
|
+
.action(async (id, opts, cmd) => {
|
|
49
|
+
await withClient(cmd, async (client, globals) => {
|
|
50
|
+
if (opts.list) {
|
|
51
|
+
const links = await client.get("/share/study/links");
|
|
52
|
+
if (globals.json) {
|
|
53
|
+
output(links.map((l) => ({
|
|
54
|
+
token: l.token,
|
|
55
|
+
study: tagAlias(ALIAS_PREFIX.study, l.study_id),
|
|
56
|
+
expires_at: l.expires_at,
|
|
57
|
+
is_revoked: l.is_revoked,
|
|
58
|
+
})), true, { preProjected: true });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (links.length === 0) {
|
|
62
|
+
console.log("No share links yet. Create one with `ish study share <study-id>`.");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
printTable(["TOKEN", "STUDY", "EXPIRES", "REVOKED"], links.map((l) => [
|
|
66
|
+
l.token,
|
|
67
|
+
tagAlias(ALIAS_PREFIX.study, l.study_id),
|
|
68
|
+
l.expires_at ?? "never",
|
|
69
|
+
l.is_revoked ? "yes" : "no",
|
|
70
|
+
]));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const studyId = resolveStudy(id);
|
|
74
|
+
const body = { study_id: studyId };
|
|
75
|
+
if (opts.expires !== undefined) {
|
|
76
|
+
body.expires_at = expiresAtFromDays(opts.expires);
|
|
77
|
+
}
|
|
78
|
+
const link = await client.post("/share/study", body);
|
|
79
|
+
if (globals.json) {
|
|
80
|
+
output(link, true, { preProjected: true });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// The share_url is the deliverable (a URL to paste into an email) →
|
|
84
|
+
// stdout. Everything else is context → stderr (stdout=data convention).
|
|
85
|
+
console.log(`${c.bold}${c.cyan}${link.share_url}${c.reset}`);
|
|
86
|
+
console.error(`\n ${c.dim}token:${c.reset} ${link.token}`);
|
|
87
|
+
console.error(` ${c.dim}expires:${c.reset} ${link.expires_at ?? "never"}`);
|
|
88
|
+
console.error(`\n ${c.dim}Anyone with this link can view the study results — no login required.${c.reset}`);
|
|
89
|
+
console.error(` ${c.dim}Revoke with:${c.reset} ish study unshare ${link.token}`);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
study
|
|
93
|
+
.command("unshare")
|
|
94
|
+
.description("Revoke a study share link by its token. The public URL stops working immediately.")
|
|
95
|
+
.argument("<token>", "Share link token (from `ish study share` or `ish study share --list`)")
|
|
96
|
+
.option("--workspace <id>", "Workspace ID; accepted for consistency")
|
|
97
|
+
.option("-y, --yes", "Skip the confirmation prompt")
|
|
98
|
+
.addHelpText("after", `
|
|
99
|
+
Examples:
|
|
100
|
+
$ ish study unshare <token>
|
|
101
|
+
$ ish study unshare <token> --yes
|
|
102
|
+
|
|
103
|
+
The <token> is the raw share token, not a study ID or alias. List tokens with
|
|
104
|
+
\`ish study share --list\`.`)
|
|
105
|
+
.action(async (token, opts, cmd) => {
|
|
106
|
+
await withClient(cmd, async (client, globals) => {
|
|
107
|
+
await confirmDestructive(`Revoke share link ${token}? The public URL will stop working.`, { yes: opts.yes, json: globals.json });
|
|
108
|
+
await client.del(`/share/study/${encodeURIComponent(token)}`);
|
|
109
|
+
if (globals.json) {
|
|
110
|
+
output({ token, revoked: true }, true, { writePath: true });
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log(`${c.green}Share link revoked:${c.reset} ${token}`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
package/dist/commands/study.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { readFileSync } from "node:fs";
|
|
5
5
|
import { Option } from "commander";
|
|
6
6
|
import { withClient, getWebUrl, terminalLink, resolveWorkspace, confirmDestructive, readFileOrStdin, collectIds } from "../lib/command-helpers.js";
|
|
7
|
+
import { validateSegmentation, warnIfOverSegmented } from "../lib/segmentation.js";
|
|
7
8
|
import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
|
|
8
9
|
import { loadConfig, saveConfig } from "../config.js";
|
|
9
10
|
import { formatStudyList, formatStudyDetail, formatStudyResults, buildStudyResultsEnvelope, buildStudyResultsSummary, buildChatTranscript, formatStudyResultsGroupBy, output, ValidationError, } from "../lib/output.js";
|
|
@@ -20,6 +21,7 @@ import { attachStudyRunCommands } from "./study-run.js";
|
|
|
20
21
|
import { attachStudyParticipantCommands } from "./study-participant.js";
|
|
21
22
|
import { attachStudyAnalyzeCommands } from "./study-analyze.js";
|
|
22
23
|
import { attachStudyScreenshotsCommands } from "./study-screenshots.js";
|
|
24
|
+
import { attachStudyShareCommands } from "./study-share.js";
|
|
23
25
|
function collectRepeatable(value, prev = []) {
|
|
24
26
|
return prev.concat([value]);
|
|
25
27
|
}
|
|
@@ -116,13 +118,19 @@ Concept pages: ish docs get-page concepts/study
|
|
|
116
118
|
.option("--assignments-file <path>", "JSON file with assignments array")
|
|
117
119
|
.option("--assignments <json>", "Inline JSON array of assignments (escape hatch)")
|
|
118
120
|
.option("--question <text>", "Add a text question to the questionnaire (repeatable; type=text, timing=after)", collectRepeatable, [])
|
|
119
|
-
.option("--questionnaire <path>", "JSON file
|
|
121
|
+
.option("--questionnaire <json|@file|path>", "Questionnaire as inline JSON array, @file, or a JSON file path — supports text, slider, likert, single-choice, multiple-choice, number; timing=before|after")
|
|
120
122
|
.option("--content-text <text>", "Text content to evaluate, or @filepath to read from file. Creates iteration A inline (text modality only)")
|
|
121
123
|
.option("--url <url>", "URL to test. Creates iteration A inline (interactive modality only)")
|
|
122
124
|
.option("--screen-format <format>", "Screen format for interactive iterations: desktop (default) or mobile_portrait (hyphen/underscore variants accepted)")
|
|
123
125
|
.option("--content-url <url>", "Public URL of the media file. Creates iteration A inline (video, audio, document modalities). For local files, use the 2-step `iteration create` flow.")
|
|
124
126
|
.option("--image-urls <urls>", "Comma-separated public image URLs. Creates iteration A inline (image modality). For local files, use the 2-step `iteration create` flow.")
|
|
125
127
|
.option("--title <title>", "Content title (text + media modalities — image, video, audio, document; optional). Not used for interactive / chat.")
|
|
128
|
+
.option("--segmentation-json <json>", "Segmentation JSON for the inline iteration A — time_based {intervals_seconds, labels?}, section_based {sections[{name,label,...}]}, or page_based {} (text + media). section_based sections are SEMANTIC: group related paragraphs into a few coherent sections (a long article is usually 3-6 sections, not one per paragraph). Lets one `study create` build a complete segmented iteration — no separate `iteration create` needed.")
|
|
129
|
+
.option("--content-config-json <json>", "Content-config JSON for the inline iteration A (early_termination, selected_segment_indices) — text + media.")
|
|
130
|
+
.option("--content-html <html>", "HTML version of the text, or @filepath — text modality (email rendering)")
|
|
131
|
+
.option("--sender-name <name>", "Email 'From' display name — text modality (email rendering)")
|
|
132
|
+
.option("--sender-email <email>", "Email sender address — text modality (email rendering)")
|
|
133
|
+
.option("--featured-image-url <url>", "Hero image URL — text modality (email rendering)")
|
|
126
134
|
.option("--endpoint <id>", "Saved chatbot endpoint id or alias. Creates iteration A inline (chat modality, external_chatbot mode)")
|
|
127
135
|
.option("--endpoint-config <file>", "ChatbotEndpointConfig JSON file (or `-` for stdin); embedded directly. Mutually exclusive with --endpoint (chat modality, external_chatbot mode)")
|
|
128
136
|
.option("--max-turns <n>", "Maximum conversation turns per participant (chat modality only; default 12)", (v) => Number(v))
|
|
@@ -138,9 +146,11 @@ Concept pages: ish docs get-page concepts/study
|
|
|
138
146
|
Note: --workspace is optional if set via \`ish workspace use <alias>\`.
|
|
139
147
|
|
|
140
148
|
The questionnaire is the set of questions participants answer. Use \`--question\` to
|
|
141
|
-
quickly add simple text questions, or \`--questionnaire
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
quickly add simple text questions, or \`--questionnaire\` for richer types (slider,
|
|
150
|
+
likert, single-choice, multiple-choice, number) and custom timing. \`--questionnaire\`
|
|
151
|
+
accepts inline JSON ('[{"question":"How easy?","type":"slider","min":0,"max":10}]'),
|
|
152
|
+
an @file (@/tmp/q.json), or a JSON file path (./questionnaire.json) — no temp file
|
|
153
|
+
required. The two forms are mutually exclusive — pick one.
|
|
144
154
|
|
|
145
155
|
Inline iteration shortcuts (one-shot study + iteration A):
|
|
146
156
|
--modality interactive --url <url> [--screen-format desktop|mobile_portrait]
|
|
@@ -311,6 +321,37 @@ Next: configure a run with \`ish iteration create --study <id>\`,
|
|
|
311
321
|
&& opts.imageUrls === undefined) {
|
|
312
322
|
throw new Error("--title only applies with --content-text (text) or --content-url / --image-urls (media). Interactive + chat iterations don't carry a title.");
|
|
313
323
|
}
|
|
324
|
+
// Inline iteration A can carry segmentation + content-config (text + media)
|
|
325
|
+
// and email-styling (text), so a single `study create` builds one COMPLETE
|
|
326
|
+
// iteration — no separate `iteration create` (which would leave an empty A
|
|
327
|
+
// plus a redundant B). Parse the JSON flags once, here.
|
|
328
|
+
const parseInlineJson = (raw, flag) => {
|
|
329
|
+
if (raw === undefined)
|
|
330
|
+
return undefined;
|
|
331
|
+
try {
|
|
332
|
+
return JSON.parse(raw);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
throw new ValidationError(`Invalid ${flag}: expected valid JSON.`, []);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const inlineMediaExtras = {
|
|
339
|
+
...(parseInlineJson(opts.segmentationJson, "--segmentation-json") && { segmentation: parseInlineJson(opts.segmentationJson, "--segmentation-json") }),
|
|
340
|
+
...(parseInlineJson(opts.contentConfigJson, "--content-config-json") && { content_config: parseInlineJson(opts.contentConfigJson, "--content-config-json") }),
|
|
341
|
+
};
|
|
342
|
+
// Segments are semantic sections, not paragraphs: reject a malformed
|
|
343
|
+
// shape (e.g. a missing label) before the network call, and nudge when
|
|
344
|
+
// it looks like one-section-per-paragraph.
|
|
345
|
+
if (inlineMediaExtras.segmentation !== undefined) {
|
|
346
|
+
validateSegmentation(inlineMediaExtras.segmentation);
|
|
347
|
+
warnIfOverSegmented(inlineMediaExtras.segmentation, { quiet: globals.quietExplicit });
|
|
348
|
+
}
|
|
349
|
+
const inlineEmailExtras = {
|
|
350
|
+
...(opts.contentHtml && { content_html: opts.contentHtml.startsWith("@") ? readFileSync(opts.contentHtml.slice(1), "utf8") : opts.contentHtml }),
|
|
351
|
+
...(opts.senderName && { sender_name: opts.senderName }),
|
|
352
|
+
...(opts.senderEmail && { sender_email: opts.senderEmail }),
|
|
353
|
+
...(opts.featuredImageUrl && { featured_image_url: opts.featuredImageUrl }),
|
|
354
|
+
};
|
|
314
355
|
let inlineIteration;
|
|
315
356
|
let chatbotEndpointId = null;
|
|
316
357
|
if (opts.contentText !== undefined) {
|
|
@@ -326,6 +367,8 @@ Next: configure a run with \`ish iteration create --study <id>\`,
|
|
|
326
367
|
type: "text",
|
|
327
368
|
content_text: text,
|
|
328
369
|
...(opts.title && { title: opts.title }),
|
|
370
|
+
...inlineEmailExtras,
|
|
371
|
+
...inlineMediaExtras,
|
|
329
372
|
},
|
|
330
373
|
};
|
|
331
374
|
}
|
|
@@ -361,6 +404,7 @@ Next: configure a run with \`ish iteration create --study <id>\`,
|
|
|
361
404
|
type: "image",
|
|
362
405
|
image_urls: urls,
|
|
363
406
|
...(opts.title && { title: opts.title }),
|
|
407
|
+
...inlineMediaExtras,
|
|
364
408
|
},
|
|
365
409
|
};
|
|
366
410
|
}
|
|
@@ -381,6 +425,7 @@ Next: configure a run with \`ish iteration create --study <id>\`,
|
|
|
381
425
|
type: opts.modality,
|
|
382
426
|
content_url: opts.contentUrl,
|
|
383
427
|
...(opts.title && { title: opts.title }),
|
|
428
|
+
...inlineMediaExtras,
|
|
384
429
|
},
|
|
385
430
|
};
|
|
386
431
|
}
|
|
@@ -1097,11 +1142,12 @@ When no runs have completed, the default envelope is returned with zero counts a
|
|
|
1097
1142
|
.option("--assignments-file <path>", "JSON file with assignments array")
|
|
1098
1143
|
.option("--assignments <json>", "Inline JSON array of assignments (escape hatch)")
|
|
1099
1144
|
.option("--question <text>", "Replace the questionnaire with these text questions (repeatable)", collectRepeatable, [])
|
|
1100
|
-
.option("--questionnaire <path>", "Replace the questionnaire from a JSON file (full InterviewQuestion shape)")
|
|
1145
|
+
.option("--questionnaire <json|@file|path>", "Replace the questionnaire from inline JSON, @file, or a JSON file path (full InterviewQuestion shape)")
|
|
1101
1146
|
.addHelpText("after", `
|
|
1102
1147
|
Replacing the questionnaire: pass either \`--question\` (one or more text
|
|
1103
|
-
questions) or \`--questionnaire
|
|
1104
|
-
|
|
1148
|
+
questions) or \`--questionnaire\` (full shape: slider, likert, choice, custom
|
|
1149
|
+
timing) as inline JSON, an @file, or a JSON file path. The two are mutually
|
|
1150
|
+
exclusive.
|
|
1105
1151
|
|
|
1106
1152
|
Examples:
|
|
1107
1153
|
$ ish study update <id> --name "Updated Name"
|
|
@@ -1207,4 +1253,5 @@ checklists ("steps") ride along when present in the JSON forms
|
|
|
1207
1253
|
attachStudyParticipantCommands(study);
|
|
1208
1254
|
attachStudyAnalyzeCommands(study);
|
|
1209
1255
|
attachStudyScreenshotsCommands(study);
|
|
1256
|
+
attachStudyShareCommands(study);
|
|
1210
1257
|
}
|
|
@@ -111,7 +111,8 @@ existing workspace was returned. On creation, \`reused: false\`.`)
|
|
|
111
111
|
.option("--name <name>", "Workspace name")
|
|
112
112
|
.option("--description <description>", "Workspace description")
|
|
113
113
|
.option("--base-url <url>", "Default base URL")
|
|
114
|
-
.
|
|
114
|
+
.option("--logo <url>", "Brand logo image URL (shown on the workspace and on shared study links)")
|
|
115
|
+
.addHelpText("after", "\nExamples:\n $ ish workspace update <id> --name \"New Name\"\n $ ish workspace update <id> --base-url https://example.com --json\n $ ish workspace update <id> --logo https://logo.clearbit.com/acme.com")
|
|
115
116
|
.action(async (id, opts, cmd) => {
|
|
116
117
|
await withClient(cmd, async (client, globals) => {
|
|
117
118
|
const body = {};
|
|
@@ -121,6 +122,8 @@ existing workspace was returned. On creation, \`reused: false\`.`)
|
|
|
121
122
|
body.description = opts.description;
|
|
122
123
|
if (opts.baseUrl !== undefined)
|
|
123
124
|
body.base_url = opts.baseUrl;
|
|
125
|
+
if (opts.logo !== undefined)
|
|
126
|
+
body.logo_url = opts.logo;
|
|
124
127
|
if (Object.keys(body).length === 0) {
|
|
125
128
|
console.error("No update flags provided. Run `ish workspace update --help` for options.");
|
|
126
129
|
return;
|
package/dist/connect.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Localhost connect CLI — wraps cloudflared and registers with Ish backend.
|
|
3
3
|
*/
|
|
4
|
+
import { type Route } from "./lib/reverse-proxy.js";
|
|
5
|
+
export type { Route } from "./lib/reverse-proxy.js";
|
|
4
6
|
export declare function runTunnel(port: number, tokenArg?: string, apiUrlArg?: string, tokenFileArg?: string, outputOpts?: {
|
|
5
7
|
json?: boolean;
|
|
6
8
|
quiet?: boolean;
|
|
7
|
-
}): Promise<void>;
|
|
8
|
-
export declare function runDetached(port: number, apiUrlArg: string | undefined, tokenArg: string | undefined, tokenFileArg: string | undefined): Promise<void>;
|
|
9
|
+
}, routes?: Route[]): Promise<void>;
|
|
10
|
+
export declare function runDetached(port: number, apiUrlArg: string | undefined, tokenArg: string | undefined, tokenFileArg: string | undefined, routes?: Route[]): Promise<void>;
|
|
9
11
|
export declare function connectStatus(json: boolean): void;
|
|
10
12
|
export declare function disconnect(json: boolean): Promise<void>;
|