@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/index.js
CHANGED
|
@@ -7,83 +7,131 @@ import { upgrade } from "./upgrade.js";
|
|
|
7
7
|
import { registerWorkspaceCommands } from "./commands/workspace.js";
|
|
8
8
|
import { registerStudyCommands } from "./commands/study.js";
|
|
9
9
|
import { registerIterationCommands } from "./commands/iteration.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import { registerSimulationCommands } from "./commands/simulation.js";
|
|
10
|
+
import { registerProfileCommands } from "./commands/profile.js";
|
|
11
|
+
import { registerSourceCommands } from "./commands/source.js";
|
|
13
12
|
import { registerConfigCommands } from "./commands/config.js";
|
|
13
|
+
import { registerAskCommands } from "./commands/ask.js";
|
|
14
|
+
import { registerDocsCommands } from "./commands/docs.js";
|
|
15
|
+
import { registerInitCommands } from "./commands/init.js";
|
|
16
|
+
import { AGENT_HELP_FOOTER } from "./lib/docs.js";
|
|
17
|
+
import { runInline, EXIT_USAGE } from "./lib/command-helpers.js";
|
|
18
|
+
import { output } from "./lib/output.js";
|
|
14
19
|
import pkg from "../package.json" with { type: "json" };
|
|
15
20
|
const { version } = pkg;
|
|
16
21
|
program
|
|
17
22
|
.name("ish")
|
|
18
|
-
.description("Ish CLI —
|
|
19
|
-
.version(version)
|
|
23
|
+
.description("Ish CLI — run studies and asks against AI tester audiences")
|
|
24
|
+
.version(version)
|
|
25
|
+
.addHelpText("after", AGENT_HELP_FOOTER);
|
|
26
|
+
// Unified error envelope for Commander-level failures (unknown command,
|
|
27
|
+
// missing required option, etc.) so JSON consumers see the same shape
|
|
28
|
+
// regardless of whether the error originated in the CLI argv parser or
|
|
29
|
+
// the API. Without this, Commander would print plain text to stderr and
|
|
30
|
+
// — for missingMandatoryOptionValue — exit 0, breaking shell pipelines.
|
|
31
|
+
//
|
|
32
|
+
// Suppress Commander's own error formatter so the only error line is the
|
|
33
|
+
// envelope our exitOverride handler emits below.
|
|
34
|
+
program.configureOutput({
|
|
35
|
+
outputError: () => { },
|
|
36
|
+
});
|
|
37
|
+
program.exitOverride((err) => {
|
|
38
|
+
// Help and --version are normal exit-0 paths, not errors.
|
|
39
|
+
if (err.code === "commander.helpDisplayed"
|
|
40
|
+
|| err.code === "commander.version"
|
|
41
|
+
|| err.code === "commander.help") {
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
// Detect --json without relying on parsed opts (parse may have failed).
|
|
45
|
+
const useJson = process.argv.includes("--json") || !process.stdout.isTTY;
|
|
46
|
+
const envelope = {
|
|
47
|
+
error: err.message,
|
|
48
|
+
error_code: "usage_error",
|
|
49
|
+
status: 0,
|
|
50
|
+
retryable: false,
|
|
51
|
+
suggestions: ["Run `ish <command> --help` for usage"],
|
|
52
|
+
};
|
|
53
|
+
if (useJson) {
|
|
54
|
+
console.error(JSON.stringify(envelope));
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
console.error(`Error: ${err.message}`);
|
|
58
|
+
console.error(" → Run `ish <command> --help` for usage");
|
|
59
|
+
}
|
|
60
|
+
process.exit(EXIT_USAGE);
|
|
61
|
+
});
|
|
20
62
|
// Global options
|
|
21
63
|
program
|
|
22
64
|
.option("-t, --token <token>", "Auth token (or set ISH_TOKEN env var)")
|
|
65
|
+
.option("--token-file <path>", "Read auth token from a file (preferred over --token / ISH_TOKEN)")
|
|
23
66
|
.option("--api-url <url>", "Backend API URL (default: ISH_API_URL or https://api.ishlabs.io)")
|
|
24
67
|
.addOption(new Option("--dev", "Use local dev API (http://localhost:8000)").hideHelp())
|
|
25
68
|
.option("--json", "Output as JSON (auto-enabled when piped)")
|
|
26
69
|
.option("--fields <fields>", "Comma-separated fields to include in JSON output (e.g. alias,name,status)")
|
|
27
70
|
.option("--verbose", "Include full UUIDs and timestamps in JSON output")
|
|
28
|
-
.option("-
|
|
71
|
+
.option("--no-color", "Disable colored output (also honored: NO_COLOR env var)")
|
|
72
|
+
.option("-q, --quiet", "Suppress progress messages on stderr (no-op for read commands that emit none)");
|
|
29
73
|
// --- Inline commands (from upstream) ---
|
|
30
74
|
program
|
|
31
75
|
.command("login")
|
|
32
76
|
.description("Authenticate with Ish via your browser")
|
|
33
77
|
.action(async (_opts, cmd) => {
|
|
34
|
-
|
|
35
|
-
const globals = cmd.optsWithGlobals();
|
|
78
|
+
await runInline(cmd, async (globals) => {
|
|
36
79
|
const appUrl = globals.dev ? "http://localhost:3000" : getAppUrl();
|
|
37
80
|
const tokens = await login(appUrl);
|
|
38
81
|
const config = loadConfig();
|
|
39
82
|
config.access_token = tokens.accessToken;
|
|
40
83
|
config.refresh_token = tokens.refreshToken;
|
|
41
84
|
saveConfig(config);
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
catch (e) {
|
|
45
|
-
console.error(`Login failed: ${e instanceof Error ? e.message : e}`);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
85
|
+
output({ message: "Login successful" }, globals.json);
|
|
86
|
+
});
|
|
48
87
|
});
|
|
49
88
|
program
|
|
50
89
|
.command("logout")
|
|
51
90
|
.description("Remove saved authentication credentials")
|
|
52
|
-
.action(() => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
91
|
+
.action(async (_opts, cmd) => {
|
|
92
|
+
await runInline(cmd, async (globals) => {
|
|
93
|
+
const config = loadConfig();
|
|
94
|
+
delete config.access_token;
|
|
95
|
+
delete config.refresh_token;
|
|
96
|
+
delete config.token;
|
|
97
|
+
saveConfig(config);
|
|
98
|
+
output({ message: "Logged out" }, globals.json);
|
|
99
|
+
});
|
|
59
100
|
});
|
|
60
101
|
program
|
|
61
102
|
.command("connect")
|
|
62
103
|
.description("Expose your localhost to Ish via a Cloudflare tunnel")
|
|
63
104
|
.argument("<port>", "Local port to connect (e.g. 3000)")
|
|
105
|
+
.addHelpText("after", "\nNote: --json emits structured one-line JSON for connected/disconnected events. --fields and --quiet have limited effect; use --json for machine-readable output.")
|
|
64
106
|
.action(async (port, _opts, cmd) => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
107
|
+
await runInline(cmd, async (globals) => {
|
|
108
|
+
const portNum = parseInt(port, 10);
|
|
109
|
+
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
|
|
110
|
+
throw new Error(`Invalid port: ${port}`);
|
|
111
|
+
}
|
|
112
|
+
const apiUrl = globals.dev ? "http://localhost:8000" : globals.apiUrl;
|
|
113
|
+
await runTunnel(portNum, globals.token, apiUrl, globals.tokenFile, {
|
|
114
|
+
json: globals.json,
|
|
115
|
+
quiet: globals.quiet,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
73
118
|
});
|
|
74
119
|
// --- Modular command groups ---
|
|
75
120
|
registerWorkspaceCommands(program);
|
|
76
121
|
registerStudyCommands(program);
|
|
77
122
|
registerIterationCommands(program);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
registerSimulationCommands(program);
|
|
123
|
+
registerProfileCommands(program);
|
|
124
|
+
registerSourceCommands(program);
|
|
81
125
|
registerConfigCommands(program);
|
|
126
|
+
registerAskCommands(program);
|
|
127
|
+
registerDocsCommands(program);
|
|
128
|
+
registerInitCommands(program);
|
|
82
129
|
program
|
|
83
130
|
.command("upgrade")
|
|
84
131
|
.description("Update ish to the latest version")
|
|
85
|
-
.option("--
|
|
132
|
+
.option("--release <version>", "Install a specific release (e.g. 0.8.1)")
|
|
133
|
+
.addHelpText("after", "\nPin a specific release with --release <version>. Note: --version is the global CLI-version flag; use --release here.")
|
|
86
134
|
.action(async (options) => {
|
|
87
|
-
await upgrade(version, options.
|
|
135
|
+
await upgrade(version, options.release);
|
|
88
136
|
});
|
|
89
137
|
program.parse();
|
|
@@ -11,9 +11,12 @@ export declare const ALIAS_PREFIX: {
|
|
|
11
11
|
readonly study: "s";
|
|
12
12
|
readonly iteration: "i";
|
|
13
13
|
readonly testerProfile: "tp";
|
|
14
|
+
readonly testerProfileSource: "tps";
|
|
14
15
|
readonly tester: "t";
|
|
15
16
|
readonly config: "c";
|
|
16
17
|
readonly job: "j";
|
|
18
|
+
readonly ask: "a";
|
|
19
|
+
readonly askRound: "r";
|
|
17
20
|
};
|
|
18
21
|
/**
|
|
19
22
|
* Save aliases for a list of IDs under the given prefix.
|
package/dist/lib/alias-store.js
CHANGED
|
@@ -7,17 +7,19 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
-
import
|
|
11
|
-
const ALIASES_FILE = path.join(os.homedir(), ".ish", "aliases.json");
|
|
10
|
+
import { aliasesPath } from "./paths.js";
|
|
12
11
|
/** Entity type → alias prefix */
|
|
13
12
|
export const ALIAS_PREFIX = {
|
|
14
13
|
workspace: "w",
|
|
15
14
|
study: "s",
|
|
16
15
|
iteration: "i",
|
|
17
16
|
testerProfile: "tp",
|
|
17
|
+
testerProfileSource: "tps",
|
|
18
18
|
tester: "t",
|
|
19
19
|
config: "c",
|
|
20
20
|
job: "j",
|
|
21
|
+
ask: "a",
|
|
22
|
+
askRound: "r",
|
|
21
23
|
};
|
|
22
24
|
/** Format a number with zero-padding (minimum 2 digits). */
|
|
23
25
|
function padNum(n) {
|
|
@@ -25,8 +27,8 @@ function padNum(n) {
|
|
|
25
27
|
}
|
|
26
28
|
function loadAliases() {
|
|
27
29
|
try {
|
|
28
|
-
if (fs.existsSync(
|
|
29
|
-
return JSON.parse(fs.readFileSync(
|
|
30
|
+
if (fs.existsSync(aliasesPath())) {
|
|
31
|
+
return JSON.parse(fs.readFileSync(aliasesPath(), "utf-8"));
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
catch {
|
|
@@ -35,11 +37,11 @@ function loadAliases() {
|
|
|
35
37
|
return {};
|
|
36
38
|
}
|
|
37
39
|
function persistAliases(aliases) {
|
|
38
|
-
const dir = path.dirname(
|
|
40
|
+
const dir = path.dirname(aliasesPath());
|
|
39
41
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
40
|
-
const tmp =
|
|
42
|
+
const tmp = aliasesPath() + ".tmp";
|
|
41
43
|
fs.writeFileSync(tmp, JSON.stringify(aliases, null, 2) + "\n", { mode: 0o600 });
|
|
42
|
-
fs.renameSync(tmp,
|
|
44
|
+
fs.renameSync(tmp, aliasesPath());
|
|
43
45
|
}
|
|
44
46
|
/**
|
|
45
47
|
* Save aliases for a list of IDs under the given prefix.
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ export declare class ApiError extends Error {
|
|
|
9
9
|
retryable: boolean;
|
|
10
10
|
constructor(status: number, statusText: string, body: unknown);
|
|
11
11
|
}
|
|
12
|
+
interface RequestOpts {
|
|
13
|
+
timeout?: number;
|
|
14
|
+
}
|
|
12
15
|
export declare class ApiClient {
|
|
13
16
|
private baseUrl;
|
|
14
17
|
private token;
|
|
@@ -18,12 +21,11 @@ export declare class ApiClient {
|
|
|
18
21
|
});
|
|
19
22
|
get accessToken(): string;
|
|
20
23
|
private headers;
|
|
21
|
-
get<T = unknown>(path: string, params?: Record<string, string
|
|
22
|
-
post<T = unknown>(path: string, body?: unknown, opts?:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
del(path: string): Promise<void>;
|
|
24
|
+
get<T = unknown>(path: string, params?: Record<string, string | string[]>, opts?: RequestOpts): Promise<T>;
|
|
25
|
+
post<T = unknown>(path: string, body?: unknown, opts?: RequestOpts): Promise<T>;
|
|
26
|
+
put<T = unknown>(path: string, body?: unknown, opts?: RequestOpts): Promise<T>;
|
|
27
|
+
patch<T = unknown>(path: string, body?: unknown, opts?: RequestOpts): Promise<T>;
|
|
28
|
+
del(path: string, opts?: RequestOpts): Promise<void>;
|
|
27
29
|
localSimInit(body: {
|
|
28
30
|
tester_id: string;
|
|
29
31
|
study_id: string;
|
|
@@ -56,3 +58,4 @@ export declare class ApiClient {
|
|
|
56
58
|
}>;
|
|
57
59
|
private handleResponse;
|
|
58
60
|
}
|
|
61
|
+
export {};
|
package/dist/lib/api-client.js
CHANGED
|
@@ -11,6 +11,8 @@ function mapErrorCode(status) {
|
|
|
11
11
|
return "not_found";
|
|
12
12
|
if (status === 402)
|
|
13
13
|
return "insufficient_credits";
|
|
14
|
+
if (status === 408)
|
|
15
|
+
return "timeout";
|
|
14
16
|
if (status === 422)
|
|
15
17
|
return "validation_error";
|
|
16
18
|
if (status === 429)
|
|
@@ -20,7 +22,7 @@ function mapErrorCode(status) {
|
|
|
20
22
|
return "request_failed";
|
|
21
23
|
}
|
|
22
24
|
function isRetryable(status) {
|
|
23
|
-
return status === 429 || status >= 500;
|
|
25
|
+
return status === 408 || status === 429 || status >= 500;
|
|
24
26
|
}
|
|
25
27
|
export class ApiError extends Error {
|
|
26
28
|
status;
|
|
@@ -41,6 +43,21 @@ export class ApiError extends Error {
|
|
|
41
43
|
this.retryable = isRetryable(status);
|
|
42
44
|
}
|
|
43
45
|
}
|
|
46
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
47
|
+
function timeoutError(method, timeoutMs) {
|
|
48
|
+
const seconds = Math.round(timeoutMs / 1000);
|
|
49
|
+
return new ApiError(408, "Request Timeout", { detail: `${method} request timed out after ${seconds}s. The server may be slow — try again.` });
|
|
50
|
+
}
|
|
51
|
+
function isAbortTimeout(err) {
|
|
52
|
+
return err instanceof DOMException
|
|
53
|
+
&& (err.name === "TimeoutError" || err.name === "AbortError");
|
|
54
|
+
}
|
|
55
|
+
function networkError(url) {
|
|
56
|
+
const err = new ApiError(0, "Network Error", { detail: `Could not reach API at ${url}` });
|
|
57
|
+
err.error_code = "network_error";
|
|
58
|
+
err.retryable = true;
|
|
59
|
+
return err;
|
|
60
|
+
}
|
|
44
61
|
export class ApiClient {
|
|
45
62
|
baseUrl;
|
|
46
63
|
token;
|
|
@@ -57,34 +74,50 @@ export class ApiClient {
|
|
|
57
74
|
"Content-Type": "application/json",
|
|
58
75
|
};
|
|
59
76
|
}
|
|
60
|
-
async get(path, params) {
|
|
77
|
+
async get(path, params, opts) {
|
|
61
78
|
let url = `${this.baseUrl}${path}`;
|
|
62
79
|
if (params) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
const tuples = [];
|
|
81
|
+
for (const [k, v] of Object.entries(params)) {
|
|
82
|
+
if (v === undefined || v === "")
|
|
83
|
+
continue;
|
|
84
|
+
if (Array.isArray(v)) {
|
|
85
|
+
for (const item of v) {
|
|
86
|
+
if (item !== undefined && item !== "")
|
|
87
|
+
tuples.push([k, String(item)]);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
tuples.push([k, v]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (tuples.length > 0) {
|
|
95
|
+
url += "?" + new URLSearchParams(tuples).toString();
|
|
66
96
|
}
|
|
67
97
|
}
|
|
98
|
+
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
68
99
|
let res;
|
|
69
100
|
try {
|
|
70
101
|
res = await fetch(url, {
|
|
71
102
|
headers: this.headers(),
|
|
72
|
-
signal: AbortSignal.timeout(
|
|
103
|
+
signal: AbortSignal.timeout(timeout),
|
|
73
104
|
});
|
|
74
105
|
}
|
|
75
106
|
catch (err) {
|
|
76
|
-
if (
|
|
77
|
-
throw
|
|
78
|
-
|
|
107
|
+
if (isAbortTimeout(err))
|
|
108
|
+
throw timeoutError("GET", timeout);
|
|
109
|
+
if (err instanceof TypeError)
|
|
110
|
+
throw networkError(url);
|
|
79
111
|
throw err;
|
|
80
112
|
}
|
|
81
113
|
return this.handleResponse(res);
|
|
82
114
|
}
|
|
83
115
|
async post(path, body, opts) {
|
|
84
|
-
const timeout = opts?.timeout ??
|
|
116
|
+
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
117
|
+
const url = `${this.baseUrl}${path}`;
|
|
85
118
|
let res;
|
|
86
119
|
try {
|
|
87
|
-
res = await fetch(
|
|
120
|
+
res = await fetch(url, {
|
|
88
121
|
method: "POST",
|
|
89
122
|
headers: this.headers(),
|
|
90
123
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
@@ -92,44 +125,72 @@ export class ApiClient {
|
|
|
92
125
|
});
|
|
93
126
|
}
|
|
94
127
|
catch (err) {
|
|
95
|
-
if (
|
|
96
|
-
throw
|
|
97
|
-
|
|
128
|
+
if (isAbortTimeout(err))
|
|
129
|
+
throw timeoutError("POST", timeout);
|
|
130
|
+
if (err instanceof TypeError)
|
|
131
|
+
throw networkError(url);
|
|
98
132
|
throw err;
|
|
99
133
|
}
|
|
100
134
|
return this.handleResponse(res);
|
|
101
135
|
}
|
|
102
|
-
async put(path, body) {
|
|
136
|
+
async put(path, body, opts) {
|
|
137
|
+
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
138
|
+
const url = `${this.baseUrl}${path}`;
|
|
103
139
|
let res;
|
|
104
140
|
try {
|
|
105
|
-
res = await fetch(
|
|
141
|
+
res = await fetch(url, {
|
|
106
142
|
method: "PUT",
|
|
107
143
|
headers: this.headers(),
|
|
108
144
|
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
109
|
-
signal: AbortSignal.timeout(
|
|
145
|
+
signal: AbortSignal.timeout(timeout),
|
|
110
146
|
});
|
|
111
147
|
}
|
|
112
148
|
catch (err) {
|
|
113
|
-
if (
|
|
114
|
-
throw
|
|
115
|
-
|
|
149
|
+
if (isAbortTimeout(err))
|
|
150
|
+
throw timeoutError("PUT", timeout);
|
|
151
|
+
if (err instanceof TypeError)
|
|
152
|
+
throw networkError(url);
|
|
116
153
|
throw err;
|
|
117
154
|
}
|
|
118
155
|
return this.handleResponse(res);
|
|
119
156
|
}
|
|
120
|
-
async
|
|
157
|
+
async patch(path, body, opts) {
|
|
158
|
+
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
159
|
+
const url = `${this.baseUrl}${path}`;
|
|
121
160
|
let res;
|
|
122
161
|
try {
|
|
123
|
-
res = await fetch(
|
|
162
|
+
res = await fetch(url, {
|
|
163
|
+
method: "PATCH",
|
|
164
|
+
headers: this.headers(),
|
|
165
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
166
|
+
signal: AbortSignal.timeout(timeout),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
if (isAbortTimeout(err))
|
|
171
|
+
throw timeoutError("PATCH", timeout);
|
|
172
|
+
if (err instanceof TypeError)
|
|
173
|
+
throw networkError(url);
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
return this.handleResponse(res);
|
|
177
|
+
}
|
|
178
|
+
async del(path, opts) {
|
|
179
|
+
const timeout = opts?.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
180
|
+
const url = `${this.baseUrl}${path}`;
|
|
181
|
+
let res;
|
|
182
|
+
try {
|
|
183
|
+
res = await fetch(url, {
|
|
124
184
|
method: "DELETE",
|
|
125
185
|
headers: this.headers(),
|
|
126
|
-
signal: AbortSignal.timeout(
|
|
186
|
+
signal: AbortSignal.timeout(timeout),
|
|
127
187
|
});
|
|
128
188
|
}
|
|
129
189
|
catch (err) {
|
|
130
|
-
if (
|
|
131
|
-
throw
|
|
132
|
-
|
|
190
|
+
if (isAbortTimeout(err))
|
|
191
|
+
throw timeoutError("DELETE", timeout);
|
|
192
|
+
if (err instanceof TypeError)
|
|
193
|
+
throw networkError(url);
|
|
133
194
|
throw err;
|
|
134
195
|
}
|
|
135
196
|
if (!res.ok) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader/validator for `--questions <file.json>`.
|
|
3
|
+
*
|
|
4
|
+
* Accepts a JSON array of InterviewQuestion entries. Mirrors the loose validation
|
|
5
|
+
* used by `ish study create --questions`: requires `question: string` on every entry,
|
|
6
|
+
* passes the rest through. The backend is the source of truth for the full schema.
|
|
7
|
+
*/
|
|
8
|
+
import type { InterviewQuestion } from "./types.js";
|
|
9
|
+
export declare function loadQuestionsManifest(filePath: string): InterviewQuestion[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader/validator for `--questions <file.json>`.
|
|
3
|
+
*
|
|
4
|
+
* Accepts a JSON array of InterviewQuestion entries. Mirrors the loose validation
|
|
5
|
+
* used by `ish study create --questions`: requires `question: string` on every entry,
|
|
6
|
+
* passes the rest through. The backend is the source of truth for the full schema.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { resolve as resolvePath } from "node:path";
|
|
10
|
+
export function loadQuestionsManifest(filePath) {
|
|
11
|
+
let raw;
|
|
12
|
+
try {
|
|
13
|
+
raw = readFileSync(resolvePath(filePath), "utf-8");
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error(`Cannot read questions file: ${filePath}`);
|
|
17
|
+
}
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new Error(`Invalid JSON in questions file: ${filePath}`);
|
|
24
|
+
}
|
|
25
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
26
|
+
throw new Error(`Questions file must be a non-empty JSON array: ${filePath}`);
|
|
27
|
+
}
|
|
28
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
29
|
+
const q = parsed[i];
|
|
30
|
+
if (!q || typeof q !== "object" || typeof q.question !== "string" || !q.question.trim()) {
|
|
31
|
+
throw new Error(`questions[${i}].question must be a non-empty string.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return parsed;
|
|
35
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse `--variant` flags and `--variants <manifest.json>` into AskVariantInput[],
|
|
3
|
+
* uploading any local files to the asks variant-upload endpoint along the way.
|
|
4
|
+
*
|
|
5
|
+
* Flag form: `--variant <kind>:<value>[::label=<label>]`
|
|
6
|
+
* text:"hello" → literal text
|
|
7
|
+
* text:@./body.md → text loaded from file
|
|
8
|
+
* ./logo.png → media; kind inferred from MIME
|
|
9
|
+
* image:./logo.png → media; kind override
|
|
10
|
+
* ./hero.png::label=B → media with label
|
|
11
|
+
*
|
|
12
|
+
* Manifest form (--variants ./manifest.json): JSON array of
|
|
13
|
+
* { kind: "text"|"image"|..., label?: string, content?: string, file?: string }
|
|
14
|
+
* - kind="text": `content` holds the literal text (or `file` to load text from disk).
|
|
15
|
+
* - kind=media: `file` is a local path to upload, or `content` is an already-uploaded file_path.
|
|
16
|
+
*/
|
|
17
|
+
import type { ApiClient } from "./api-client.js";
|
|
18
|
+
import type { AskVariantInput, AskVariantKind } from "./types.js";
|
|
19
|
+
/** Internal representation of a parsed variant before any upload happens. */
|
|
20
|
+
export interface ParsedVariant {
|
|
21
|
+
kind: AskVariantKind;
|
|
22
|
+
label?: string;
|
|
23
|
+
/** For kind=text: the literal text. For media: a local file path or an already-set file_path. */
|
|
24
|
+
source: string;
|
|
25
|
+
/** Whether `source` is a local file that still needs to be uploaded. */
|
|
26
|
+
needsUpload: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Parse a single `--variant` flag value. */
|
|
29
|
+
export declare function parseVariantFlag(raw: string): ParsedVariant;
|
|
30
|
+
export declare function parseVariantFlags(flags: string[]): ParsedVariant[];
|
|
31
|
+
/** Load a JSON manifest (an array of variant entries) from disk. */
|
|
32
|
+
export declare function loadVariantManifest(filePath: string): ParsedVariant[];
|
|
33
|
+
/**
|
|
34
|
+
* Walk the parsed variants, request signed upload URLs for the ones that need
|
|
35
|
+
* uploading, PUT each file to its signed URL, then return an AskVariantInput[]
|
|
36
|
+
* with the right `content` value (file_path for media, literal for text).
|
|
37
|
+
*/
|
|
38
|
+
export declare function uploadAndBuildVariants(client: ApiClient, productId: string, parsed: ParsedVariant[], opts?: {
|
|
39
|
+
quiet?: boolean;
|
|
40
|
+
}): Promise<AskVariantInput[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Convenience: parse flags OR manifest, validate that they're not both set, and
|
|
43
|
+
* return the parsed list. Caller still has to call `uploadAndBuildVariants`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseVariantInputs(opts: {
|
|
46
|
+
variant?: string[];
|
|
47
|
+
variants?: string;
|
|
48
|
+
}): ParsedVariant[];
|