@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.
Files changed (70) hide show
  1. package/README.md +323 -21
  2. package/dist/auth.d.ts +17 -1
  3. package/dist/auth.js +62 -9
  4. package/dist/commands/ask.d.ts +5 -0
  5. package/dist/commands/ask.js +722 -0
  6. package/dist/commands/config.js +25 -1
  7. package/dist/commands/docs.d.ts +17 -0
  8. package/dist/commands/docs.js +147 -0
  9. package/dist/commands/init.d.ts +16 -0
  10. package/dist/commands/init.js +182 -0
  11. package/dist/commands/iteration.d.ts +5 -1
  12. package/dist/commands/iteration.js +243 -31
  13. package/dist/commands/profile.d.ts +5 -0
  14. package/dist/commands/profile.js +313 -0
  15. package/dist/commands/source.d.ts +10 -0
  16. package/dist/commands/source.js +78 -0
  17. package/dist/commands/study-run.d.ts +11 -0
  18. package/dist/commands/study-run.js +552 -0
  19. package/dist/commands/study-tester.d.ts +8 -0
  20. package/dist/commands/study-tester.js +149 -0
  21. package/dist/commands/study.js +145 -70
  22. package/dist/commands/workspace.js +193 -7
  23. package/dist/config.d.ts +3 -1
  24. package/dist/config.js +10 -10
  25. package/dist/connect.d.ts +4 -1
  26. package/dist/connect.js +127 -94
  27. package/dist/index.js +82 -34
  28. package/dist/lib/alias-store.d.ts +3 -0
  29. package/dist/lib/alias-store.js +9 -7
  30. package/dist/lib/api-client.d.ts +9 -6
  31. package/dist/lib/api-client.js +87 -26
  32. package/dist/lib/ask-questions.d.ts +9 -0
  33. package/dist/lib/ask-questions.js +35 -0
  34. package/dist/lib/ask-variants.d.ts +48 -0
  35. package/dist/lib/ask-variants.js +236 -0
  36. package/dist/lib/auth.d.ts +1 -1
  37. package/dist/lib/auth.js +24 -8
  38. package/dist/lib/colors.d.ts +30 -0
  39. package/dist/lib/colors.js +48 -0
  40. package/dist/lib/command-helpers.d.ts +74 -0
  41. package/dist/lib/command-helpers.js +232 -6
  42. package/dist/lib/docs.d.ts +32 -0
  43. package/dist/lib/docs.js +930 -0
  44. package/dist/lib/local-sim/browser.d.ts +0 -1
  45. package/dist/lib/local-sim/browser.js +0 -2
  46. package/dist/lib/local-sim/install.d.ts +4 -7
  47. package/dist/lib/local-sim/install.js +6 -21
  48. package/dist/lib/output.d.ts +25 -3
  49. package/dist/lib/output.js +465 -20
  50. package/dist/lib/paths.d.ts +14 -0
  51. package/dist/lib/paths.js +36 -0
  52. package/dist/lib/profile-sources.d.ts +55 -0
  53. package/dist/lib/profile-sources.js +157 -0
  54. package/dist/lib/site-access.d.ts +80 -0
  55. package/dist/lib/site-access.js +188 -0
  56. package/dist/lib/skill-content.d.ts +31 -0
  57. package/dist/lib/skill-content.js +462 -0
  58. package/dist/lib/study-inputs.d.ts +20 -0
  59. package/dist/lib/study-inputs.js +72 -0
  60. package/dist/lib/types.d.ts +207 -9
  61. package/dist/lib/types.js +7 -0
  62. package/dist/lib/upload.js +2 -2
  63. package/dist/upgrade.js +11 -1
  64. package/package.json +1 -1
  65. package/dist/commands/simulation.d.ts +0 -10
  66. package/dist/commands/simulation.js +0 -647
  67. package/dist/commands/tester-profile.d.ts +0 -5
  68. package/dist/commands/tester-profile.js +0 -109
  69. package/dist/commands/tester.d.ts +0 -5
  70. package/dist/commands/tester.js +0 -73
@@ -1,14 +1,22 @@
1
1
  /**
2
2
  * ish workspace — Manage workspaces (API: /products).
3
3
  */
4
- import { withClient, getWebUrl, terminalLink } from "../lib/command-helpers.js";
4
+ import { withClient, getWebUrl, terminalLink, resolveWorkspace } from "../lib/command-helpers.js";
5
5
  import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
6
6
  import { loadConfig, saveConfig } from "../config.js";
7
- import { formatWorkspaceList, formatWorkspaceDetail, output } from "../lib/output.js";
7
+ import { formatWorkspaceList, formatWorkspaceDetail, formatSiteAccessStatus, output } from "../lib/output.js";
8
+ import { SITE_ACCESS_KEYS, applySiteAccessWrites, clearPublicAffirmation, deleteSiteAccessKeys, deriveOrigin, keysForMethod, loadSiteAccess, summarize, } from "../lib/site-access.js";
8
9
  export function registerWorkspaceCommands(program) {
9
10
  const workspace = program
10
11
  .command("workspace")
11
- .description("Manage workspaces");
12
+ .description("Manage workspaces")
13
+ .addHelpText("after", `
14
+ A workspace is the top-level container — one per product. Studies, asks, profiles, sources,
15
+ and configs all belong to exactly one workspace. \`ish workspace site-access\` configures
16
+ credentials for gated study URLs (basic auth, session cookie, or login form).
17
+
18
+ Concept pages: ish docs get-page concepts/workspace
19
+ ish docs get-page concepts/site-access`);
12
20
  workspace
13
21
  .command("list")
14
22
  .description("List all workspaces")
@@ -37,7 +45,7 @@ export function registerWorkspaceCommands(program) {
37
45
  const result = data;
38
46
  if (result.id)
39
47
  result.alias = tagAlias(ALIAS_PREFIX.workspace, String(result.id));
40
- formatWorkspaceDetail(result, globals.json);
48
+ formatWorkspaceDetail(result, globals.json, { writePath: true });
41
49
  if (!globals.json && data.id) {
42
50
  const url = getWebUrl(globals, `/${data.id}`);
43
51
  console.error(`\n ${terminalLink(url, "Open in browser ↗")}\n`);
@@ -88,7 +96,7 @@ export function registerWorkspaceCommands(program) {
88
96
  const result = data;
89
97
  if (result.id)
90
98
  result.alias = tagAlias(ALIAS_PREFIX.workspace, String(result.id));
91
- formatWorkspaceDetail(result, globals.json);
99
+ formatWorkspaceDetail(result, globals.json, { writePath: true });
92
100
  });
93
101
  });
94
102
  workspace
@@ -98,10 +106,12 @@ export function registerWorkspaceCommands(program) {
98
106
  .addHelpText("after", "\nExamples:\n $ ish workspace delete <id>")
99
107
  .action(async (id, _opts, cmd) => {
100
108
  await withClient(cmd, async (client, globals) => {
101
- await client.del(`/products/${resolveId(id)}`);
102
- output({ message: "Workspace deleted" }, globals.json);
109
+ const rid = resolveId(id);
110
+ await client.del(`/products/${rid}`);
111
+ output({ id: rid, alias: tagAlias(ALIAS_PREFIX.workspace, rid), message: "Workspace deleted" }, globals.json, { writePath: true });
103
112
  });
104
113
  });
114
+ registerSiteAccessCommands(workspace);
105
115
  workspace
106
116
  .command("use")
107
117
  .description("Set the active workspace (saved to ~/.ish/config.json)")
@@ -131,3 +141,179 @@ export function registerWorkspaceCommands(program) {
131
141
  });
132
142
  });
133
143
  }
144
+ // ---------------------------------------------------------------------------
145
+ // site-access — workspace-level credentials for gated test sites.
146
+ // Stored as product secrets with reserved keys (see src/lib/site-access.ts).
147
+ // ---------------------------------------------------------------------------
148
+ /** Read a flag value, treating "-" as "read from stdin" (whitespace stripped). */
149
+ async function readSecretFlag(raw) {
150
+ if (raw !== "-")
151
+ return raw;
152
+ if (process.stdin.isTTY) {
153
+ throw new Error('Use "-" only when piping the value on stdin.');
154
+ }
155
+ return await new Promise((resolve, reject) => {
156
+ let data = "";
157
+ process.stdin.setEncoding("utf-8");
158
+ process.stdin.on("data", (chunk) => { data += chunk; });
159
+ process.stdin.on("end", () => resolve(data.replace(/\r?\n$/, "")));
160
+ process.stdin.on("error", reject);
161
+ });
162
+ }
163
+ async function resolveOriginForWriting(client, workspaceId, explicit) {
164
+ if (explicit) {
165
+ const derived = deriveOrigin(explicit);
166
+ if (!derived) {
167
+ throw new Error(`Invalid --origin: "${explicit}". Provide a URL like https://example.com.`);
168
+ }
169
+ return derived;
170
+ }
171
+ const product = await client.get(`/products/${workspaceId}`);
172
+ const derived = deriveOrigin(product.base_url);
173
+ if (!derived) {
174
+ throw new Error('No --origin provided and the workspace has no base_url set. ' +
175
+ 'Run `ish workspace update <id> --base-url https://your-site.com` first, or pass --origin explicitly.');
176
+ }
177
+ return derived;
178
+ }
179
+ function registerSiteAccessCommands(workspace) {
180
+ const sa = workspace
181
+ .command("site-access")
182
+ .description("Manage credentials for gated test sites (basic auth, session cookie, login form)");
183
+ sa
184
+ .command("status")
185
+ .description("Show which site-access methods are configured")
186
+ .option("--workspace <id>", "Workspace ID; defaults to active workspace")
187
+ .addHelpText("after", "\nExamples:\n $ ish workspace site-access status\n $ ish workspace site-access status --json")
188
+ .action(async (opts, cmd) => {
189
+ await withClient(cmd, async (client, globals) => {
190
+ const wid = resolveWorkspace(opts.workspace);
191
+ const state = await loadSiteAccess(client, wid);
192
+ formatSiteAccessStatus(summarize(state), globals.json);
193
+ });
194
+ });
195
+ sa
196
+ .command("basic-auth")
197
+ .description("Set HTTP basic auth credentials")
198
+ .requiredOption("--username <u>", 'Username (or "-" to read from stdin)')
199
+ .requiredOption("--password <p>", 'Password (or "-" to read from stdin)')
200
+ .option("--origin <url>", "Origin to bind credentials to; defaults to workspace base_url")
201
+ .option("--workspace <id>", "Workspace ID; defaults to active workspace")
202
+ .addHelpText("after", '\nExamples:\n $ ish workspace site-access basic-auth --username alice --password hunter2\n $ printf %s "$PW" | ish workspace site-access basic-auth --username alice --password -\n\nRequires either --origin or a workspace base_url (set with `ish workspace update --base-url ...`).')
203
+ .action(async (opts, cmd) => {
204
+ await withClient(cmd, async (client, globals) => {
205
+ const wid = resolveWorkspace(opts.workspace);
206
+ const username = await readSecretFlag(opts.username);
207
+ const password = await readSecretFlag(opts.password);
208
+ const origin = await resolveOriginForWriting(client, wid, opts.origin);
209
+ const state = await loadSiteAccess(client, wid);
210
+ await applySiteAccessWrites(client, wid, state, [
211
+ { key: SITE_ACCESS_KEYS.basicAuthUsername, value: username, scope: "project", variable_type: "secret" },
212
+ { key: SITE_ACCESS_KEYS.basicAuthPassword, value: password, scope: "project", variable_type: "secret" },
213
+ { key: SITE_ACCESS_KEYS.basicAuthOrigin, value: origin, scope: "project", variable_type: "variable" },
214
+ ]);
215
+ const clearedAffirmation = await clearPublicAffirmation(client, wid, state);
216
+ output({
217
+ message: "Basic auth credentials saved",
218
+ origin,
219
+ cleared_public_affirmation: clearedAffirmation,
220
+ }, globals.json);
221
+ });
222
+ });
223
+ sa
224
+ .command("cookie")
225
+ .description("Set a session cookie for sites that gate on a token (Vercel preview, Lovable, etc.)")
226
+ .requiredOption("--name <n>", "Cookie name")
227
+ .requiredOption("--value <v>", 'Cookie value (or "-" to read from stdin)')
228
+ .option("--origin <url>", "Origin to bind cookie to; defaults to workspace base_url")
229
+ .option("--workspace <id>", "Workspace ID; defaults to active workspace")
230
+ .addHelpText("after", "\nExamples:\n $ ish workspace site-access cookie --name session --value abc123\n\nRequires either --origin or a workspace base_url (set with `ish workspace update --base-url ...`).")
231
+ .action(async (opts, cmd) => {
232
+ await withClient(cmd, async (client, globals) => {
233
+ const wid = resolveWorkspace(opts.workspace);
234
+ const value = await readSecretFlag(opts.value);
235
+ const origin = await resolveOriginForWriting(client, wid, opts.origin);
236
+ const state = await loadSiteAccess(client, wid);
237
+ await applySiteAccessWrites(client, wid, state, [
238
+ { key: SITE_ACCESS_KEYS.sessionCookieName, value: opts.name, scope: "project", variable_type: "variable" },
239
+ { key: SITE_ACCESS_KEYS.sessionCookieValue, value, scope: "project", variable_type: "secret" },
240
+ { key: SITE_ACCESS_KEYS.sessionCookieOrigin, value: origin, scope: "project", variable_type: "variable" },
241
+ ]);
242
+ const clearedAffirmation = await clearPublicAffirmation(client, wid, state);
243
+ output({
244
+ message: "Session cookie saved",
245
+ origin,
246
+ cleared_public_affirmation: clearedAffirmation,
247
+ }, globals.json);
248
+ });
249
+ });
250
+ sa
251
+ .command("login")
252
+ .description("Set login form credentials a tester will type into the site")
253
+ .requiredOption("--username <u>", 'Username (or "-" to read from stdin)')
254
+ .requiredOption("--password <p>", 'Password (or "-" to read from stdin)')
255
+ .option("--workspace <id>", "Workspace ID; defaults to active workspace")
256
+ .addHelpText("after", "\nExamples:\n $ ish workspace site-access login --username demo --password demo")
257
+ .action(async (opts, cmd) => {
258
+ await withClient(cmd, async (client, globals) => {
259
+ const wid = resolveWorkspace(opts.workspace);
260
+ const username = await readSecretFlag(opts.username);
261
+ const password = await readSecretFlag(opts.password);
262
+ const state = await loadSiteAccess(client, wid);
263
+ await applySiteAccessWrites(client, wid, state, [
264
+ { key: SITE_ACCESS_KEYS.loginUsername, value: username, scope: "agent", variable_type: "secret" },
265
+ { key: SITE_ACCESS_KEYS.loginPassword, value: password, scope: "agent", variable_type: "secret" },
266
+ ]);
267
+ const clearedAffirmation = await clearPublicAffirmation(client, wid, state);
268
+ output({
269
+ message: "Login credentials saved",
270
+ cleared_public_affirmation: clearedAffirmation,
271
+ }, globals.json);
272
+ });
273
+ });
274
+ sa
275
+ .command("affirm-public")
276
+ .description("Mark the site as not requiring credentials")
277
+ .option("--origin <url>", "Origin to mark public; defaults to workspace base_url")
278
+ .option("--workspace <id>", "Workspace ID; defaults to active workspace")
279
+ .addHelpText("after", "\nExamples:\n $ ish workspace site-access affirm-public\n\nRequires either --origin or a workspace base_url (set with `ish workspace update --base-url ...`).")
280
+ .action(async (opts, cmd) => {
281
+ await withClient(cmd, async (client, globals) => {
282
+ const wid = resolveWorkspace(opts.workspace);
283
+ const origin = await resolveOriginForWriting(client, wid, opts.origin);
284
+ const state = await loadSiteAccess(client, wid);
285
+ await applySiteAccessWrites(client, wid, state, [
286
+ { key: SITE_ACCESS_KEYS.publicAffirmedOrigin, value: origin, scope: "project", variable_type: "variable" },
287
+ ]);
288
+ output({ message: "Site marked public", origin }, globals.json);
289
+ });
290
+ });
291
+ sa
292
+ .command("clear")
293
+ .description("Clear configured site-access credentials")
294
+ .argument("<method>", "basic-auth | cookie | login | public | all")
295
+ .option("--workspace <id>", "Workspace ID; defaults to active workspace")
296
+ .addHelpText("after", "\nExamples:\n $ ish workspace site-access clear cookie\n $ ish workspace site-access clear all")
297
+ .action(async (method, opts, cmd) => {
298
+ const valid = ["basic-auth", "cookie", "login", "public", "all"];
299
+ if (!valid.includes(method)) {
300
+ throw new Error(`Invalid method "${method}". Use one of: ${valid.join(", ")}.`);
301
+ }
302
+ await withClient(cmd, async (client, globals) => {
303
+ const wid = resolveWorkspace(opts.workspace);
304
+ const state = await loadSiteAccess(client, wid);
305
+ const keys = keysForMethod(method);
306
+ const deleted = await deleteSiteAccessKeys(client, wid, state, keys);
307
+ if (deleted === 0) {
308
+ output({ deleted: 0, message: "Nothing to clear", method }, globals.json);
309
+ }
310
+ else {
311
+ output({
312
+ deleted,
313
+ message: `Cleared ${deleted} secret${deleted === 1 ? "" : "s"}`,
314
+ method,
315
+ }, globals.json);
316
+ }
317
+ });
318
+ });
319
+ }
package/dist/config.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Config persistence for ~/.ish/config.json
2
+ * Config persistence. Default state dir is ~/.ish; override via the
3
+ * ISH_HOME env var (see src/lib/paths.ts).
3
4
  */
4
5
  export interface IshConfig {
5
6
  access_token?: string;
@@ -7,6 +8,7 @@ export interface IshConfig {
7
8
  token?: string;
8
9
  workspace?: string;
9
10
  study?: string;
11
+ ask?: string;
10
12
  [key: string]: string | undefined;
11
13
  }
12
14
  export declare function loadConfig(): IshConfig;
package/dist/config.js CHANGED
@@ -1,15 +1,14 @@
1
1
  /**
2
- * Config persistence for ~/.ish/config.json
2
+ * Config persistence. Default state dir is ~/.ish; override via the
3
+ * ISH_HOME env var (see src/lib/paths.ts).
3
4
  */
4
5
  import * as fs from "node:fs";
5
- import * as path from "node:path";
6
- import * as os from "node:os";
7
- const CONFIG_DIR = path.join(os.homedir(), ".ish");
8
- const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
6
+ import { ishDir, configPath } from "./lib/paths.js";
9
7
  export function loadConfig() {
10
8
  try {
11
- if (fs.existsSync(CONFIG_FILE)) {
12
- return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
9
+ const file = configPath();
10
+ if (fs.existsSync(file)) {
11
+ return JSON.parse(fs.readFileSync(file, "utf-8"));
13
12
  }
14
13
  }
15
14
  catch {
@@ -18,8 +17,9 @@ export function loadConfig() {
18
17
  return {};
19
18
  }
20
19
  export function saveConfig(config) {
21
- fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
22
- const tmp = CONFIG_FILE + ".tmp";
20
+ fs.mkdirSync(ishDir(), { recursive: true, mode: 0o700 });
21
+ const file = configPath();
22
+ const tmp = file + ".tmp";
23
23
  fs.writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
24
- fs.renameSync(tmp, CONFIG_FILE);
24
+ fs.renameSync(tmp, file);
25
25
  }
package/dist/connect.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  /**
2
2
  * Localhost connect CLI — wraps cloudflared and registers with Ish backend.
3
3
  */
4
- export declare function runTunnel(port: number, tokenArg?: string, apiUrlArg?: string): Promise<void>;
4
+ export declare function runTunnel(port: number, tokenArg?: string, apiUrlArg?: string, tokenFileArg?: string, outputOpts?: {
5
+ json?: boolean;
6
+ quiet?: boolean;
7
+ }): Promise<void>;