@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,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audience source upload + processing lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the frontend flow in ensure-sources-processed.ts and
|
|
5
|
+
* UploadSection.tsx:
|
|
6
|
+
* 1. POST /tester-profiles/sources/initiate → signed URL + source_id
|
|
7
|
+
* 2. PUT signed_url → upload bytes (x-upsert: true)
|
|
8
|
+
* 3. POST /tester-profiles/sources/{id}/confirm → kick off parse / transcribe
|
|
9
|
+
* 4. GET /tester-profiles/sources/{id} → poll until terminal status
|
|
10
|
+
*
|
|
11
|
+
* Distinct from the study upload in upload.ts: different endpoints and a
|
|
12
|
+
* fourth polling step because text/image are parsed inline but audio is
|
|
13
|
+
* transcribed by an async worker.
|
|
14
|
+
*/
|
|
15
|
+
import type { ApiClient } from "./api-client.js";
|
|
16
|
+
import type { AudienceSource, SourceKind } from "./types.js";
|
|
17
|
+
/**
|
|
18
|
+
* Map a local file to the backend's SourceKind enum.
|
|
19
|
+
* Throws if the extension/MIME isn't recognized — caller can pass an
|
|
20
|
+
* explicit `kind` to override.
|
|
21
|
+
*/
|
|
22
|
+
export declare function inferSourceKind(filePath: string): SourceKind;
|
|
23
|
+
export declare function isUuid(value: string): boolean;
|
|
24
|
+
/** Poll a source until it reaches a terminal status or the timeout fires. */
|
|
25
|
+
export declare function pollSourceUntilTerminal(client: ApiClient, sourceId: string, timeoutMs?: number): Promise<AudienceSource>;
|
|
26
|
+
export interface UploadAndProcessOpts {
|
|
27
|
+
productId: string;
|
|
28
|
+
filePath: string;
|
|
29
|
+
kind?: SourceKind;
|
|
30
|
+
description?: string;
|
|
31
|
+
diarize?: boolean;
|
|
32
|
+
/** When false, return after confirm without polling. Default true. */
|
|
33
|
+
wait?: boolean;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
/** Print progress lines to stderr. Default true unless quiet. */
|
|
36
|
+
quiet?: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Run the full initiate → PUT → confirm → poll cycle for a single file. */
|
|
39
|
+
export declare function uploadAndProcessSource(client: ApiClient, opts: UploadAndProcessOpts): Promise<AudienceSource>;
|
|
40
|
+
export interface ResolveSourceRefOpts {
|
|
41
|
+
productId: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
diarize?: boolean;
|
|
44
|
+
wait?: boolean;
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
quiet?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve a `--source` value: a UUID is returned as-is; a local path is
|
|
50
|
+
* uploaded and the resulting source ID is returned.
|
|
51
|
+
*
|
|
52
|
+
* If a UUID-shaped string is provided but turns out not to be a real
|
|
53
|
+
* source, the backend rejects it on /generate — no need to pre-check.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveSourceRef(client: ApiClient, value: string, opts: ResolveSourceRefOpts): Promise<string>;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audience source upload + processing lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the frontend flow in ensure-sources-processed.ts and
|
|
5
|
+
* UploadSection.tsx:
|
|
6
|
+
* 1. POST /tester-profiles/sources/initiate → signed URL + source_id
|
|
7
|
+
* 2. PUT signed_url → upload bytes (x-upsert: true)
|
|
8
|
+
* 3. POST /tester-profiles/sources/{id}/confirm → kick off parse / transcribe
|
|
9
|
+
* 4. GET /tester-profiles/sources/{id} → poll until terminal status
|
|
10
|
+
*
|
|
11
|
+
* Distinct from the study upload in upload.ts: different endpoints and a
|
|
12
|
+
* fourth polling step because text/image are parsed inline but audio is
|
|
13
|
+
* transcribed by an async worker.
|
|
14
|
+
*/
|
|
15
|
+
import { readFile } from "node:fs/promises";
|
|
16
|
+
import { basename, extname, resolve as resolvePath } from "node:path";
|
|
17
|
+
import { detectMimeType, validateFile } from "./upload.js";
|
|
18
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
19
|
+
const POLL_INTERVAL_MS = 2_000;
|
|
20
|
+
const DEFAULT_POLL_TIMEOUT_MS = 300_000; // 5 min — matches frontend
|
|
21
|
+
const TERMINAL_STATUSES = new Set(["processed", "failed"]);
|
|
22
|
+
const AUDIO_EXTS = new Set([".mp3", ".wav", ".m4a", ".ogg", ".flac", ".aac"]);
|
|
23
|
+
const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"]);
|
|
24
|
+
const TEXT_EXTS = new Set([
|
|
25
|
+
".pdf",
|
|
26
|
+
".txt",
|
|
27
|
+
".md",
|
|
28
|
+
".csv",
|
|
29
|
+
".json",
|
|
30
|
+
".html",
|
|
31
|
+
".htm",
|
|
32
|
+
".doc",
|
|
33
|
+
".docx",
|
|
34
|
+
]);
|
|
35
|
+
/**
|
|
36
|
+
* Map a local file to the backend's SourceKind enum.
|
|
37
|
+
* Throws if the extension/MIME isn't recognized — caller can pass an
|
|
38
|
+
* explicit `kind` to override.
|
|
39
|
+
*/
|
|
40
|
+
export function inferSourceKind(filePath) {
|
|
41
|
+
const ext = extname(filePath).toLowerCase();
|
|
42
|
+
if (AUDIO_EXTS.has(ext))
|
|
43
|
+
return "audio";
|
|
44
|
+
if (IMAGE_EXTS.has(ext))
|
|
45
|
+
return "image";
|
|
46
|
+
if (TEXT_EXTS.has(ext))
|
|
47
|
+
return "text_file";
|
|
48
|
+
// Fallback to MIME type
|
|
49
|
+
const mime = detectMimeType(filePath);
|
|
50
|
+
if (mime.startsWith("audio/"))
|
|
51
|
+
return "audio";
|
|
52
|
+
if (mime.startsWith("image/"))
|
|
53
|
+
return "image";
|
|
54
|
+
if (mime === "application/pdf" ||
|
|
55
|
+
mime.startsWith("text/") ||
|
|
56
|
+
mime === "application/json" ||
|
|
57
|
+
mime.includes("officedocument")) {
|
|
58
|
+
return "text_file";
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`Cannot infer source kind for ${filePath}. Pass --kind text_file|audio|image to override.`);
|
|
61
|
+
}
|
|
62
|
+
export function isUuid(value) {
|
|
63
|
+
return UUID_RE.test(value);
|
|
64
|
+
}
|
|
65
|
+
/** PUT the file bytes to the Supabase signed URL. */
|
|
66
|
+
async function putFileToSignedUrl(signedUrl, filePath, mime) {
|
|
67
|
+
const buffer = await readFile(filePath);
|
|
68
|
+
// Supabase signed-upload endpoint authenticates via the ?token=... query
|
|
69
|
+
// string already in the URL. Adding Authorization: Bearer here makes
|
|
70
|
+
// Supabase fall through to JWT validation and 400s on PDFs (see frontend
|
|
71
|
+
// putAudienceSourceFile comment).
|
|
72
|
+
const res = await fetch(signedUrl, {
|
|
73
|
+
method: "PUT",
|
|
74
|
+
headers: {
|
|
75
|
+
"Content-Type": mime,
|
|
76
|
+
"x-upsert": "true",
|
|
77
|
+
},
|
|
78
|
+
body: buffer,
|
|
79
|
+
signal: AbortSignal.timeout(300_000),
|
|
80
|
+
});
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const body = await res.text().catch(() => "");
|
|
83
|
+
throw new Error(`Source upload failed (HTTP ${res.status} ${res.statusText})${body ? ` — ${body.slice(0, 500)}` : ""}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Poll a source until it reaches a terminal status or the timeout fires. */
|
|
87
|
+
export async function pollSourceUntilTerminal(client, sourceId, timeoutMs = DEFAULT_POLL_TIMEOUT_MS) {
|
|
88
|
+
const deadline = Date.now() + timeoutMs;
|
|
89
|
+
while (true) {
|
|
90
|
+
const source = await client.get(`/tester-profiles/sources/${sourceId}`);
|
|
91
|
+
if (TERMINAL_STATUSES.has(source.status))
|
|
92
|
+
return source;
|
|
93
|
+
if (Date.now() >= deadline) {
|
|
94
|
+
throw new Error(`Timed out after ${Math.round(timeoutMs / 1000)}s waiting for source ${sourceId} (status: ${source.status})`);
|
|
95
|
+
}
|
|
96
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Run the full initiate → PUT → confirm → poll cycle for a single file. */
|
|
100
|
+
export async function uploadAndProcessSource(client, opts) {
|
|
101
|
+
const resolved = resolvePath(opts.filePath);
|
|
102
|
+
const { mime } = await validateFile(opts.filePath);
|
|
103
|
+
const fileName = basename(resolved);
|
|
104
|
+
const kind = opts.kind ?? inferSourceKind(opts.filePath);
|
|
105
|
+
const log = (msg) => {
|
|
106
|
+
if (!opts.quiet)
|
|
107
|
+
process.stderr.write(msg);
|
|
108
|
+
};
|
|
109
|
+
log(`Uploading ${fileName}…`);
|
|
110
|
+
const init = await client.post("/tester-profiles/sources/initiate", {
|
|
111
|
+
product_id: opts.productId,
|
|
112
|
+
file_name: fileName,
|
|
113
|
+
content_type: mime,
|
|
114
|
+
kind,
|
|
115
|
+
}, { timeout: 60_000 });
|
|
116
|
+
await putFileToSignedUrl(init.signed_url, resolved, mime);
|
|
117
|
+
const confirmed = await client.post(`/tester-profiles/sources/${init.source_id}/confirm`, {
|
|
118
|
+
diarize: opts.diarize,
|
|
119
|
+
description: opts.description,
|
|
120
|
+
}, { timeout: 60_000 });
|
|
121
|
+
if (opts.wait === false) {
|
|
122
|
+
log(` ${confirmed.status}.\n`);
|
|
123
|
+
return confirmed;
|
|
124
|
+
}
|
|
125
|
+
if (TERMINAL_STATUSES.has(confirmed.status)) {
|
|
126
|
+
log(` ${confirmed.status}.\n`);
|
|
127
|
+
return confirmed;
|
|
128
|
+
}
|
|
129
|
+
log(" processing…");
|
|
130
|
+
const final = await pollSourceUntilTerminal(client, init.source_id, opts.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS);
|
|
131
|
+
log(` ${final.status}.\n`);
|
|
132
|
+
return final;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Resolve a `--source` value: a UUID is returned as-is; a local path is
|
|
136
|
+
* uploaded and the resulting source ID is returned.
|
|
137
|
+
*
|
|
138
|
+
* If a UUID-shaped string is provided but turns out not to be a real
|
|
139
|
+
* source, the backend rejects it on /generate — no need to pre-check.
|
|
140
|
+
*/
|
|
141
|
+
export async function resolveSourceRef(client, value, opts) {
|
|
142
|
+
if (isUuid(value))
|
|
143
|
+
return value;
|
|
144
|
+
const source = await uploadAndProcessSource(client, {
|
|
145
|
+
productId: opts.productId,
|
|
146
|
+
filePath: value,
|
|
147
|
+
description: opts.description,
|
|
148
|
+
diarize: opts.diarize,
|
|
149
|
+
wait: opts.wait,
|
|
150
|
+
timeoutMs: opts.timeoutMs,
|
|
151
|
+
quiet: opts.quiet,
|
|
152
|
+
});
|
|
153
|
+
if (source.status === "failed") {
|
|
154
|
+
throw new Error(`Source ${source.original_filename} failed to process: ${source.error ?? "unknown error"}`);
|
|
155
|
+
}
|
|
156
|
+
return source.id;
|
|
157
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site-access helpers — workspace-level credentials for gated test sites.
|
|
3
|
+
*
|
|
4
|
+
* Site-access is stored as product-level secrets with reserved keys. The
|
|
5
|
+
* backend enforces pair invariants (e.g. BASIC_AUTH_USERNAME and
|
|
6
|
+
* BASIC_AUTH_PASSWORD must be created together), so new keys must go through
|
|
7
|
+
* the batch endpoint while updates to existing keys go through PUT.
|
|
8
|
+
*
|
|
9
|
+
* Key constants are kept in sync with the frontend at
|
|
10
|
+
* /ish-frontend/src/lib/site-access/secret-keys.ts.
|
|
11
|
+
*/
|
|
12
|
+
import type { ApiClient } from "./api-client.js";
|
|
13
|
+
import type { Secret, SecretScope, SecretVariableType } from "./types.js";
|
|
14
|
+
export declare const SITE_ACCESS_KEYS: {
|
|
15
|
+
readonly basicAuthUsername: "BASIC_AUTH_USERNAME";
|
|
16
|
+
readonly basicAuthPassword: "BASIC_AUTH_PASSWORD";
|
|
17
|
+
readonly basicAuthOrigin: "BASIC_AUTH_ORIGIN";
|
|
18
|
+
readonly loginUsername: "LOGIN_USERNAME";
|
|
19
|
+
readonly loginPassword: "LOGIN_PASSWORD";
|
|
20
|
+
readonly sessionCookieName: "SESSION_COOKIE_NAME";
|
|
21
|
+
readonly sessionCookieValue: "SESSION_COOKIE_VALUE";
|
|
22
|
+
readonly sessionCookieOrigin: "SESSION_COOKIE_ORIGIN";
|
|
23
|
+
readonly publicAffirmedOrigin: "SITE_ACCESS_PUBLIC_AFFIRMED_ORIGIN";
|
|
24
|
+
};
|
|
25
|
+
export type SiteAccessKey = (typeof SITE_ACCESS_KEYS)[keyof typeof SITE_ACCESS_KEYS];
|
|
26
|
+
/** Bare-origin form (`https://host[:port]`) used for credential binding. */
|
|
27
|
+
export declare function deriveOrigin(url: string | undefined | null): string | null;
|
|
28
|
+
export interface SiteAccessState {
|
|
29
|
+
/** Indexed view of every site-access secret currently on the product. */
|
|
30
|
+
byKey: Map<string, Secret>;
|
|
31
|
+
hasBasicAuth: boolean;
|
|
32
|
+
hasSessionCookie: boolean;
|
|
33
|
+
hasLoginCreds: boolean;
|
|
34
|
+
publicAffirmedOrigin: string | null;
|
|
35
|
+
basicAuthOrigin: string | null;
|
|
36
|
+
sessionCookieOrigin: string | null;
|
|
37
|
+
}
|
|
38
|
+
export declare function loadSiteAccess(client: ApiClient, productId: string): Promise<SiteAccessState>;
|
|
39
|
+
export interface WriteItem {
|
|
40
|
+
key: string;
|
|
41
|
+
value: string;
|
|
42
|
+
scope: SecretScope;
|
|
43
|
+
variable_type: SecretVariableType;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Apply a set of paired-key writes. Keys that don't yet exist go through the
|
|
47
|
+
* batch endpoint in one request — the backend's pair invariant is checked
|
|
48
|
+
* once against the projected union, not per-item where the first POST would
|
|
49
|
+
* always fail with "missing pair member". Existing keys are updated via PUT.
|
|
50
|
+
*
|
|
51
|
+
* Mirrors useSiteAccessSecrets.applySaveInstructions in the frontend.
|
|
52
|
+
*/
|
|
53
|
+
export declare function applySiteAccessWrites(client: ApiClient, productId: string, state: SiteAccessState, items: WriteItem[]): Promise<void>;
|
|
54
|
+
export declare function deleteSiteAccessKeys(client: ApiClient, productId: string, state: SiteAccessState, keys: string[]): Promise<number>;
|
|
55
|
+
/** Methods the user can clear individually. */
|
|
56
|
+
export type SiteAccessMethod = "basic-auth" | "cookie" | "login" | "public" | "all";
|
|
57
|
+
export declare function keysForMethod(method: SiteAccessMethod): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Saving any credential clears the public-affirmed flag (matches frontend).
|
|
60
|
+
* Returns true if a flag was actually cleared, false if no flag was set.
|
|
61
|
+
*/
|
|
62
|
+
export declare function clearPublicAffirmation(client: ApiClient, productId: string, state: SiteAccessState): Promise<boolean>;
|
|
63
|
+
export interface SiteAccessSummary {
|
|
64
|
+
basic_auth: {
|
|
65
|
+
configured: boolean;
|
|
66
|
+
origin: string | null;
|
|
67
|
+
};
|
|
68
|
+
session_cookie: {
|
|
69
|
+
configured: boolean;
|
|
70
|
+
origin: string | null;
|
|
71
|
+
};
|
|
72
|
+
login: {
|
|
73
|
+
configured: boolean;
|
|
74
|
+
};
|
|
75
|
+
public_affirmed: {
|
|
76
|
+
affirmed: boolean;
|
|
77
|
+
origin: string | null;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export declare function summarize(state: SiteAccessState): SiteAccessSummary;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Site-access helpers — workspace-level credentials for gated test sites.
|
|
3
|
+
*
|
|
4
|
+
* Site-access is stored as product-level secrets with reserved keys. The
|
|
5
|
+
* backend enforces pair invariants (e.g. BASIC_AUTH_USERNAME and
|
|
6
|
+
* BASIC_AUTH_PASSWORD must be created together), so new keys must go through
|
|
7
|
+
* the batch endpoint while updates to existing keys go through PUT.
|
|
8
|
+
*
|
|
9
|
+
* Key constants are kept in sync with the frontend at
|
|
10
|
+
* /ish-frontend/src/lib/site-access/secret-keys.ts.
|
|
11
|
+
*/
|
|
12
|
+
export const SITE_ACCESS_KEYS = {
|
|
13
|
+
basicAuthUsername: "BASIC_AUTH_USERNAME",
|
|
14
|
+
basicAuthPassword: "BASIC_AUTH_PASSWORD",
|
|
15
|
+
basicAuthOrigin: "BASIC_AUTH_ORIGIN",
|
|
16
|
+
loginUsername: "LOGIN_USERNAME",
|
|
17
|
+
loginPassword: "LOGIN_PASSWORD",
|
|
18
|
+
sessionCookieName: "SESSION_COOKIE_NAME",
|
|
19
|
+
sessionCookieValue: "SESSION_COOKIE_VALUE",
|
|
20
|
+
sessionCookieOrigin: "SESSION_COOKIE_ORIGIN",
|
|
21
|
+
publicAffirmedOrigin: "SITE_ACCESS_PUBLIC_AFFIRMED_ORIGIN",
|
|
22
|
+
};
|
|
23
|
+
const ALL_SITE_ACCESS_KEYS = Object.values(SITE_ACCESS_KEYS);
|
|
24
|
+
/** Bare-origin form (`https://host[:port]`) used for credential binding. */
|
|
25
|
+
export function deriveOrigin(url) {
|
|
26
|
+
if (!url || !url.trim())
|
|
27
|
+
return null;
|
|
28
|
+
let candidate = url.trim();
|
|
29
|
+
if (!/^https?:\/\//i.test(candidate)) {
|
|
30
|
+
candidate = `https://${candidate}`;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const parsed = new URL(candidate);
|
|
34
|
+
if (!parsed.host)
|
|
35
|
+
return null;
|
|
36
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function loadSiteAccess(client, productId) {
|
|
43
|
+
const all = await client.get(`/products/${productId}/secrets`);
|
|
44
|
+
const byKey = new Map();
|
|
45
|
+
for (const s of all) {
|
|
46
|
+
if (ALL_SITE_ACCESS_KEYS.includes(s.key))
|
|
47
|
+
byKey.set(s.key, s);
|
|
48
|
+
}
|
|
49
|
+
const hasBasicAuth = byKey.has(SITE_ACCESS_KEYS.basicAuthUsername) &&
|
|
50
|
+
byKey.has(SITE_ACCESS_KEYS.basicAuthPassword);
|
|
51
|
+
const hasSessionCookie = byKey.has(SITE_ACCESS_KEYS.sessionCookieName) &&
|
|
52
|
+
byKey.has(SITE_ACCESS_KEYS.sessionCookieValue);
|
|
53
|
+
const hasLoginCreds = byKey.has(SITE_ACCESS_KEYS.loginUsername) &&
|
|
54
|
+
byKey.has(SITE_ACCESS_KEYS.loginPassword);
|
|
55
|
+
const publicAffirmed = byKey.get(SITE_ACCESS_KEYS.publicAffirmedOrigin);
|
|
56
|
+
const basicOrigin = byKey.get(SITE_ACCESS_KEYS.basicAuthOrigin);
|
|
57
|
+
const cookieOrigin = byKey.get(SITE_ACCESS_KEYS.sessionCookieOrigin);
|
|
58
|
+
return {
|
|
59
|
+
byKey,
|
|
60
|
+
hasBasicAuth,
|
|
61
|
+
hasSessionCookie,
|
|
62
|
+
hasLoginCreds,
|
|
63
|
+
publicAffirmedOrigin: publicAffirmed ? await revealValue(client, productId, publicAffirmed.id) : null,
|
|
64
|
+
basicAuthOrigin: basicOrigin ? await revealValue(client, productId, basicOrigin.id) : null,
|
|
65
|
+
sessionCookieOrigin: cookieOrigin ? await revealValue(client, productId, cookieOrigin.id) : null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Origin keys are stored as `variable_type: "variable"` (not secrets), so
|
|
70
|
+
* revealing them is safe — the frontend `SiteAccessDialog` does the same to
|
|
71
|
+
* pre-fill the URL field. We only reveal origin keys, never credentials.
|
|
72
|
+
*/
|
|
73
|
+
async function revealValue(client, productId, secretId) {
|
|
74
|
+
try {
|
|
75
|
+
const res = await client.get(`/products/${productId}/secrets/${secretId}/reveal`);
|
|
76
|
+
return res.value ?? null;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Apply a set of paired-key writes. Keys that don't yet exist go through the
|
|
84
|
+
* batch endpoint in one request — the backend's pair invariant is checked
|
|
85
|
+
* once against the projected union, not per-item where the first POST would
|
|
86
|
+
* always fail with "missing pair member". Existing keys are updated via PUT.
|
|
87
|
+
*
|
|
88
|
+
* Mirrors useSiteAccessSecrets.applySaveInstructions in the frontend.
|
|
89
|
+
*/
|
|
90
|
+
export async function applySiteAccessWrites(client, productId, state, items) {
|
|
91
|
+
const creates = [];
|
|
92
|
+
const updates = [];
|
|
93
|
+
for (const item of items) {
|
|
94
|
+
const existing = state.byKey.get(item.key);
|
|
95
|
+
if (existing) {
|
|
96
|
+
updates.push({ id: existing.id, value: item.value });
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
creates.push({
|
|
100
|
+
key: item.key,
|
|
101
|
+
value: item.value,
|
|
102
|
+
scope: item.scope,
|
|
103
|
+
variable_type: item.variable_type,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (creates.length > 0) {
|
|
108
|
+
await client.post(`/products/${productId}/secrets/batch`, { secrets: creates });
|
|
109
|
+
}
|
|
110
|
+
for (const u of updates) {
|
|
111
|
+
await client.put(`/products/${productId}/secrets/${u.id}`, { value: u.value });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export async function deleteSiteAccessKeys(client, productId, state, keys) {
|
|
115
|
+
const ids = [];
|
|
116
|
+
for (const key of keys) {
|
|
117
|
+
const existing = state.byKey.get(key);
|
|
118
|
+
if (!existing)
|
|
119
|
+
continue;
|
|
120
|
+
ids.push(existing.id);
|
|
121
|
+
}
|
|
122
|
+
if (ids.length === 0)
|
|
123
|
+
return 0;
|
|
124
|
+
await client.post(`/products/${productId}/secrets/batch-delete`, {
|
|
125
|
+
secret_ids: ids,
|
|
126
|
+
});
|
|
127
|
+
return ids.length;
|
|
128
|
+
}
|
|
129
|
+
export function keysForMethod(method) {
|
|
130
|
+
switch (method) {
|
|
131
|
+
case "basic-auth":
|
|
132
|
+
return [
|
|
133
|
+
SITE_ACCESS_KEYS.basicAuthUsername,
|
|
134
|
+
SITE_ACCESS_KEYS.basicAuthPassword,
|
|
135
|
+
SITE_ACCESS_KEYS.basicAuthOrigin,
|
|
136
|
+
];
|
|
137
|
+
case "cookie":
|
|
138
|
+
return [
|
|
139
|
+
SITE_ACCESS_KEYS.sessionCookieName,
|
|
140
|
+
SITE_ACCESS_KEYS.sessionCookieValue,
|
|
141
|
+
SITE_ACCESS_KEYS.sessionCookieOrigin,
|
|
142
|
+
];
|
|
143
|
+
case "login":
|
|
144
|
+
return [SITE_ACCESS_KEYS.loginUsername, SITE_ACCESS_KEYS.loginPassword];
|
|
145
|
+
case "public":
|
|
146
|
+
return [SITE_ACCESS_KEYS.publicAffirmedOrigin];
|
|
147
|
+
case "all":
|
|
148
|
+
return [
|
|
149
|
+
SITE_ACCESS_KEYS.basicAuthUsername,
|
|
150
|
+
SITE_ACCESS_KEYS.basicAuthPassword,
|
|
151
|
+
SITE_ACCESS_KEYS.basicAuthOrigin,
|
|
152
|
+
SITE_ACCESS_KEYS.sessionCookieName,
|
|
153
|
+
SITE_ACCESS_KEYS.sessionCookieValue,
|
|
154
|
+
SITE_ACCESS_KEYS.sessionCookieOrigin,
|
|
155
|
+
SITE_ACCESS_KEYS.loginUsername,
|
|
156
|
+
SITE_ACCESS_KEYS.loginPassword,
|
|
157
|
+
SITE_ACCESS_KEYS.publicAffirmedOrigin,
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Saving any credential clears the public-affirmed flag (matches frontend).
|
|
163
|
+
* Returns true if a flag was actually cleared, false if no flag was set.
|
|
164
|
+
*/
|
|
165
|
+
export async function clearPublicAffirmation(client, productId, state) {
|
|
166
|
+
const existing = state.byKey.get(SITE_ACCESS_KEYS.publicAffirmedOrigin);
|
|
167
|
+
if (!existing)
|
|
168
|
+
return false;
|
|
169
|
+
await client.del(`/products/${productId}/secrets/${existing.id}`);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
export function summarize(state) {
|
|
173
|
+
return {
|
|
174
|
+
basic_auth: {
|
|
175
|
+
configured: state.hasBasicAuth,
|
|
176
|
+
origin: state.basicAuthOrigin,
|
|
177
|
+
},
|
|
178
|
+
session_cookie: {
|
|
179
|
+
configured: state.hasSessionCookie,
|
|
180
|
+
origin: state.sessionCookieOrigin,
|
|
181
|
+
},
|
|
182
|
+
login: { configured: state.hasLoginCreds },
|
|
183
|
+
public_affirmed: {
|
|
184
|
+
affirmed: state.publicAffirmedOrigin !== null,
|
|
185
|
+
origin: state.publicAffirmedOrigin,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Skill content for the `ish` CLI.
|
|
3
|
+
*
|
|
4
|
+
* Shipped inline as TypeScript constants so the same content is available
|
|
5
|
+
* in both `tsc`-compiled npm installs and `bun build --compile` single
|
|
6
|
+
* binaries (which don't bundle markdown files outside of TS imports).
|
|
7
|
+
*
|
|
8
|
+
* Spec: https://agentskills.io/specification (April 2026).
|
|
9
|
+
*
|
|
10
|
+
* Distribution model: `ish init` materialises this content as a real
|
|
11
|
+
* directory tree at the path the user picks. Native consumers:
|
|
12
|
+
* - Claude Code .claude/skills/ish/
|
|
13
|
+
* - Codex / Cursor / .agents/skills/ish/
|
|
14
|
+
* Cline / Roo Code
|
|
15
|
+
*/
|
|
16
|
+
export interface SkillFile {
|
|
17
|
+
/** Path relative to the skill directory root. */
|
|
18
|
+
relativePath: string;
|
|
19
|
+
contents: string;
|
|
20
|
+
}
|
|
21
|
+
/** Returns every file that makes up the bundled skill, in stable order. */
|
|
22
|
+
export declare function buildSkillFiles(): SkillFile[];
|
|
23
|
+
/** Convenience: just the SKILL.md content (for `ish init --stdout`). */
|
|
24
|
+
export declare function getSkillMd(): string;
|
|
25
|
+
/** Where each agent product expects skills to live, relative to repo root. */
|
|
26
|
+
export interface SkillTargetSpec {
|
|
27
|
+
key: "claude" | "agents";
|
|
28
|
+
path: string;
|
|
29
|
+
consumers: string[];
|
|
30
|
+
}
|
|
31
|
+
export declare const SKILL_TARGETS: SkillTargetSpec[];
|