@ishlabs/cli 0.8.1 → 0.8.3

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 +2 -12
  47. package/dist/lib/local-sim/install.js +22 -30
  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 +3 -2
  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
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ish
2
2
 
3
- CLI tool to expose your localhost to [Ish](https://ishlabs.io) for simulation testing.
3
+ CLI tool for [Ish](https://ishlabs.io) — run studies, send asks, and expose your localhost for AI tester sessions.
4
4
 
5
5
  ## Install
6
6
 
@@ -29,41 +29,343 @@ brew tap ishlabs/tap
29
29
  brew install ish
30
30
  ```
31
31
 
32
- ## Usage
32
+ ## Authenticate
33
33
 
34
34
  ```bash
35
- ish connect <port>
35
+ ish login # browser-based auth, stores tokens in ~/.ish/config.json
36
+ ish logout # clear saved credentials
36
37
  ```
37
38
 
38
- ### Options
39
+ The CLI resolves your auth token in this order:
40
+ 1. `--token` CLI flag
41
+ 2. `ISH_TOKEN` env var
42
+ 3. Saved token from `ish login`
39
43
 
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 |
44
+ ## Concepts
45
45
 
46
- ### Token Configuration
46
+ Two top-level research primitives, both consume reusable tester profiles:
47
47
 
48
- The CLI resolves your auth token in this order:
48
+ ```
49
+ Workspace (= product, top-level container)
50
+
51
+ ├── Tester Profiles ← reusable audience personas
52
+ │ └── Audience Sources (transcripts / audio / images that seed generation)
53
+
54
+ ├── Study ─────────────── "structured research artifact"
55
+ │ ├── modality (interactive | text | video | audio | image | document)
56
+ │ ├── content-type (for non-interactive studies: email | social_post | ad | etc.)
57
+ │ ├── assignments (tasks the tester does)
58
+ │ ├── questionnaire (questions testers answer — text, slider, likert, choice)
59
+ │ └── Iterations ← the unit of execution within a study
60
+ │ └── Testers ← instance of a Profile inside this Iteration
61
+ │ └── Interactions / results
62
+
63
+ └── Ask ──────────────── "lightweight reaction artifact"
64
+ ├── Audience (testers, fixed at creation, max 5 rounds per ask)
65
+ └── Rounds ← the unit of execution within an ask
66
+ └── Responses (per-tester reactions to a variant)
67
+ ```
68
+
69
+ **The two primary run verbs:**
70
+
71
+ | | **`ish study run`** | **`ish ask run`** |
72
+ |---|---|---|
73
+ | Default | Run the latest iteration on the active study | Append a round to the active ask |
74
+ | Fresh setup | Create the iteration first: `ish iteration create …` | `--new` (creates ask + round 1 in one shot) |
75
+ | Specific target | `--iteration <id>` | Pass the ask id as positional arg |
76
+ | Audience | `--profile <ids>`, `--sample <N>`, `--all`, or demographic filters (`--country`, `--gender`, `--min-age`, `--max-age`, `--search`, `--visibility`) — else reuse iteration's testers | `--profile <ids>`, `--sample <N>`, `--all-simulatable`, or demographic filters (with `--new`) |
77
+
78
+ ## Commands
79
+
80
+ ### Auth & infra
81
+
82
+ ```bash
83
+ ish login # browser auth
84
+ ish logout
85
+ ish connect <port> # Cloudflare tunnel exposing localhost
86
+ ish upgrade # self-update (single-binary installs only)
87
+ ish upgrade --release 0.8.1 # pin a specific release
88
+ ```
89
+
90
+ `ish upgrade` only works on standalone-binary installs (curl/Homebrew). On npm-installed CLIs (`npm install -g @ishlabs/cli`) it refuses with a pointer to `npm install -g @ishlabs/cli@latest` — overwriting `node` would break every other Node tool.
91
+
92
+ ### Workspaces, studies, iterations, profiles, configs (CRUD groups)
93
+
94
+ ```bash
95
+ ish workspace list | create | get | update | delete | use
96
+ ish workspace site-access status | basic-auth | cookie | login | affirm-public | clear
97
+ ish study list | create | generate | get | results | update | delete | use
98
+ ish iteration list | create | get | update | delete
99
+ ish profile list | create | generate | get | update | delete
100
+ ish source upload | get
101
+ ish config list | create | get | schema | update | delete
102
+ ```
103
+
104
+ Testers live as a nested group on a study (low-level — usually created via `study run`):
105
+
106
+ ```bash
107
+ ish study tester <id> # show tester details and results
108
+ ish study tester create | batch-create | delete # low-level escape hatches
109
+ ```
110
+
111
+ `use` saves an active workspace/study/ask to `~/.ish/config.json` so you don't repeat `--workspace`/`--study`/`--ask` on every command. Aliases like `w-6ec`, `s-b2c`, `a-…`, `i-…`, `t-…` work anywhere an ID is expected.
112
+
113
+ ### Define a study — `ish study create`
114
+
115
+ A study locks in the persistent shape: modality, optional content-type taxonomy, the tasks testers do (`--assignment`), and the questionnaire they answer (`--question` or `--questionnaire`). The actual URL or file lives on an iteration (next step).
116
+
117
+ ```bash
118
+ # Interactive study, one assignment + a single-question questionnaire:
119
+ ish study create --name "Onboarding UX" --modality interactive \
120
+ --assignment "Sign up:Complete the signup flow" \
121
+ --question "How easy was it?"
122
+
123
+ # Multiple assignments + a richer questionnaire from a file:
124
+ ish study create --name "Checkout" --modality interactive \
125
+ --assignment "Browse:Find a product you like" \
126
+ --assignment "Buy:Add to cart and checkout" \
127
+ --questionnaire ./questionnaire.json
128
+
129
+ # Bulk assignments from a file (text/email study):
130
+ ish study create --name "Newsletter" --modality text --content-type email \
131
+ --assignments-file ./assignments.json
132
+ ```
133
+
134
+ `--question` adds a simple text question to the questionnaire (repeatable, defaults to type=text, timing=after). For richer types (slider, likert, single/multiple-choice, number) and custom timing, pass a JSON manifest via `--questionnaire <file.json>`. The two flags are mutually exclusive — pick one.
135
+
136
+ ### Configure a run — `ish iteration create`
137
+
138
+ Iterations carry the URL (interactive) or content (media) for a run. Create one before `ish study run`. Local files passed to `--content-url`, `--image-urls`, etc. are uploaded automatically; `@filepath` reads text from a file.
139
+
140
+ ```bash
141
+ # Interactive — URL:
142
+ ish iteration create --study s-b2c --url https://example.com
143
+
144
+ # Interactive on mobile:
145
+ ish iteration create --url https://example.com --screen-format mobile_portrait
146
+
147
+ # Text/email (inline or @file):
148
+ ish iteration create --content-text @./email.html --title "Newsletter"
149
+
150
+ # Video (URL or local file):
151
+ ish iteration create --content-url ./video.mp4
152
+
153
+ # Image set:
154
+ ish iteration create --image-urls "./a.png,./b.png"
155
+
156
+ # Document (PDF):
157
+ ish iteration create --content-url ./report.pdf
158
+
159
+ # Video ad with copy text:
160
+ ish iteration create --content-url ./ad.mp4 --copy-text "Buy now — 50% off!"
161
+
162
+ # Social post with caption:
163
+ ish iteration create --image-urls ./post.png \
164
+ --copy-text @./caption.txt --social-platform instagram
165
+ ```
166
+
167
+ ### Configure site access — `ish workspace site-access`
168
+
169
+ For studies that target a gated URL (HTTP basic auth, a session-cookie wall like Vercel preview protection, or a login form), set credentials on the workspace once. Testers reuse them whenever a study points at a matching origin. Credentials are encrypted at rest; the CLI never reads them back, only checks whether each method is configured.
170
+
171
+ ```bash
172
+ # Show what's configured:
173
+ ish workspace site-access status
174
+
175
+ # HTTP basic auth (e.g. staging gate):
176
+ ish workspace site-access basic-auth --username alice --password hunter2
177
+
178
+ # Session cookie (Vercel preview, Lovable, etc.):
179
+ ish workspace site-access cookie --name session --value abc123
180
+
181
+ # Login form — credentials the tester types into the page:
182
+ ish workspace site-access login --username demo --password demo
183
+
184
+ # Mark the site as public (silences the "credentials needed?" check):
185
+ ish workspace site-access affirm-public
186
+
187
+ # Clear one method, or everything:
188
+ ish workspace site-access clear cookie
189
+ ish workspace site-access clear all
190
+ ```
191
+
192
+ `--origin` defaults to the workspace `base_url` (set via `ish workspace update --base-url`). Pass `-` for `--password`/`--value` to read from stdin and keep secrets out of shell history:
193
+
194
+ ```bash
195
+ printf %s "$STAGING_PW" | ish workspace site-access basic-auth --username alice --password -
196
+ ```
49
197
 
50
- 1. `--token` CLI argument
51
- 2. `ISH_TOKEN` environment variable
52
- 3. Saved token from `ish login` (stored in `~/.ish/config.json`)
198
+ ### Run a study — `ish study run`
53
199
 
54
- ## Examples
200
+ Picks the latest iteration on the study (or `--iteration <id>`), creates testers (explicit `--profile`, demographic-filtered sample, or reusing the iteration's existing testers), and dispatches simulations. If the study has no iterations, run `ish iteration create` first.
55
201
 
56
202
  ```bash
57
- # Expose port 3000
58
- ish connect 3000
203
+ # Run the latest iteration, reusing its testers (after `study use`):
204
+ ish study run -y
59
205
 
60
- # With explicit token
61
- ish connect 3000 --token YOUR_TOKEN
206
+ # Run the latest iteration with an explicit audience:
207
+ ish study run --profile tp-795,tp-af2
62
208
 
63
- # Using environment variable
209
+ # Sample 3 Swedish profiles aged 35–50 from the workspace pool:
210
+ ish study run --country SE --min-age 35 --max-age 50 --sample 3
211
+
212
+ # Run with every female profile in the workspace:
213
+ ish study run --gender female --all
214
+
215
+ # Run a specific iteration:
216
+ ish study run --iteration i-d4e
217
+
218
+ # Override the simulation config (e.g. for a media study):
219
+ ish study run --config c-c3c
220
+
221
+ # Block until all simulations finish (or timeout):
222
+ ish study run --wait
223
+ ish study run --wait --timeout 600
224
+
225
+ # Local browser (no remote Browserbase):
226
+ ish study run --local --headed --slow-mo 500
227
+ ```
228
+
229
+ **Audience flags** (shared with `ish ask`): `--profile <ids>` for explicit IDs, or any of `--country`, `--gender`, `--min-age`, `--max-age`, `--search`, `--visibility` paired with `--sample <N>` or `--all` to seed from the workspace pool. Mutually exclusive with `--profile`.
230
+
231
+ **Other study verbs (low-level):**
232
+
233
+ ```bash
234
+ ish study poll --study <id> # one-shot progress
235
+ ish study poll <tester_id> # single tester status
236
+ ish study wait --study <id> # block until all done
237
+ ish study wait --iteration <id> # block on one iteration
238
+ ish study wait <tester_id> --timeout 600 # block on one tester
239
+ ish study cancel <tester_id> # cancel a running sim
240
+ ```
241
+
242
+ `<tester_id>` is a tester alias (`t-...`) or UUID. Get them from `ish study run --json`'s `tester_aliases[]` / `tester_ids[]` arrays:
243
+
244
+ ```bash
245
+ ish study run --study s-b2c -y --json | jq -r '.tester_aliases[]' # → t-072, t-1ed, ...
246
+ ```
247
+
248
+ ### Run an ask — `ish ask run`
249
+
250
+ Smart wrapper around the ask flow. With `--new`, creates a fresh ask + round 1 (audience from `--profile`, `--sample`, `--all-simulatable`, or any demographic filter — `--country`, `--gender`, `--min-age`, `--max-age`, `--search`, `--visibility`). Without `--new`, appends a new round to the active or specified ask.
251
+
252
+ ```bash
253
+ # Append a round to the active ask:
254
+ ish ask run --prompt "And now which?" \
255
+ --variant text:"X" --variant text:"Y" --wait
256
+
257
+ # Append to a specific ask:
258
+ ish ask run a-6ec --prompt "Round 2" \
259
+ --variant text:"A" --variant text:"B"
260
+
261
+ # Create a fresh ask with round 1, sample 30 profiles, wait for results:
262
+ ish ask run --new --name "tagline AB" \
263
+ --prompt "Which sounds better?" \
264
+ --variant text:"Short and punchy." \
265
+ --variant text:"A longer, descriptive line." \
266
+ --sample 30 --wants-pick --wait
267
+
268
+ # Demographic-filtered sample (Swedish profiles aged 35–50):
269
+ ish ask run --new --name "SE 35-50" \
270
+ --prompt "Which sounds better?" \
271
+ --variant text:"A" --variant text:"B" \
272
+ --country SE --min-age 35 --max-age 50 --sample 10 --wants-pick
273
+
274
+ # Image comparison from local files (auto-uploaded), explicit profiles:
275
+ ish ask run --new --name "hero shots" \
276
+ --prompt "Which feels premium?" \
277
+ --variant image:./hero-a.png::label=A \
278
+ --variant image:./hero-b.png::label=B \
279
+ --profile tp-d4e,tp-a17 --wants-ratings
280
+
281
+ # Text from a markdown file + JSON questionnaire:
282
+ ish ask run --new --name "newsletter" \
283
+ --prompt "Would you read this?" \
284
+ --variant text:@./body.md \
285
+ --questions ./questions.json --sample 30
286
+ ```
287
+
288
+ `--questions` takes a JSON array shaped `[{"question": "...", "type": "open_ended"|"slider"|"choice"|"likert"}]`. The server requires the key `question` (not `text`). Minimal example:
289
+
290
+ ```json
291
+ [
292
+ { "question": "What stood out?", "type": "open_ended" },
293
+ { "question": "Rate it 1-5", "type": "slider" }
294
+ ]
295
+ ```
296
+
297
+ **Audience flags** (`--profile`, `--sample`, `--all-simulatable`, `--country`, `--gender`, `--min-age`, `--max-age`, `--search`, `--visibility`, `--name`, `--description`, `--workspace`) only apply with `--new` — the audience is fixed at ask creation.
298
+
299
+ **Other ask verbs:**
300
+
301
+ ```bash
302
+ ish ask list [--archived]
303
+ ish ask get [id] [--round <n>]
304
+ ish ask results [id] [--round <n>]
305
+ ish ask wait [id] [--round <n>] [--timeout <s>]
306
+ ish ask add-round [id] --prompt … --variant … # explicit form of `run`
307
+ ish ask add-questions [id] --round <n> --questions ./qs.json
308
+ ish ask add-testers [id] --profile … # extend audience for one round
309
+ ish ask create … # explicit form of `run --new`
310
+ ish ask update | archive | unarchive | delete | use
311
+ ```
312
+
313
+ ### Generate tester profiles
314
+
315
+ `ish profile generate` runs the same audience-generation flow used in the web UI: an LLM reads your description and any uploaded sources (transcripts, customer records, audio interviews, screenshots) and returns either a single profile or a full audience.
316
+
317
+ ```bash
318
+ # 5 profiles from a written brief:
319
+ ish profile generate \
320
+ --description "Tech-savvy millennials in the US who use mobile banking" \
321
+ --count 5
322
+
323
+ # One profile from a transcript — the file is uploaded automatically:
324
+ ish profile generate --source ./interviews/sarah.txt --count 1
325
+
326
+ # Audio call with a written brief, diarized:
327
+ ish profile generate \
328
+ --description "Voices behind support tickets" \
329
+ --source ./call.mp3 --diarize --count 3
330
+ ```
331
+
332
+ For explicit control over uploads — e.g. reusing the same source across multiple `generate` runs — upload first and pass the returned alias:
333
+
334
+ ```bash
335
+ ish source upload ./call.mp3 --diarize
336
+ # → tps-3a4 (status: processed)
337
+
338
+ ish profile generate --source tps-3a4 --propose-count
339
+ # → { proposed_count: 4, rationale: "..." }
340
+
341
+ ish profile generate --source tps-3a4 --count 4
342
+ ```
343
+
344
+ ### Expose localhost
345
+
346
+ For interactive studies that need to reach a service running on your machine:
347
+
348
+ ```bash
349
+ ish connect 3000 # Cloudflare tunnel to localhost:3000
64
350
  ISH_TOKEN=YOUR_TOKEN ish connect 8080
65
351
  ```
66
352
 
353
+ `connect` is a long-running command — keep it open while testers run. The Cloudflare tunnel URL prints prominently after "Connected"; pass `--json` for one-line machine-readable output (`{"status":"connected","tunnel_url":"...","local_port":3000,"registered":true}`) suitable for scripts.
354
+
355
+ ## Global flags
356
+
357
+ | Flag | Description |
358
+ |------|-------------|
359
+ | `-t, --token <token>` | Auth token (or set `ISH_TOKEN` env var) |
360
+ | `--api-url <url>` | Backend API URL (default `https://api.ishlabs.io` or `ISH_API_URL`) |
361
+ | `--json` | Output JSON (auto-enabled when piped) |
362
+ | `--fields <a,b,c>` | Comma-separated fields to include in JSON output |
363
+ | `--verbose` | Include full UUIDs and timestamps in JSON output |
364
+ | `-q, --quiet` | Suppress progress messages on stderr |
365
+ | `-V, --version` | Show CLI version |
366
+
367
+ All commands print machine-readable JSON when stdout is piped or `--json` is passed, so AI agents can chain them together without parsing tables.
368
+
67
369
  ## License
68
370
 
69
- Copyright (c) 2025 Ish Labs. All rights reserved. See [LICENSE](LICENSE).
371
+ Copyright (c) 2026 Ish Labs. All rights reserved. See [LICENSE](LICENSE).
package/dist/auth.d.ts CHANGED
@@ -5,13 +5,29 @@
5
5
  export declare function getAppUrl(): string;
6
6
  export declare function getSupabaseUrl(): string;
7
7
  export declare function getSupabaseAnonKey(): string;
8
+ /**
9
+ * Resolve the Supabase project (URL + anon key) that issued the given JWT.
10
+ * Falls back to env vars / production defaults when the token is unparsable
11
+ * or its issuer isn't in our known list.
12
+ */
13
+ export declare function resolveSupabaseProjectFromToken(accessToken: string | undefined): {
14
+ url: string;
15
+ anonKey: string;
16
+ };
8
17
  export declare function decodeJwtExp(token: string): number;
9
18
  export declare function isTokenExpired(token: string, bufferSeconds?: number): boolean;
10
19
  export declare function login(appUrl?: string): Promise<{
11
20
  accessToken: string;
12
21
  refreshToken: string;
13
22
  }>;
14
- export declare function refreshTokens(refreshToken: string, supabaseUrl?: string, anonKey?: string): Promise<{
23
+ export declare function refreshTokens(refreshToken: string, options?: {
24
+ /** The (possibly expired) access token. Used to pick the correct Supabase project. */
25
+ accessToken?: string;
26
+ /** Force a specific Supabase project URL (e.g. for tests). */
27
+ supabaseUrl?: string;
28
+ /** Force a specific anon/publishable key. */
29
+ anonKey?: string;
30
+ }): Promise<{
15
31
  accessToken: string;
16
32
  refreshToken: string;
17
33
  }>;
package/dist/auth.js CHANGED
@@ -7,16 +7,68 @@ import { execFile } from "node:child_process";
7
7
  const POLL_INTERVAL = 2_000;
8
8
  const LOGIN_TIMEOUT = 5 * 60 * 1000; // 5 minutes (matches server-side token TTL)
9
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";
10
+ // Known Supabase projects, keyed by hostname. The CLI may be talking to either
11
+ // production or development depending on how the user logged in — the access
12
+ // token's `iss` claim tells us which project minted the refresh token, and we
13
+ // must send refresh requests back to that same project (with its matching
14
+ // publishable/anon key) or Supabase will reject them.
15
+ const SUPABASE_PROJECTS = {
16
+ // Production
17
+ "muqvgnqyubmqnfnqwxuk.supabase.co": {
18
+ url: "https://muqvgnqyubmqnfnqwxuk.supabase.co",
19
+ anonKey: "sb_publishable_pxXwY9EaWFwkR7h728NWvQ_NFqGfh8K",
20
+ },
21
+ // Development
22
+ "hngymyxdyamokpbeakps.supabase.co": {
23
+ url: "https://hngymyxdyamokpbeakps.supabase.co",
24
+ anonKey: "sb_publishable_JlS-HfwNyDqLNbrfbrkUlw_PSdZJdo2",
25
+ },
26
+ };
27
+ const DEFAULT_SUPABASE_PROJECT = SUPABASE_PROJECTS["muqvgnqyubmqnfnqwxuk.supabase.co"];
12
28
  export function getAppUrl() {
13
29
  return process.env.ISH_APP_URL ?? DEFAULT_APP_URL;
14
30
  }
15
31
  export function getSupabaseUrl() {
16
- return process.env.ISH_SUPABASE_URL ?? DEFAULT_SUPABASE_URL;
32
+ return process.env.ISH_SUPABASE_URL ?? DEFAULT_SUPABASE_PROJECT.url;
17
33
  }
18
34
  export function getSupabaseAnonKey() {
19
- return process.env.ISH_SUPABASE_ANON_KEY ?? DEFAULT_SUPABASE_ANON_KEY;
35
+ return process.env.ISH_SUPABASE_ANON_KEY ?? DEFAULT_SUPABASE_PROJECT.anonKey;
36
+ }
37
+ /**
38
+ * Resolve the Supabase project (URL + anon key) that issued the given JWT.
39
+ * Falls back to env vars / production defaults when the token is unparsable
40
+ * or its issuer isn't in our known list.
41
+ */
42
+ export function resolveSupabaseProjectFromToken(accessToken) {
43
+ const envUrl = process.env.ISH_SUPABASE_URL;
44
+ const envKey = process.env.ISH_SUPABASE_ANON_KEY;
45
+ if (envUrl && envKey)
46
+ return { url: envUrl, anonKey: envKey };
47
+ if (accessToken) {
48
+ try {
49
+ const payload = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url").toString());
50
+ const iss = payload.iss;
51
+ if (iss) {
52
+ const host = new URL(iss).host;
53
+ const project = SUPABASE_PROJECTS[host];
54
+ if (project)
55
+ return project;
56
+ // Unknown but well-formed issuer — trust it for the URL, use env or
57
+ // default key for apikey (best-effort; will likely fail without env override).
58
+ return {
59
+ url: `https://${host}`,
60
+ anonKey: envKey ?? DEFAULT_SUPABASE_PROJECT.anonKey,
61
+ };
62
+ }
63
+ }
64
+ catch {
65
+ // fall through
66
+ }
67
+ }
68
+ return {
69
+ url: envUrl ?? DEFAULT_SUPABASE_PROJECT.url,
70
+ anonKey: envKey ?? DEFAULT_SUPABASE_PROJECT.anonKey,
71
+ };
20
72
  }
21
73
  // --- Browser open ---
22
74
  function openBrowser(url) {
@@ -78,13 +130,14 @@ export async function login(appUrl) {
78
130
  throw new Error("Login timed out. Please try again.");
79
131
  }
80
132
  // --- 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`, {
133
+ export async function refreshTokens(refreshToken, options) {
134
+ const project = options?.supabaseUrl && options?.anonKey
135
+ ? { url: options.supabaseUrl, anonKey: options.anonKey }
136
+ : resolveSupabaseProjectFromToken(options?.accessToken);
137
+ const resp = await fetch(`${project.url}/auth/v1/token?grant_type=refresh_token`, {
85
138
  method: "POST",
86
139
  headers: {
87
- apikey: key,
140
+ apikey: project.anonKey,
88
141
  "Content-Type": "application/json",
89
142
  },
90
143
  body: JSON.stringify({ refresh_token: refreshToken }),
@@ -0,0 +1,5 @@
1
+ /**
2
+ * ish ask — Create and run asks (multi-round surveys with variants).
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerAskCommands(program: Command): void;