@ishlabs/cli 0.8.1

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 (57) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +69 -0
  3. package/dist/auth.d.ts +17 -0
  4. package/dist/auth.js +102 -0
  5. package/dist/commands/config.d.ts +5 -0
  6. package/dist/commands/config.js +82 -0
  7. package/dist/commands/iteration.d.ts +5 -0
  8. package/dist/commands/iteration.js +134 -0
  9. package/dist/commands/simulation.d.ts +10 -0
  10. package/dist/commands/simulation.js +647 -0
  11. package/dist/commands/study.d.ts +5 -0
  12. package/dist/commands/study.js +283 -0
  13. package/dist/commands/tester-profile.d.ts +5 -0
  14. package/dist/commands/tester-profile.js +109 -0
  15. package/dist/commands/tester.d.ts +5 -0
  16. package/dist/commands/tester.js +73 -0
  17. package/dist/commands/workspace.d.ts +5 -0
  18. package/dist/commands/workspace.js +133 -0
  19. package/dist/config.d.ts +13 -0
  20. package/dist/config.js +25 -0
  21. package/dist/connect.d.ts +4 -0
  22. package/dist/connect.js +573 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.js +89 -0
  25. package/dist/lib/alias-store.d.ts +49 -0
  26. package/dist/lib/alias-store.js +138 -0
  27. package/dist/lib/api-client.d.ts +58 -0
  28. package/dist/lib/api-client.js +177 -0
  29. package/dist/lib/auth.d.ts +8 -0
  30. package/dist/lib/auth.js +73 -0
  31. package/dist/lib/command-helpers.d.ts +28 -0
  32. package/dist/lib/command-helpers.js +131 -0
  33. package/dist/lib/local-sim/actions.d.ts +22 -0
  34. package/dist/lib/local-sim/actions.js +379 -0
  35. package/dist/lib/local-sim/browser.d.ts +63 -0
  36. package/dist/lib/local-sim/browser.js +332 -0
  37. package/dist/lib/local-sim/debug-report.d.ts +21 -0
  38. package/dist/lib/local-sim/debug-report.js +186 -0
  39. package/dist/lib/local-sim/debug.d.ts +44 -0
  40. package/dist/lib/local-sim/debug.js +103 -0
  41. package/dist/lib/local-sim/install.d.ts +25 -0
  42. package/dist/lib/local-sim/install.js +72 -0
  43. package/dist/lib/local-sim/loop.d.ts +60 -0
  44. package/dist/lib/local-sim/loop.js +526 -0
  45. package/dist/lib/local-sim/types.d.ts +232 -0
  46. package/dist/lib/local-sim/types.js +8 -0
  47. package/dist/lib/local-sim/upload.d.ts +6 -0
  48. package/dist/lib/local-sim/upload.js +24 -0
  49. package/dist/lib/output.d.ts +34 -0
  50. package/dist/lib/output.js +675 -0
  51. package/dist/lib/types.d.ts +179 -0
  52. package/dist/lib/types.js +12 -0
  53. package/dist/lib/upload.d.ts +47 -0
  54. package/dist/lib/upload.js +178 -0
  55. package/dist/upgrade.d.ts +1 -0
  56. package/dist/upgrade.js +94 -0
  57. package/package.json +43 -0
package/LICENSE ADDED
@@ -0,0 +1,6 @@
1
+ Copyright (c) 2025 Ish Labs. All rights reserved.
2
+
3
+ This software is the proprietary property of Ish Labs and is provided under the
4
+ terms of the Ish Labs Terms of Service (https://ishlabs.io/terms). Unauthorized
5
+ copying, modification, distribution, or use of this software, in whole or in
6
+ part, is strictly prohibited without prior written consent from Ish Labs.
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # ish
2
+
3
+ CLI tool to expose your localhost to [Ish](https://ishlabs.io) for simulation testing.
4
+
5
+ ## Install
6
+
7
+ ### Quick install (recommended)
8
+
9
+ **macOS / Linux:**
10
+ ```bash
11
+ curl -fsSL https://ishlabs.io/install.sh | sh
12
+ ```
13
+
14
+ **Windows (PowerShell):**
15
+ ```powershell
16
+ irm https://ishlabs.io/install.ps1 | iex
17
+ ```
18
+
19
+ ### npm (all platforms)
20
+
21
+ ```bash
22
+ npm install -g @ishlabs/cli
23
+ ```
24
+
25
+ ### Homebrew (macOS / Linux)
26
+
27
+ ```bash
28
+ brew tap ishlabs/tap
29
+ brew install ish
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```bash
35
+ ish connect <port>
36
+ ```
37
+
38
+ ### Options
39
+
40
+ | Flag | Description |
41
+ |------|-------------|
42
+ | `-t, --token <token>` | Auth token (or set `ISH_TOKEN` env var, or enter interactively) |
43
+ | `--api-url <url>` | Backend API URL (default: `https://api.ishlabs.io` or `ISH_API_URL` env var) |
44
+ | `--version` | Show version |
45
+
46
+ ### Token Configuration
47
+
48
+ The CLI resolves your auth token in this order:
49
+
50
+ 1. `--token` CLI argument
51
+ 2. `ISH_TOKEN` environment variable
52
+ 3. Saved token from `ish login` (stored in `~/.ish/config.json`)
53
+
54
+ ## Examples
55
+
56
+ ```bash
57
+ # Expose port 3000
58
+ ish connect 3000
59
+
60
+ # With explicit token
61
+ ish connect 3000 --token YOUR_TOKEN
62
+
63
+ # Using environment variable
64
+ ISH_TOKEN=YOUR_TOKEN ish connect 8080
65
+ ```
66
+
67
+ ## License
68
+
69
+ Copyright (c) 2025 Ish Labs. All rights reserved. See [LICENSE](LICENSE).
package/dist/auth.d.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Browser-based authentication via the Ish frontend plugin auth flow.
3
+ * Uses the existing /auth/plugin + /api/plugin/auth/poll infrastructure.
4
+ */
5
+ export declare function getAppUrl(): string;
6
+ export declare function getSupabaseUrl(): string;
7
+ export declare function getSupabaseAnonKey(): string;
8
+ export declare function decodeJwtExp(token: string): number;
9
+ export declare function isTokenExpired(token: string, bufferSeconds?: number): boolean;
10
+ export declare function login(appUrl?: string): Promise<{
11
+ accessToken: string;
12
+ refreshToken: string;
13
+ }>;
14
+ export declare function refreshTokens(refreshToken: string, supabaseUrl?: string, anonKey?: string): Promise<{
15
+ accessToken: string;
16
+ refreshToken: string;
17
+ }>;
package/dist/auth.js ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Browser-based authentication via the Ish frontend plugin auth flow.
3
+ * Uses the existing /auth/plugin + /api/plugin/auth/poll infrastructure.
4
+ */
5
+ import * as crypto from "node:crypto";
6
+ import { execFile } from "node:child_process";
7
+ const POLL_INTERVAL = 2_000;
8
+ const LOGIN_TIMEOUT = 5 * 60 * 1000; // 5 minutes (matches server-side token TTL)
9
+ const DEFAULT_APP_URL = "https://app.ishlabs.io";
10
+ const DEFAULT_SUPABASE_URL = "https://hngymyxdyamokpbeakps.supabase.co";
11
+ const DEFAULT_SUPABASE_ANON_KEY = "sb_publishable_JlS-HfwNyDqLNbrfbrkUlw_PSdZJdo2";
12
+ export function getAppUrl() {
13
+ return process.env.ISH_APP_URL ?? DEFAULT_APP_URL;
14
+ }
15
+ export function getSupabaseUrl() {
16
+ return process.env.ISH_SUPABASE_URL ?? DEFAULT_SUPABASE_URL;
17
+ }
18
+ export function getSupabaseAnonKey() {
19
+ return process.env.ISH_SUPABASE_ANON_KEY ?? DEFAULT_SUPABASE_ANON_KEY;
20
+ }
21
+ // --- Browser open ---
22
+ function openBrowser(url) {
23
+ if (process.platform === "win32") {
24
+ execFile("cmd", ["/c", "start", "", url]);
25
+ }
26
+ else if (process.platform === "darwin") {
27
+ execFile("open", [url]);
28
+ }
29
+ else {
30
+ execFile("xdg-open", [url]);
31
+ }
32
+ }
33
+ // --- JWT decode ---
34
+ export function decodeJwtExp(token) {
35
+ try {
36
+ const payload = token.split(".")[1];
37
+ const decoded = JSON.parse(Buffer.from(payload, "base64url").toString());
38
+ return decoded.exp;
39
+ }
40
+ catch {
41
+ return 0;
42
+ }
43
+ }
44
+ export function isTokenExpired(token, bufferSeconds = 300) {
45
+ const exp = decodeJwtExp(token);
46
+ if (!exp)
47
+ return true;
48
+ return Date.now() / 1000 >= exp - bufferSeconds;
49
+ }
50
+ // --- Login via browser polling ---
51
+ export async function login(appUrl) {
52
+ const url = appUrl ?? getAppUrl();
53
+ const state = crypto.randomBytes(32).toString("hex");
54
+ const loginUrl = `${url}/auth/plugin?state=${state}`;
55
+ console.log("Opening browser to sign in...");
56
+ console.log(`If the browser doesn't open, visit:\n ${loginUrl}\n`);
57
+ openBrowser(loginUrl);
58
+ console.log("Waiting for authentication...");
59
+ const deadline = Date.now() + LOGIN_TIMEOUT;
60
+ while (Date.now() < deadline) {
61
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL));
62
+ try {
63
+ const resp = await fetch(`${url}/api/plugin/auth/poll?state=${state}`, {
64
+ signal: AbortSignal.timeout(10_000),
65
+ });
66
+ if (resp.status === 200) {
67
+ const data = await resp.json();
68
+ if (data.status === "complete" && data.access_token && data.refresh_token) {
69
+ return { accessToken: data.access_token, refreshToken: data.refresh_token };
70
+ }
71
+ }
72
+ // 202 = pending, keep polling
73
+ }
74
+ catch {
75
+ // Network error, keep polling
76
+ }
77
+ }
78
+ throw new Error("Login timed out. Please try again.");
79
+ }
80
+ // --- Token refresh ---
81
+ export async function refreshTokens(refreshToken, supabaseUrl, anonKey) {
82
+ const url = supabaseUrl ?? getSupabaseUrl();
83
+ const key = anonKey ?? getSupabaseAnonKey();
84
+ const resp = await fetch(`${url}/auth/v1/token?grant_type=refresh_token`, {
85
+ method: "POST",
86
+ headers: {
87
+ apikey: key,
88
+ "Content-Type": "application/json",
89
+ },
90
+ body: JSON.stringify({ refresh_token: refreshToken }),
91
+ signal: AbortSignal.timeout(10_000),
92
+ });
93
+ if (!resp.ok) {
94
+ const body = await resp.text().catch(() => "");
95
+ throw new Error(`Token refresh failed (HTTP ${resp.status}): ${body}`);
96
+ }
97
+ const data = await resp.json();
98
+ if (!data.access_token || !data.refresh_token) {
99
+ throw new Error("Token refresh response missing required fields");
100
+ }
101
+ return { accessToken: data.access_token, refreshToken: data.refresh_token };
102
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * ish config — Manage simulation configs.
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerConfigCommands(program: Command): void;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * ish config — Manage simulation configs.
3
+ */
4
+ import { withClient, readJsonFileOrStdin } from "../lib/command-helpers.js";
5
+ import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
6
+ import { output, formatConfigList } from "../lib/output.js";
7
+ export function registerConfigCommands(program) {
8
+ const config = program
9
+ .command("config")
10
+ .description("Manage simulation configs");
11
+ config
12
+ .command("list")
13
+ .description("List all simulation configs")
14
+ .addHelpText("after", "\nExamples:\n $ ish config list\n $ ish config list --json")
15
+ .action(async (_opts, cmd) => {
16
+ await withClient(cmd, async (client, globals) => {
17
+ const data = await client.get("/dev/simulation-configs");
18
+ formatConfigList(data, globals.json);
19
+ });
20
+ });
21
+ config
22
+ .command("create")
23
+ .description("Create a simulation config")
24
+ .requiredOption("--file <path>", "JSON file with config data")
25
+ .action(async (opts, cmd) => {
26
+ await withClient(cmd, async (client, globals) => {
27
+ const body = await readJsonFileOrStdin(opts.file);
28
+ const data = await client.post("/dev/simulation-configs", body);
29
+ const result = data;
30
+ if (result.id)
31
+ result.alias = tagAlias(ALIAS_PREFIX.config, String(result.id));
32
+ output(result, globals.json);
33
+ });
34
+ });
35
+ config
36
+ .command("get")
37
+ .description("Get simulation config details")
38
+ .argument("<id>", "Config ID")
39
+ .action(async (id, _opts, cmd) => {
40
+ await withClient(cmd, async (client, globals) => {
41
+ const data = await client.get(`/dev/simulation-configs/${resolveId(id)}`);
42
+ const result = data;
43
+ if (result.id)
44
+ result.alias = tagAlias(ALIAS_PREFIX.config, String(result.id));
45
+ output(result, globals.json);
46
+ });
47
+ });
48
+ config
49
+ .command("schema")
50
+ .description("Get simulation config schema with defaults")
51
+ .action(async (_opts, cmd) => {
52
+ await withClient(cmd, async (client, globals) => {
53
+ const data = await client.get("/dev/simulation-configs/schema");
54
+ output(data, globals.json);
55
+ });
56
+ });
57
+ config
58
+ .command("update")
59
+ .description("Update a simulation config")
60
+ .argument("<id>", "Config ID")
61
+ .requiredOption("--file <path>", "JSON file with update data")
62
+ .action(async (id, opts, cmd) => {
63
+ await withClient(cmd, async (client, globals) => {
64
+ const body = await readJsonFileOrStdin(opts.file);
65
+ const data = await client.put(`/dev/simulation-configs/${resolveId(id)}`, body);
66
+ const result = data;
67
+ if (result.id)
68
+ result.alias = tagAlias(ALIAS_PREFIX.config, String(result.id));
69
+ output(result, globals.json);
70
+ });
71
+ });
72
+ config
73
+ .command("delete")
74
+ .description("Delete a simulation config")
75
+ .argument("<id>", "Config ID")
76
+ .action(async (id, _opts, cmd) => {
77
+ await withClient(cmd, async (client, globals) => {
78
+ await client.del(`/dev/simulation-configs/${resolveId(id)}`);
79
+ output({ message: "Config deleted" }, globals.json);
80
+ });
81
+ });
82
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * ish iteration — Manage iterations (usually created via `simulation run`).
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerIterationCommands(program: Command): void;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * ish iteration — Manage iterations (usually created via `simulation run`).
3
+ */
4
+ import { withClient, resolveStudy } from "../lib/command-helpers.js";
5
+ import { resolveId, tagAlias, ALIAS_PREFIX } from "../lib/alias-store.js";
6
+ import { output, formatIterationList } from "../lib/output.js";
7
+ export function registerIterationCommands(program) {
8
+ const iteration = program
9
+ .command("iteration")
10
+ .description("Manage iterations (usually created via `simulation run`)");
11
+ iteration
12
+ .command("list")
13
+ .description("List iterations for a study")
14
+ .option("--study <id>", "Study ID")
15
+ .addHelpText("after", "\nExamples:\n $ ish iteration list --study <id>\n $ ish iteration list --study <id> --json")
16
+ .action(async (opts, cmd) => {
17
+ await withClient(cmd, async (client, globals) => {
18
+ const data = await client.get(`/studies/${resolveStudy(opts.study)}/iterations`);
19
+ formatIterationList(data, globals.json);
20
+ });
21
+ });
22
+ iteration
23
+ .command("create")
24
+ .description("Create a new iteration (low-level)")
25
+ .option("--study <id>", "Study ID")
26
+ .requiredOption("--name <name>", "Iteration name")
27
+ .option("--description <description>", "Iteration description")
28
+ .option("--details-json <json>", "Iteration details as JSON string")
29
+ .addHelpText("after", `
30
+ Examples:
31
+ # Interactive:
32
+ $ ish iteration create --study S --name "v1" \\
33
+ --details-json '{"type":"interactive","platform":"browser","url":"https://example.com","screen_format":"desktop"}'
34
+
35
+ # Text/email:
36
+ $ ish iteration create --study S --name "v1" \\
37
+ --details-json '{"type":"text","content_text":"Your email content here","title":"Newsletter"}'
38
+
39
+ # Video:
40
+ $ ish iteration create --study S --name "v1" \\
41
+ --details-json '{"type":"video","content_url":"https://cdn.example.com/video.mp4","mime_type":"video/mp4"}'
42
+
43
+ # Image:
44
+ $ ish iteration create --study S --name "v1" \\
45
+ --details-json '{"type":"image","image_urls":["https://cdn.example.com/a.png","https://cdn.example.com/b.png"]}'
46
+
47
+ # Document (PDF):
48
+ $ ish iteration create --study S --name "v1" \\
49
+ --details-json '{"type":"document","content_url":"https://cdn.example.com/report.pdf","mime_type":"application/pdf"}'
50
+
51
+ Note: For local file uploads, use \`ish simulation run\` which automatically
52
+ uploads files and resolves URLs (e.g. --content-url ./video.mp4).`)
53
+ .action(async (opts, cmd) => {
54
+ await withClient(cmd, async (client, globals) => {
55
+ const body = {
56
+ name: opts.name,
57
+ ...(opts.description !== undefined && { description: opts.description }),
58
+ ...(opts.detailsJson && { details: (() => { try {
59
+ return JSON.parse(opts.detailsJson);
60
+ }
61
+ catch {
62
+ throw new Error("Invalid --details-json: expected valid JSON string");
63
+ } })() }),
64
+ };
65
+ const data = await client.post(`/studies/${resolveStudy(opts.study)}/iterations`, body);
66
+ const result = data;
67
+ if (result.id)
68
+ result.alias = tagAlias(ALIAS_PREFIX.iteration, String(result.id));
69
+ output(result, globals.json);
70
+ });
71
+ });
72
+ iteration
73
+ .command("get")
74
+ .description("Get iteration details")
75
+ .argument("<id>", "Iteration ID")
76
+ .addHelpText("after", "\nExamples:\n $ ish iteration get <id>\n $ ish iteration get <id> --json")
77
+ .action(async (id, _opts, cmd) => {
78
+ await withClient(cmd, async (client, globals) => {
79
+ const data = await client.get(`/iterations/${resolveId(id)}`);
80
+ const result = data;
81
+ if (result.id)
82
+ result.alias = tagAlias(ALIAS_PREFIX.iteration, String(result.id));
83
+ output(result, globals.json);
84
+ });
85
+ });
86
+ iteration
87
+ .command("update")
88
+ .description("Update an iteration")
89
+ .argument("<id>", "Iteration ID")
90
+ .option("--name <name>", "Iteration name")
91
+ .option("--description <description>", "Iteration description")
92
+ .option("--details-json <json>", "Iteration details as JSON string")
93
+ .option("--label <label>", "Iteration label (uppercase letters)")
94
+ .addHelpText("after", "\nExamples:\n $ ish iteration update <id> --name \"v2\"\n $ ish iteration update <id> --label B --json")
95
+ .action(async (id, opts, cmd) => {
96
+ await withClient(cmd, async (client, globals) => {
97
+ const body = {};
98
+ if (opts.name !== undefined)
99
+ body.name = opts.name;
100
+ if (opts.description !== undefined)
101
+ body.description = opts.description;
102
+ if (opts.detailsJson !== undefined) {
103
+ try {
104
+ body.details = JSON.parse(opts.detailsJson);
105
+ }
106
+ catch {
107
+ throw new Error("Invalid --details-json: expected valid JSON string");
108
+ }
109
+ }
110
+ if (opts.label !== undefined)
111
+ body.label = opts.label;
112
+ if (Object.keys(body).length === 0) {
113
+ console.error("No update flags provided. Run `ish iteration update --help` for options.");
114
+ return;
115
+ }
116
+ const data = await client.put(`/iterations/${resolveId(id)}`, body);
117
+ const result = data;
118
+ if (result.id)
119
+ result.alias = tagAlias(ALIAS_PREFIX.iteration, String(result.id));
120
+ output(result, globals.json);
121
+ });
122
+ });
123
+ iteration
124
+ .command("delete")
125
+ .description("Delete an iteration")
126
+ .argument("<id>", "Iteration ID")
127
+ .addHelpText("after", "\nExamples:\n $ ish iteration delete <id>")
128
+ .action(async (id, _opts, cmd) => {
129
+ await withClient(cmd, async (client, globals) => {
130
+ await client.del(`/iterations/${resolveId(id)}`);
131
+ output({ message: "Iteration deleted" }, globals.json);
132
+ });
133
+ });
134
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ish simulation — Run, monitor, and cancel simulations.
3
+ *
4
+ * Primary command: `ish simulation run` — orchestrates the full flow:
5
+ * 1. Creates iteration (if not provided)
6
+ * 2. Creates testers from profiles
7
+ * 3. Starts simulations (interactive or media, based on study modality)
8
+ */
9
+ import type { Command } from "commander";
10
+ export declare function registerSimulationCommands(program: Command): void;