@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.
- 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 +2 -12
- package/dist/lib/local-sim/install.js +22 -30
- 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 +3 -2
- 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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ish
|
|
2
2
|
|
|
3
|
-
CLI tool
|
|
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
|
-
##
|
|
32
|
+
## Authenticate
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
ish
|
|
35
|
+
ish login # browser-based auth, stores tokens in ~/.ish/config.json
|
|
36
|
+
ish logout # clear saved credentials
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
46
|
+
Two top-level research primitives, both consume reusable tester profiles:
|
|
47
47
|
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
58
|
-
ish
|
|
203
|
+
# Run the latest iteration, reusing its testers (after `study use`):
|
|
204
|
+
ish study run -y
|
|
59
205
|
|
|
60
|
-
#
|
|
61
|
-
ish
|
|
206
|
+
# Run the latest iteration with an explicit audience:
|
|
207
|
+
ish study run --profile tp-795,tp-af2
|
|
62
208
|
|
|
63
|
-
#
|
|
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)
|
|
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,
|
|
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
|
-
|
|
11
|
-
|
|
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 ??
|
|
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 ??
|
|
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,
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
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:
|
|
140
|
+
apikey: project.anonKey,
|
|
88
141
|
"Content-Type": "application/json",
|
|
89
142
|
},
|
|
90
143
|
body: JSON.stringify({ refresh_token: refreshToken }),
|