@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
package/dist/lib/output.js
CHANGED
|
@@ -16,15 +16,20 @@ export function setVerbose(v) { _verbose = v; }
|
|
|
16
16
|
export function setFields(fields) { _fields = fields; }
|
|
17
17
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
18
18
|
const TIMESTAMP_KEYS = new Set(["created_at", "updated_at"]);
|
|
19
|
+
const PAGINATION_KEYS = new Set(["items", "total", "limit", "offset"]);
|
|
19
20
|
/**
|
|
20
21
|
* Strip UUID-valued fields, null/undefined values, and timestamps.
|
|
21
22
|
* Preserves alias, name, label, status, and other meaningful fields.
|
|
23
|
+
*
|
|
24
|
+
* When `keepIds` is true, `id` and `*_id` keys (and their UUID values) are
|
|
25
|
+
* preserved — used for write-path responses so agents always get a canonical
|
|
26
|
+
* handle without having to set --verbose.
|
|
22
27
|
*/
|
|
23
|
-
function leanJson(data) {
|
|
28
|
+
function leanJson(data, keepIds = false) {
|
|
24
29
|
if (data === null || data === undefined)
|
|
25
30
|
return undefined;
|
|
26
31
|
if (Array.isArray(data)) {
|
|
27
|
-
return data.map(leanJson).filter((v) => v !== undefined);
|
|
32
|
+
return data.map((v) => leanJson(v, keepIds)).filter((v) => v !== undefined);
|
|
28
33
|
}
|
|
29
34
|
if (typeof data !== "object")
|
|
30
35
|
return data;
|
|
@@ -36,6 +41,12 @@ function leanJson(data) {
|
|
|
36
41
|
result[key] = value;
|
|
37
42
|
continue;
|
|
38
43
|
}
|
|
44
|
+
// Write-path: preserve canonical identifier keys
|
|
45
|
+
if (keepIds && (key === "id" || key.endsWith("_id"))) {
|
|
46
|
+
if (value !== null && value !== undefined)
|
|
47
|
+
result[key] = value;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
39
50
|
// Strip null/undefined
|
|
40
51
|
if (value === null || value === undefined)
|
|
41
52
|
continue;
|
|
@@ -47,7 +58,7 @@ function leanJson(data) {
|
|
|
47
58
|
continue;
|
|
48
59
|
// Recurse into objects/arrays
|
|
49
60
|
if (typeof value === "object") {
|
|
50
|
-
const cleaned = leanJson(value);
|
|
61
|
+
const cleaned = leanJson(value, keepIds);
|
|
51
62
|
if (cleaned !== undefined && !(Array.isArray(cleaned) && cleaned.length === 0)) {
|
|
52
63
|
result[key] = cleaned;
|
|
53
64
|
}
|
|
@@ -58,7 +69,30 @@ function leanJson(data) {
|
|
|
58
69
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
59
70
|
}
|
|
60
71
|
/**
|
|
61
|
-
*
|
|
72
|
+
* Detect a paginated list wrapper: `{items: [...], total?, limit?, offset?}`.
|
|
73
|
+
* Used so `--fields` filters per-item shape without dropping pagination metadata.
|
|
74
|
+
*/
|
|
75
|
+
function isListWrapper(data) {
|
|
76
|
+
if (data === null || typeof data !== "object" || Array.isArray(data))
|
|
77
|
+
return false;
|
|
78
|
+
const obj = data;
|
|
79
|
+
if (!Array.isArray(obj.items))
|
|
80
|
+
return false;
|
|
81
|
+
// Conservative: only treat as a wrapper if every top-level key is recognized
|
|
82
|
+
// pagination metadata. Otherwise an entity that happens to expose a nested
|
|
83
|
+
// `items` field would be unwrapped by mistake.
|
|
84
|
+
for (const key of Object.keys(obj)) {
|
|
85
|
+
if (!PAGINATION_KEYS.has(key))
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Pick only specified fields from each item.
|
|
92
|
+
*
|
|
93
|
+
* Rule: `--fields` projects per-item shape; the wrapper (pagination metadata
|
|
94
|
+
* for paginated lists, top-level object for `get`-style responses) stays
|
|
95
|
+
* stable so downstream JSON parsers don't have to branch on flag presence.
|
|
62
96
|
*/
|
|
63
97
|
function pickFields(data, fields) {
|
|
64
98
|
if (Array.isArray(data)) {
|
|
@@ -66,9 +100,9 @@ function pickFields(data, fields) {
|
|
|
66
100
|
}
|
|
67
101
|
if (typeof data === "object" && data !== null) {
|
|
68
102
|
const obj = data;
|
|
69
|
-
//
|
|
70
|
-
if (
|
|
71
|
-
return pickFields(obj.items, fields);
|
|
103
|
+
// Preserve list wrapper; project items only.
|
|
104
|
+
if (isListWrapper(obj)) {
|
|
105
|
+
return { ...obj, items: pickFields(obj.items, fields) };
|
|
72
106
|
}
|
|
73
107
|
const result = {};
|
|
74
108
|
for (const field of fields) {
|
|
@@ -80,8 +114,16 @@ function pickFields(data, fields) {
|
|
|
80
114
|
return data;
|
|
81
115
|
}
|
|
82
116
|
/** Serialize data as JSON, applying lean transform and field selection. */
|
|
83
|
-
function jsonOutput(data) {
|
|
84
|
-
let out
|
|
117
|
+
function jsonOutput(data, options = {}) {
|
|
118
|
+
let out;
|
|
119
|
+
if (_verbose || options.preProjected) {
|
|
120
|
+
// Verbose: full payload. preProjected: caller already chose the fields,
|
|
121
|
+
// so don't strip again (otherwise leanJson would drop e.g. created_at).
|
|
122
|
+
out = data;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
out = leanJson(data, options.writePath);
|
|
126
|
+
}
|
|
85
127
|
if (_fields && _fields.length > 0) {
|
|
86
128
|
out = pickFields(out, _fields);
|
|
87
129
|
}
|
|
@@ -100,9 +142,9 @@ function injectAliases(items, prefix, idField = "id") {
|
|
|
100
142
|
}
|
|
101
143
|
}
|
|
102
144
|
// --- JSON mode ---
|
|
103
|
-
export function output(data, json) {
|
|
145
|
+
export function output(data, json, options = {}) {
|
|
104
146
|
if (json) {
|
|
105
|
-
console.log(jsonOutput(data));
|
|
147
|
+
console.log(jsonOutput(data, options));
|
|
106
148
|
return;
|
|
107
149
|
}
|
|
108
150
|
if (data === null || data === undefined)
|
|
@@ -203,13 +245,31 @@ function suggestionsForError(err) {
|
|
|
203
245
|
export function outputError(err, json) {
|
|
204
246
|
const suggestions = suggestionsForError(err);
|
|
205
247
|
if (err instanceof ApiError) {
|
|
248
|
+
// Surface backend-structured fields when present in the response body
|
|
249
|
+
// (e.g. 422 errors return `errors: [{loc, msg, type, input, allowed_values}]`,
|
|
250
|
+
// and some endpoints attach `suggestions: [...]`). Merging these gives
|
|
251
|
+
// JSON consumers everything the server told us, without losing the
|
|
252
|
+
// client-side suggestion mapping.
|
|
253
|
+
let bodyErrors;
|
|
254
|
+
let bodySuggestions;
|
|
255
|
+
if (err.body && typeof err.body === "object") {
|
|
256
|
+
const body = err.body;
|
|
257
|
+
if (Array.isArray(body.errors))
|
|
258
|
+
bodyErrors = body.errors;
|
|
259
|
+
if (Array.isArray(body.suggestions))
|
|
260
|
+
bodySuggestions = body.suggestions;
|
|
261
|
+
}
|
|
262
|
+
const mergedSuggestions = bodySuggestions
|
|
263
|
+
? Array.from(new Set([...bodySuggestions.map(String), ...suggestions]))
|
|
264
|
+
: suggestions;
|
|
206
265
|
if (json) {
|
|
207
266
|
console.error(JSON.stringify({
|
|
208
267
|
error: err.message,
|
|
209
268
|
error_code: err.error_code,
|
|
210
269
|
status: err.status,
|
|
211
270
|
retryable: err.retryable,
|
|
212
|
-
...(
|
|
271
|
+
...(bodyErrors !== undefined && { errors: bodyErrors }),
|
|
272
|
+
...(mergedSuggestions.length > 0 && { suggestions: mergedSuggestions }),
|
|
213
273
|
}));
|
|
214
274
|
}
|
|
215
275
|
else {
|
|
@@ -219,7 +279,20 @@ export function outputError(err, json) {
|
|
|
219
279
|
else {
|
|
220
280
|
console.error(`Error: ${err.message}`);
|
|
221
281
|
}
|
|
222
|
-
|
|
282
|
+
if (Array.isArray(bodyErrors)) {
|
|
283
|
+
for (const entry of bodyErrors) {
|
|
284
|
+
if (entry && typeof entry === "object") {
|
|
285
|
+
const e = entry;
|
|
286
|
+
const loc = Array.isArray(e.loc) ? e.loc.join(".") : "";
|
|
287
|
+
const msg = e.msg ? String(e.msg) : "";
|
|
288
|
+
const allowed = Array.isArray(e.allowed_values)
|
|
289
|
+
? ` (allowed: ${e.allowed_values.join(", ")})`
|
|
290
|
+
: "";
|
|
291
|
+
console.error(` • ${loc}${loc ? ": " : ""}${msg}${allowed}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const s of mergedSuggestions)
|
|
223
296
|
console.error(` → ${s}`);
|
|
224
297
|
}
|
|
225
298
|
}
|
|
@@ -290,6 +363,22 @@ function formatLabel(key) {
|
|
|
290
363
|
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
291
364
|
}
|
|
292
365
|
// --- Workspace formatting ---
|
|
366
|
+
/**
|
|
367
|
+
* Default workspace fields exposed in both TTY tables and JSON output.
|
|
368
|
+
* Anything not in this set (e.g. has_figma_token) is hidden unless --verbose.
|
|
369
|
+
*/
|
|
370
|
+
const WORKSPACE_DEFAULT_FIELDS = ["alias", "name", "base_url", "created_at"];
|
|
371
|
+
function projectWorkspace(workspace, options = {}) {
|
|
372
|
+
const result = {};
|
|
373
|
+
if (options.writePath && workspace.id !== null && workspace.id !== undefined) {
|
|
374
|
+
result.id = workspace.id;
|
|
375
|
+
}
|
|
376
|
+
for (const field of WORKSPACE_DEFAULT_FIELDS) {
|
|
377
|
+
if (field in workspace)
|
|
378
|
+
result[field] = workspace[field];
|
|
379
|
+
}
|
|
380
|
+
return result;
|
|
381
|
+
}
|
|
293
382
|
export function formatWorkspaceList(workspaces, json) {
|
|
294
383
|
if (workspaces.length === 0) {
|
|
295
384
|
if (json)
|
|
@@ -300,19 +389,32 @@ export function formatWorkspaceList(workspaces, json) {
|
|
|
300
389
|
}
|
|
301
390
|
injectAliases(workspaces, ALIAS_PREFIX.workspace);
|
|
302
391
|
if (json) {
|
|
303
|
-
|
|
392
|
+
if (_verbose) {
|
|
393
|
+
console.log(jsonOutput(workspaces));
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
const projected = workspaces.map((w) => projectWorkspace(w));
|
|
397
|
+
console.log(jsonOutput(projected, { preProjected: true }));
|
|
398
|
+
}
|
|
304
399
|
return;
|
|
305
400
|
}
|
|
306
401
|
const aliasMap = getAliasMap(ALIAS_PREFIX.workspace);
|
|
307
|
-
printTable(["#", "NAME", "CREATED"], workspaces.map((w) => [
|
|
402
|
+
printTable(["#", "NAME", "BASE URL", "CREATED"], workspaces.map((w) => [
|
|
308
403
|
aliasMap.get(String(w.id)) || String(w.id || ""),
|
|
309
404
|
String(w.name || ""),
|
|
405
|
+
String(w.base_url || "-"),
|
|
310
406
|
formatDate(w.created_at),
|
|
311
407
|
]));
|
|
312
408
|
}
|
|
313
|
-
export function formatWorkspaceDetail(workspace, json) {
|
|
409
|
+
export function formatWorkspaceDetail(workspace, json, options = {}) {
|
|
314
410
|
if (json) {
|
|
315
|
-
|
|
411
|
+
if (_verbose) {
|
|
412
|
+
console.log(jsonOutput(workspace, options));
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
const projected = projectWorkspace(workspace, { writePath: options.writePath });
|
|
416
|
+
console.log(jsonOutput(projected, { ...options, preProjected: true }));
|
|
417
|
+
}
|
|
316
418
|
return;
|
|
317
419
|
}
|
|
318
420
|
const display = {
|
|
@@ -324,6 +426,19 @@ export function formatWorkspaceDetail(workspace, json) {
|
|
|
324
426
|
};
|
|
325
427
|
printKeyValue(display);
|
|
326
428
|
}
|
|
429
|
+
// --- Site-access formatting ---
|
|
430
|
+
export function formatSiteAccessStatus(summary, json) {
|
|
431
|
+
if (json) {
|
|
432
|
+
console.log(jsonOutput(summary));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
printTable(["METHOD", "STATUS", "ORIGIN"], [
|
|
436
|
+
["Basic auth", summary.basic_auth.configured ? "configured" : "-", summary.basic_auth.origin || "-"],
|
|
437
|
+
["Session cookie", summary.session_cookie.configured ? "configured" : "-", summary.session_cookie.origin || "-"],
|
|
438
|
+
["Login form", summary.login.configured ? "configured" : "-", "-"],
|
|
439
|
+
["Public", summary.public_affirmed.affirmed ? "affirmed" : "-", summary.public_affirmed.origin || "-"],
|
|
440
|
+
]);
|
|
441
|
+
}
|
|
327
442
|
// --- Study formatting ---
|
|
328
443
|
export function formatStudyList(studies, json) {
|
|
329
444
|
if (studies.length === 0) {
|
|
@@ -348,9 +463,9 @@ export function formatStudyList(studies, json) {
|
|
|
348
463
|
String(s.tester_count ?? "0"),
|
|
349
464
|
]));
|
|
350
465
|
}
|
|
351
|
-
export function formatStudyDetail(study, json) {
|
|
466
|
+
export function formatStudyDetail(study, json, options = {}) {
|
|
352
467
|
if (json) {
|
|
353
|
-
console.log(jsonOutput(study));
|
|
468
|
+
console.log(jsonOutput(study, options));
|
|
354
469
|
return;
|
|
355
470
|
}
|
|
356
471
|
// Header
|
|
@@ -397,9 +512,73 @@ export function formatStudyDetail(study, json) {
|
|
|
397
512
|
]));
|
|
398
513
|
}
|
|
399
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* Stable JSON envelope for `study results`. Schema is fixed regardless of
|
|
517
|
+
* study state — fields default to `null`, `0`, or `[]` when nothing has run.
|
|
518
|
+
* Agents can rely on the keys always being present (M4).
|
|
519
|
+
*/
|
|
520
|
+
function buildStudyResultsEnvelope(study) {
|
|
521
|
+
const allTesters = collectTesters(study);
|
|
522
|
+
const studyAlias = study.id
|
|
523
|
+
? deterministicAlias(ALIAS_PREFIX.study, String(study.id))
|
|
524
|
+
: null;
|
|
525
|
+
const completedCount = allTesters.filter((t) => t.status === "completed" || t.status === "complete").length;
|
|
526
|
+
// Aggregate sentiment across all interactions on all testers.
|
|
527
|
+
const sentimentCounts = {};
|
|
528
|
+
let sentimentTotal = 0;
|
|
529
|
+
for (const t of allTesters) {
|
|
530
|
+
for (const [label, count] of Object.entries(t.sentimentCounts)) {
|
|
531
|
+
sentimentCounts[label] = (sentimentCounts[label] || 0) + count;
|
|
532
|
+
sentimentTotal += count;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const sentiment = sentimentTotal > 0
|
|
536
|
+
? {
|
|
537
|
+
counts: sentimentCounts,
|
|
538
|
+
total: sentimentTotal,
|
|
539
|
+
}
|
|
540
|
+
: null;
|
|
541
|
+
// Group interview answers by question for easy parsing.
|
|
542
|
+
const questions = Array.isArray(study.interview_questions) ? study.interview_questions : [];
|
|
543
|
+
const interviewAnswers = questions.map((q) => {
|
|
544
|
+
const qObj = q;
|
|
545
|
+
const answers = [];
|
|
546
|
+
for (const t of allTesters) {
|
|
547
|
+
const a = t.interviewAnswers.find((x) => x.questionId === qObj.id);
|
|
548
|
+
if (a) {
|
|
549
|
+
answers.push({
|
|
550
|
+
tester_alias: t.id ? deterministicAlias(ALIAS_PREFIX.tester, t.id) : null,
|
|
551
|
+
tester_name: t.name,
|
|
552
|
+
iteration: t.iterationLabel,
|
|
553
|
+
answer: a.answer,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return {
|
|
558
|
+
question: qObj.question || "",
|
|
559
|
+
type: qObj.type || "text",
|
|
560
|
+
answers,
|
|
561
|
+
};
|
|
562
|
+
});
|
|
563
|
+
return {
|
|
564
|
+
study: {
|
|
565
|
+
alias: studyAlias,
|
|
566
|
+
name: study.name || null,
|
|
567
|
+
status: study.status || null,
|
|
568
|
+
modality: study.modality || null,
|
|
569
|
+
},
|
|
570
|
+
tester_count: allTesters.length,
|
|
571
|
+
completed_count: completedCount,
|
|
572
|
+
sentiment,
|
|
573
|
+
interview_answers: interviewAnswers,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
400
576
|
export function formatStudyResults(study, json) {
|
|
401
577
|
if (json) {
|
|
402
|
-
|
|
578
|
+
// preProjected: bypass leanJson so the stable envelope keeps documented
|
|
579
|
+
// empty defaults (sentiment: null, interview_answers[].answers: []) rather
|
|
580
|
+
// than having them stripped by the lean transform.
|
|
581
|
+
console.log(jsonOutput(buildStudyResultsEnvelope(study), { preProjected: true }));
|
|
403
582
|
return;
|
|
404
583
|
}
|
|
405
584
|
const allTesters = collectTesters(study);
|
|
@@ -595,6 +774,54 @@ export function formatTesterProfileList(profiles, json, limit) {
|
|
|
595
774
|
console.log(`\n Showing ${list.length} of ${fullList.length} profiles. Use --limit and --offset for more.`);
|
|
596
775
|
}
|
|
597
776
|
}
|
|
777
|
+
// --- Audience source formatting ---
|
|
778
|
+
export function formatAudienceSource(source, json) {
|
|
779
|
+
if (source.id) {
|
|
780
|
+
source.alias = deterministicAlias(ALIAS_PREFIX.testerProfileSource, String(source.id));
|
|
781
|
+
}
|
|
782
|
+
if (json) {
|
|
783
|
+
console.log(jsonOutput(source));
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const display = {
|
|
787
|
+
Alias: source.alias || source.id || "-",
|
|
788
|
+
File: source.original_filename || "-",
|
|
789
|
+
Kind: source.kind || "-",
|
|
790
|
+
Status: source.status || "-",
|
|
791
|
+
"Content-Type": source.content_type || "-",
|
|
792
|
+
};
|
|
793
|
+
if (source.extracted_text_length != null) {
|
|
794
|
+
display["Text length"] = source.extracted_text_length;
|
|
795
|
+
}
|
|
796
|
+
if (source.error) {
|
|
797
|
+
display.Error = source.error;
|
|
798
|
+
}
|
|
799
|
+
printKeyValue(display);
|
|
800
|
+
}
|
|
801
|
+
// --- Generated profile list (returned by /tester-profiles/generate) ---
|
|
802
|
+
export function formatGeneratedProfileList(profiles, json) {
|
|
803
|
+
const list = Array.isArray(profiles) ? profiles : [];
|
|
804
|
+
if (list.length === 0) {
|
|
805
|
+
if (json)
|
|
806
|
+
console.log("[]");
|
|
807
|
+
else
|
|
808
|
+
console.log("No profiles generated.");
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
injectAliases(list, ALIAS_PREFIX.testerProfile);
|
|
812
|
+
if (json) {
|
|
813
|
+
console.log(jsonOutput(list));
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
printTable(["#", "NAME", "OCCUPATION", "COUNTRY", "GENDER", "AGE"], list.map((p) => [
|
|
817
|
+
String(p.alias || p.id || ""),
|
|
818
|
+
String(p.name || ""),
|
|
819
|
+
String(p.occupation || "-"),
|
|
820
|
+
String(p.country || "-"),
|
|
821
|
+
String(p.gender || "-"),
|
|
822
|
+
formatAge(p.date_of_birth),
|
|
823
|
+
]));
|
|
824
|
+
}
|
|
598
825
|
// --- Simulation poll formatting ---
|
|
599
826
|
export function formatSimulationPoll(results, json, isMedia = false) {
|
|
600
827
|
if (results.length === 0) {
|
|
@@ -621,6 +848,224 @@ export function formatSimulationPoll(results, json, isMedia = false) {
|
|
|
621
848
|
];
|
|
622
849
|
}));
|
|
623
850
|
}
|
|
851
|
+
// --- Ask formatting ---
|
|
852
|
+
function variantLetter(index) {
|
|
853
|
+
if (index < 26)
|
|
854
|
+
return String.fromCharCode(65 + index);
|
|
855
|
+
return `V${index + 1}`;
|
|
856
|
+
}
|
|
857
|
+
export function formatAskList(asks, json) {
|
|
858
|
+
if (asks.length === 0) {
|
|
859
|
+
if (json)
|
|
860
|
+
console.log("[]");
|
|
861
|
+
else
|
|
862
|
+
console.log("No asks.");
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
injectAliases(asks, ALIAS_PREFIX.ask);
|
|
866
|
+
if (json) {
|
|
867
|
+
console.log(jsonOutput(asks));
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const aliasMap = getAliasMap(ALIAS_PREFIX.ask);
|
|
871
|
+
printTable(["#", "NAME", "AUDIENCE", "ROUNDS", "LAST ROUND", "ARCHIVED"], asks.map((a) => [
|
|
872
|
+
aliasMap.get(String(a.id)) || String(a.id || ""),
|
|
873
|
+
String(a.name || ""),
|
|
874
|
+
String(a.audience_count ?? "0"),
|
|
875
|
+
String(a.round_count ?? "0"),
|
|
876
|
+
formatDate(a.last_round_at),
|
|
877
|
+
a.is_archived ? "yes" : "no",
|
|
878
|
+
]));
|
|
879
|
+
}
|
|
880
|
+
export function formatAskDetail(ask, json) {
|
|
881
|
+
if (json) {
|
|
882
|
+
console.log(jsonOutput(ask));
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
console.log(`${ask.name || "Untitled"} (${ask.id || ""})`);
|
|
886
|
+
if (ask.description)
|
|
887
|
+
console.log(String(ask.description));
|
|
888
|
+
const meta = [];
|
|
889
|
+
if (ask.is_archived)
|
|
890
|
+
meta.push("archived");
|
|
891
|
+
meta.push(formatDate(ask.created_at));
|
|
892
|
+
console.log(meta.join(" · "));
|
|
893
|
+
const testers = Array.isArray(ask.testers) ? ask.testers : [];
|
|
894
|
+
console.log(`\nAudience (${testers.length}):`);
|
|
895
|
+
if (testers.length > 0) {
|
|
896
|
+
const rows = testers.slice(0, 20).map((t) => {
|
|
897
|
+
const obj = t;
|
|
898
|
+
const profile = obj.tester_profile;
|
|
899
|
+
const name = String(profile?.name || obj.instance_name || "Unknown");
|
|
900
|
+
return [
|
|
901
|
+
obj.id ? deterministicAlias(ALIAS_PREFIX.tester, String(obj.id)) : "-",
|
|
902
|
+
name,
|
|
903
|
+
String(obj.status || "-"),
|
|
904
|
+
];
|
|
905
|
+
});
|
|
906
|
+
printTable(["#", "NAME", "STATUS"], rows);
|
|
907
|
+
if (testers.length > 20)
|
|
908
|
+
console.log(` … and ${testers.length - 20} more`);
|
|
909
|
+
}
|
|
910
|
+
const rounds = Array.isArray(ask.rounds) ? ask.rounds : [];
|
|
911
|
+
if (rounds.length > 0) {
|
|
912
|
+
console.log(`\nRounds (${rounds.length}):`);
|
|
913
|
+
for (const r of rounds) {
|
|
914
|
+
const round = r;
|
|
915
|
+
const variants = Array.isArray(round.variants) ? round.variants : [];
|
|
916
|
+
const responses = Array.isArray(round.responses) ? round.responses : [];
|
|
917
|
+
const completed = responses.filter((x) => x.status === "completed").length;
|
|
918
|
+
const idx = typeof round.order_index === "number" ? round.order_index : 0;
|
|
919
|
+
console.log(` Round ${idx + 1} [${round.status || "-"}] · ${variants.length} variant(s) · ${completed}/${responses.length} responded`);
|
|
920
|
+
console.log(` "${truncate(String(round.prompt || ""), 100)}"`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
export function formatRoundDetail(round, json) {
|
|
925
|
+
if (json) {
|
|
926
|
+
console.log(jsonOutput(round));
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const variants = Array.isArray(round.variants) ? round.variants : [];
|
|
930
|
+
const responses = Array.isArray(round.responses) ? round.responses : [];
|
|
931
|
+
const idx = typeof round.order_index === "number" ? round.order_index : 0;
|
|
932
|
+
console.log(`Round ${idx + 1} [${round.status || "-"}]`);
|
|
933
|
+
console.log(`Prompt: ${round.prompt}`);
|
|
934
|
+
if (variants.length > 0) {
|
|
935
|
+
console.log("\nVariants:");
|
|
936
|
+
variants.forEach((v, i) => {
|
|
937
|
+
const variant = v;
|
|
938
|
+
const letter = variantLetter(i);
|
|
939
|
+
const label = variant.label ? ` "${variant.label}"` : "";
|
|
940
|
+
const preview = variant.kind === "text"
|
|
941
|
+
? truncate(String(variant.content || ""), 80)
|
|
942
|
+
: `<${variant.kind}>`;
|
|
943
|
+
console.log(` [${letter}]${label} (${variant.kind}): ${preview}`);
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
if (responses.length > 0) {
|
|
947
|
+
const completed = responses.filter((r) => r.status === "completed");
|
|
948
|
+
console.log(`\nResponses: ${completed.length}/${responses.length} completed`);
|
|
949
|
+
}
|
|
950
|
+
const summary = round.summary;
|
|
951
|
+
if (summary?.comment) {
|
|
952
|
+
console.log("\nSummary:");
|
|
953
|
+
console.log(` ${summary.comment}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
function computeVariantStats(round) {
|
|
957
|
+
const variants = Array.isArray(round.variants) ? round.variants : [];
|
|
958
|
+
const responses = Array.isArray(round.responses) ? round.responses : [];
|
|
959
|
+
const stats = variants.map((v, i) => {
|
|
960
|
+
const variant = v;
|
|
961
|
+
return {
|
|
962
|
+
letter: variantLetter(i),
|
|
963
|
+
label: variant.label ? String(variant.label) : undefined,
|
|
964
|
+
kind: String(variant.kind || "-"),
|
|
965
|
+
pickCount: 0,
|
|
966
|
+
ratingTotal: 0,
|
|
967
|
+
ratingCount: 0,
|
|
968
|
+
preview: variant.kind === "text"
|
|
969
|
+
? truncate(String(variant.content || ""), 60)
|
|
970
|
+
: `<${variant.kind}>`,
|
|
971
|
+
};
|
|
972
|
+
});
|
|
973
|
+
const idToIndex = new Map();
|
|
974
|
+
variants.forEach((v, i) => {
|
|
975
|
+
const id = v.id;
|
|
976
|
+
if (id)
|
|
977
|
+
idToIndex.set(String(id), i);
|
|
978
|
+
});
|
|
979
|
+
for (const r of responses) {
|
|
980
|
+
const resp = r;
|
|
981
|
+
if (resp.status !== "completed")
|
|
982
|
+
continue;
|
|
983
|
+
const pick = resp.variant_pick_id;
|
|
984
|
+
if (typeof pick === "string") {
|
|
985
|
+
const idx = idToIndex.get(pick);
|
|
986
|
+
if (idx !== undefined)
|
|
987
|
+
stats[idx].pickCount++;
|
|
988
|
+
}
|
|
989
|
+
const ratings = resp.variant_ratings;
|
|
990
|
+
if (ratings && typeof ratings === "object") {
|
|
991
|
+
for (const [vid, val] of Object.entries(ratings)) {
|
|
992
|
+
const idx = idToIndex.get(vid);
|
|
993
|
+
if (idx !== undefined && typeof val === "number") {
|
|
994
|
+
stats[idx].ratingTotal += val;
|
|
995
|
+
stats[idx].ratingCount++;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return stats;
|
|
1001
|
+
}
|
|
1002
|
+
export function formatAskResults(ask, json, roundFilter) {
|
|
1003
|
+
const rounds = (Array.isArray(ask.rounds) ? ask.rounds : []);
|
|
1004
|
+
const filtered = roundFilter !== undefined
|
|
1005
|
+
? rounds.filter((r) => (typeof r.order_index === "number" ? r.order_index : 0) === roundFilter - 1)
|
|
1006
|
+
: rounds;
|
|
1007
|
+
if (json) {
|
|
1008
|
+
const payload = roundFilter !== undefined ? { ...ask, rounds: filtered } : ask;
|
|
1009
|
+
console.log(jsonOutput(payload));
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
console.log(`${ask.name || "Untitled"} — Results`);
|
|
1013
|
+
if (filtered.length === 0) {
|
|
1014
|
+
console.log(roundFilter !== undefined ? `No round ${roundFilter}.` : "No rounds yet.");
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
for (const round of filtered) {
|
|
1018
|
+
const idx = typeof round.order_index === "number" ? round.order_index : 0;
|
|
1019
|
+
const responses = Array.isArray(round.responses) ? round.responses : [];
|
|
1020
|
+
const completed = responses.filter((r) => r.status === "completed");
|
|
1021
|
+
console.log(`\nRound ${idx + 1} [${round.status || "-"}] · ${completed.length}/${responses.length} responded`);
|
|
1022
|
+
console.log(` Prompt: "${truncate(String(round.prompt || ""), 100)}"`);
|
|
1023
|
+
const stats = computeVariantStats(round);
|
|
1024
|
+
if (stats.length > 0 && (round.wants_pick || round.wants_ratings)) {
|
|
1025
|
+
const hasPick = !!round.wants_pick;
|
|
1026
|
+
const hasRatings = !!round.wants_ratings;
|
|
1027
|
+
const headers = ["#", "LABEL", "KIND"];
|
|
1028
|
+
if (hasPick)
|
|
1029
|
+
headers.push("PICKS");
|
|
1030
|
+
if (hasRatings)
|
|
1031
|
+
headers.push("MEAN RATING");
|
|
1032
|
+
headers.push("PREVIEW");
|
|
1033
|
+
const rows = stats.map((s) => {
|
|
1034
|
+
const row = [s.letter, s.label || "-", s.kind];
|
|
1035
|
+
if (hasPick)
|
|
1036
|
+
row.push(String(s.pickCount));
|
|
1037
|
+
if (hasRatings) {
|
|
1038
|
+
row.push(s.ratingCount > 0 ? (s.ratingTotal / s.ratingCount).toFixed(2) : "-");
|
|
1039
|
+
}
|
|
1040
|
+
row.push(s.preview);
|
|
1041
|
+
return row;
|
|
1042
|
+
});
|
|
1043
|
+
console.log("");
|
|
1044
|
+
printTable(headers, rows);
|
|
1045
|
+
}
|
|
1046
|
+
else if (stats.length > 0) {
|
|
1047
|
+
console.log("");
|
|
1048
|
+
printTable(["#", "LABEL", "KIND", "PREVIEW"], stats.map((s) => [s.letter, s.label || "-", s.kind, s.preview]));
|
|
1049
|
+
}
|
|
1050
|
+
if (completed.length > 0) {
|
|
1051
|
+
console.log("\n Comments:");
|
|
1052
|
+
for (const r of completed.slice(0, 10)) {
|
|
1053
|
+
const resp = r;
|
|
1054
|
+
const comment = resp.comment ? truncate(String(resp.comment), 120) : "-";
|
|
1055
|
+
const tester = resp.tester_id ? deterministicAlias(ALIAS_PREFIX.tester, String(resp.tester_id)) : "-";
|
|
1056
|
+
console.log(` ${tester}: ${comment}`);
|
|
1057
|
+
}
|
|
1058
|
+
if (completed.length > 10) {
|
|
1059
|
+
console.log(` … and ${completed.length - 10} more`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
const summary = round.summary;
|
|
1063
|
+
if (summary?.comment) {
|
|
1064
|
+
console.log("\n Summary:");
|
|
1065
|
+
console.log(` ${summary.comment}`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
624
1069
|
// --- Config formatting ---
|
|
625
1070
|
export function formatConfigList(configs, json) {
|
|
626
1071
|
if (configs.length === 0) {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem paths used by the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Honors the `ISH_HOME` environment variable so users on systems with an
|
|
5
|
+
* XDG layout (or anywhere else) can override the default `~/.ish` root.
|
|
6
|
+
* Mirrors the pattern used by `gh` (GH_CONFIG_DIR) and `aws` (AWS_CONFIG_FILE).
|
|
7
|
+
*/
|
|
8
|
+
export declare function ishDir(): string;
|
|
9
|
+
export declare function configPath(): string;
|
|
10
|
+
export declare function aliasesPath(): string;
|
|
11
|
+
export declare function binDir(): string;
|
|
12
|
+
export declare function browsersDir(): string;
|
|
13
|
+
export declare function simulationsDir(): string;
|
|
14
|
+
export declare function cloudflaredBin(): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem paths used by the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Honors the `ISH_HOME` environment variable so users on systems with an
|
|
5
|
+
* XDG layout (or anywhere else) can override the default `~/.ish` root.
|
|
6
|
+
* Mirrors the pattern used by `gh` (GH_CONFIG_DIR) and `aws` (AWS_CONFIG_FILE).
|
|
7
|
+
*/
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
function rootDir() {
|
|
11
|
+
if (process.env.ISH_HOME)
|
|
12
|
+
return process.env.ISH_HOME;
|
|
13
|
+
return path.join(os.homedir(), ".ish");
|
|
14
|
+
}
|
|
15
|
+
export function ishDir() {
|
|
16
|
+
return rootDir();
|
|
17
|
+
}
|
|
18
|
+
export function configPath() {
|
|
19
|
+
return path.join(rootDir(), "config.json");
|
|
20
|
+
}
|
|
21
|
+
export function aliasesPath() {
|
|
22
|
+
return path.join(rootDir(), "aliases.json");
|
|
23
|
+
}
|
|
24
|
+
export function binDir() {
|
|
25
|
+
return path.join(rootDir(), "bin");
|
|
26
|
+
}
|
|
27
|
+
export function browsersDir() {
|
|
28
|
+
return path.join(rootDir(), "browsers");
|
|
29
|
+
}
|
|
30
|
+
export function simulationsDir() {
|
|
31
|
+
return path.join(rootDir(), "simulations");
|
|
32
|
+
}
|
|
33
|
+
export function cloudflaredBin() {
|
|
34
|
+
const exe = process.platform === "win32" ? "cloudflared.exe" : "cloudflared";
|
|
35
|
+
return path.join(binDir(), exe);
|
|
36
|
+
}
|