@ishlabs/cli 0.19.0 → 0.20.0
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/dist/commands/ask.js +26 -2
- package/dist/commands/config.js +9 -1
- package/dist/commands/docs.js +6 -7
- package/dist/commands/person.js +123 -9
- package/dist/commands/secret.js +25 -2
- package/dist/commands/source.d.ts +1 -1
- package/dist/commands/source.js +10 -6
- package/dist/commands/study.js +19 -0
- package/dist/commands/workspace.js +41 -6
- package/dist/index.js +227 -44
- package/dist/lib/alias-store.js +23 -4
- package/dist/lib/auth.js +22 -4
- package/dist/lib/baggage.d.ts +15 -6
- package/dist/lib/baggage.js +14 -8
- package/dist/lib/command-helpers.d.ts +1 -0
- package/dist/lib/command-helpers.js +79 -7
- package/dist/lib/docs.js +211 -21
- package/dist/lib/output.js +61 -17
- package/dist/lib/profile-sources.js +18 -0
- package/dist/lib/skill-content.js +10 -2
- package/dist/upgrade.js +9 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import { runTunnel, runDetached, connectStatus, disconnect } from "./connect.js";
|
|
7
|
-
import { login, decodeJwtClaims } from "./auth.js";
|
|
7
|
+
import { login, decodeJwtClaims, isTokenExpired } from "./auth.js";
|
|
8
8
|
import { loadConfig, saveConfig } from "./config.js";
|
|
9
9
|
import { upgrade } from "./upgrade.js";
|
|
10
10
|
import { registerWorkspaceCommands } from "./commands/workspace.js";
|
|
@@ -27,8 +27,9 @@ import { tagAlias, ALIAS_PREFIX } from "./lib/alias-store.js";
|
|
|
27
27
|
import { output } from "./lib/output.js";
|
|
28
28
|
import { ishDir } from "./lib/paths.js";
|
|
29
29
|
import { findInstalledSkill } from "./lib/skill-content.js";
|
|
30
|
-
import {
|
|
30
|
+
import { initObservability } from "./lib/observability.js";
|
|
31
31
|
import { setSurfaceBaggage } from "./lib/baggage.js";
|
|
32
|
+
import { resolveId } from "./lib/alias-store.js";
|
|
32
33
|
import pkg from "../package.json" with { type: "json" };
|
|
33
34
|
const { version } = pkg;
|
|
34
35
|
// Bootstrap observability before anything else so the very first
|
|
@@ -58,31 +59,61 @@ program
|
|
|
58
59
|
program.configureOutput({
|
|
59
60
|
outputError: () => { },
|
|
60
61
|
});
|
|
62
|
+
// Enable Commander's built-in suggestion engine ("did you mean 'workspace'?"
|
|
63
|
+
// for typos). Closes part of ISSUE-022.
|
|
64
|
+
program.showSuggestionAfterError(true);
|
|
61
65
|
program.exitOverride((err) => {
|
|
62
|
-
// Help and --version are normal exit-0 paths, not errors.
|
|
66
|
+
// Help and --version are normal exit-0 paths, not errors. process.exit
|
|
67
|
+
// is synchronous so the right exit code propagates regardless of any
|
|
68
|
+
// in-flight async work — Commander will otherwise rethrow CommanderError
|
|
69
|
+
// and Node's default uncaughtException handler exits with the error's
|
|
70
|
+
// own exitCode (1) before our async exitWithFlush gets to run.
|
|
63
71
|
if (err.code === "commander.helpDisplayed"
|
|
64
72
|
|| err.code === "commander.version"
|
|
65
73
|
|| err.code === "commander.help") {
|
|
66
|
-
|
|
67
|
-
return;
|
|
74
|
+
process.exit(0);
|
|
68
75
|
}
|
|
76
|
+
// Pattern E + ISSUE-022: the previous envelope used a literal "<command>"
|
|
77
|
+
// placeholder in suggestions that was never substituted. Reconstruct
|
|
78
|
+
// the right help target based on what kind of error fired:
|
|
79
|
+
// - unknownCommand → point at top-level `ish --help` (the user's
|
|
80
|
+
// firstWord IS the typo; no point pointing to it)
|
|
81
|
+
// - missingArgument /
|
|
82
|
+
// requiredOption → point at the actual command's --help
|
|
83
|
+
// (`ish workspace --help` etc.)
|
|
84
|
+
// ISSUE-022.
|
|
85
|
+
const userArgs = process.argv.slice(2);
|
|
86
|
+
const firstWord = userArgs.find((a) => !a.startsWith("-")) ?? "";
|
|
87
|
+
const isUnknownCommand = err.code === "commander.unknownCommand"
|
|
88
|
+
|| err.code === "commander.help" && /unknown/i.test(err.message);
|
|
89
|
+
const helpPath = isUnknownCommand || !firstWord ? "ish --help" : `ish ${firstWord} --help`;
|
|
69
90
|
// Detect --json without relying on parsed opts (parse may have failed).
|
|
70
91
|
const useJson = process.argv.includes("--json") || !process.stdout.isTTY;
|
|
92
|
+
// Strip the redundant "error: " prefix Commander adds — the envelope
|
|
93
|
+
// field is already called "error" (ISSUE-022).
|
|
94
|
+
const cleanedMessage = err.message.replace(/^error:\s*/i, "");
|
|
71
95
|
const envelope = {
|
|
72
|
-
error:
|
|
96
|
+
error: cleanedMessage,
|
|
73
97
|
error_code: "usage_error",
|
|
74
|
-
|
|
98
|
+
// Pattern D: omit `status` entirely when there's no upstream HTTP
|
|
99
|
+
// status to report. The previous `status: 0` was meaningless and
|
|
100
|
+
// confused agents branching on the field.
|
|
75
101
|
retryable: false,
|
|
76
|
-
suggestions: [
|
|
102
|
+
suggestions: [`Run \`${helpPath}\` for usage`],
|
|
77
103
|
};
|
|
78
104
|
if (useJson) {
|
|
79
105
|
console.error(JSON.stringify(envelope));
|
|
80
106
|
}
|
|
81
107
|
else {
|
|
82
|
-
console.error(`Error: ${
|
|
83
|
-
console.error(
|
|
108
|
+
console.error(`Error: ${cleanedMessage}`);
|
|
109
|
+
console.error(` → Run \`${helpPath}\``);
|
|
84
110
|
}
|
|
85
|
-
|
|
111
|
+
// Synchronous exit so the documented usage exit code (2) actually
|
|
112
|
+
// propagates. exitWithFlush is async — its inner process.exit only
|
|
113
|
+
// fires after Commander has already rethrown CommanderError, at which
|
|
114
|
+
// point Node's default handler exits with the error's exitCode (1).
|
|
115
|
+
// Pattern E (ISSUE-021).
|
|
116
|
+
process.exit(EXIT_USAGE);
|
|
86
117
|
});
|
|
87
118
|
// Global options
|
|
88
119
|
program
|
|
@@ -102,8 +133,55 @@ program
|
|
|
102
133
|
program
|
|
103
134
|
.command("login")
|
|
104
135
|
.description("Authenticate with ish via your browser")
|
|
105
|
-
.
|
|
136
|
+
.option("-f, --force", "Re-run the browser flow even if already authenticated (replaces the saved token)")
|
|
137
|
+
.addHelpText("after", `
|
|
138
|
+
By default, \`ish login\` short-circuits when there's already a valid
|
|
139
|
+
saved token — it tells you who you're logged in as and exits without
|
|
140
|
+
opening a browser. Pass --force to re-run the flow anyway (e.g. to
|
|
141
|
+
switch accounts).
|
|
142
|
+
|
|
143
|
+
Without the guard, every reflexive \`ish login\` opens a new browser
|
|
144
|
+
tab AND registers a fresh OAuth client (orphaning the previous one
|
|
145
|
+
on the auth server). The guard prevents both.
|
|
146
|
+
|
|
147
|
+
Examples:
|
|
148
|
+
$ ish login # noop if already authenticated
|
|
149
|
+
$ ish login --force # always re-run`)
|
|
150
|
+
.action(async (opts, cmd) => {
|
|
106
151
|
await runInline(cmd, async (globals) => {
|
|
152
|
+
// ISSUE-007: idempotency guard. Default behavior on an already-
|
|
153
|
+
// authenticated session is to short-circuit with a friendly
|
|
154
|
+
// "Already logged in as ..." message. Pass --force to bypass.
|
|
155
|
+
if (!opts.force) {
|
|
156
|
+
const existing = loadConfig();
|
|
157
|
+
const existingToken = existing.access_token;
|
|
158
|
+
if (existingToken && !isTokenExpired(existingToken)) {
|
|
159
|
+
const claims = decodeJwtClaims(existingToken);
|
|
160
|
+
const email = typeof claims?.email === "string" ? claims.email : "(no email in token)";
|
|
161
|
+
const exp = typeof claims?.exp === "number" ? claims.exp : 0;
|
|
162
|
+
const remainingSec = Math.max(0, exp - Math.floor(Date.now() / 1000));
|
|
163
|
+
const remainingMin = Math.floor(remainingSec / 60);
|
|
164
|
+
// Code-review #7: previously this called output(...) for the
|
|
165
|
+
// structured envelope AND console.error(...) for the human line —
|
|
166
|
+
// human-mode users saw the key-value dump on stdout AND the
|
|
167
|
+
// cleaner sentence on stderr (duplicate). JSON consumers get the
|
|
168
|
+
// structured envelope; human-mode users get just the stderr
|
|
169
|
+
// sentence. Single output path per mode.
|
|
170
|
+
if (globals.json) {
|
|
171
|
+
output({
|
|
172
|
+
message: "Already logged in",
|
|
173
|
+
email,
|
|
174
|
+
token_valid: true,
|
|
175
|
+
expires_in_seconds: remainingSec,
|
|
176
|
+
hint: "Pass --force to re-run the browser flow (e.g. to switch accounts).",
|
|
177
|
+
}, true);
|
|
178
|
+
}
|
|
179
|
+
else if (!globals.quiet) {
|
|
180
|
+
console.error(`Already logged in as ${email} (token valid, expires in ${remainingMin}m). Pass --force to switch accounts.`);
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
107
185
|
const tokens = await login({ dev: globals.dev });
|
|
108
186
|
const config = loadConfig();
|
|
109
187
|
config.access_token = tokens.accessToken;
|
|
@@ -159,19 +237,39 @@ program
|
|
|
159
237
|
// Resolve names of active resources. Failures are non-fatal — we still
|
|
160
238
|
// return the saved IDs so the user knows what's configured.
|
|
161
239
|
const client = token ? new ApiClient({ apiUrl, token }) : null;
|
|
240
|
+
// Helper: look up an active ref's name; on failure attach a `warning`
|
|
241
|
+
// field so the user can tell apart "name unavailable due to transient"
|
|
242
|
+
// vs. "this active ref is orphan — switch or clear it". Pattern A.
|
|
243
|
+
const ORPHAN_HINT = (kind) => `Active ${kind} is no longer accessible (deleted, moved workspace, or auth issue). Use \`ish ${kind} use <id>\` to switch, or \`ish ${kind} use --clear\` to drop.`;
|
|
244
|
+
const lookupName = async (path) => {
|
|
245
|
+
if (!client)
|
|
246
|
+
return {};
|
|
247
|
+
try {
|
|
248
|
+
const row = await client.get(path);
|
|
249
|
+
return row?.name ? { name: row.name } : {};
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
// Distinguish missing-entity (404) from other failures. Either
|
|
253
|
+
// way the user benefits from the warning; the 404 specifically
|
|
254
|
+
// means "orphan", anything else is "couldn't confirm".
|
|
255
|
+
const status = err?.status;
|
|
256
|
+
if (status === 404)
|
|
257
|
+
return { warning: "orphan — entity no longer exists in this workspace" };
|
|
258
|
+
return { warning: "name lookup failed (auth, network, or permission)" };
|
|
259
|
+
}
|
|
260
|
+
};
|
|
162
261
|
let workspace = null;
|
|
163
262
|
if (config.workspace) {
|
|
164
263
|
workspace = {
|
|
165
264
|
id: config.workspace,
|
|
166
265
|
alias: tagAlias(ALIAS_PREFIX.workspace, config.workspace),
|
|
167
266
|
};
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
catch { /* keep id+alias only */ }
|
|
267
|
+
const { name, warning } = await lookupName(`/products/${config.workspace}`);
|
|
268
|
+
if (name)
|
|
269
|
+
workspace.name = name;
|
|
270
|
+
if (warning) {
|
|
271
|
+
workspace.warning = warning;
|
|
272
|
+
workspace.hint = ORPHAN_HINT("workspace");
|
|
175
273
|
}
|
|
176
274
|
}
|
|
177
275
|
let study = null;
|
|
@@ -180,13 +278,12 @@ program
|
|
|
180
278
|
id: config.study,
|
|
181
279
|
alias: tagAlias(ALIAS_PREFIX.study, config.study),
|
|
182
280
|
};
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
catch { /* keep id+alias only */ }
|
|
281
|
+
const { name, warning } = await lookupName(`/studies/${config.study}`);
|
|
282
|
+
if (name)
|
|
283
|
+
study.name = name;
|
|
284
|
+
if (warning) {
|
|
285
|
+
study.warning = warning;
|
|
286
|
+
study.hint = ORPHAN_HINT("study");
|
|
190
287
|
}
|
|
191
288
|
}
|
|
192
289
|
let ask = null;
|
|
@@ -195,13 +292,29 @@ program
|
|
|
195
292
|
id: config.ask,
|
|
196
293
|
alias: tagAlias(ALIAS_PREFIX.ask, config.ask),
|
|
197
294
|
};
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
295
|
+
const { name, warning } = await lookupName(`/asks/${config.ask}`);
|
|
296
|
+
if (name)
|
|
297
|
+
ask.name = name;
|
|
298
|
+
if (warning) {
|
|
299
|
+
ask.warning = warning;
|
|
300
|
+
ask.hint = ORPHAN_HINT("ask");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Pattern A: chat_endpoint was silently absent from `ish status` even
|
|
304
|
+
// when set (ISSUE-017). Surface it the same shape as workspace/study/ask.
|
|
305
|
+
let chatEndpoint = null;
|
|
306
|
+
if (config.chat_endpoint) {
|
|
307
|
+
chatEndpoint = {
|
|
308
|
+
id: config.chat_endpoint,
|
|
309
|
+
alias: tagAlias(ALIAS_PREFIX.chatEndpoint, config.chat_endpoint),
|
|
310
|
+
};
|
|
311
|
+
const { name, warning } = await lookupName(`/chatbot-endpoints/${config.chat_endpoint}`);
|
|
312
|
+
if (name)
|
|
313
|
+
chatEndpoint.name = name;
|
|
314
|
+
if (warning) {
|
|
315
|
+
chatEndpoint.warning = warning;
|
|
316
|
+
chatEndpoint.hint =
|
|
317
|
+
"Active chat endpoint is no longer accessible. Use `ish chat endpoint use <id>` to switch or `ish chat endpoint use --clear` to drop.";
|
|
205
318
|
}
|
|
206
319
|
}
|
|
207
320
|
// Best-effort skill detection: walks parent directories from cwd so an
|
|
@@ -219,12 +332,21 @@ program
|
|
|
219
332
|
workspace,
|
|
220
333
|
study,
|
|
221
334
|
ask,
|
|
335
|
+
chat_endpoint: chatEndpoint,
|
|
222
336
|
skill,
|
|
223
337
|
api_url: apiUrl,
|
|
224
338
|
home: ishDir(),
|
|
225
339
|
};
|
|
226
|
-
if (tokenError && !token)
|
|
227
|
-
|
|
340
|
+
if (tokenError && !token) {
|
|
341
|
+
// Pattern B (ISSUE-001): the inner message already tells the user
|
|
342
|
+
// to run `ish login` (auth.ts emits "Session expired. Run \"ish
|
|
343
|
+
// login\" to re-authenticate." / "No auth token found. Run \"ish
|
|
344
|
+
// login\"…"). Don't re-wrap with another "Run `ish login`. (…)"
|
|
345
|
+
// shell — that produced nested JSON soup with duplicated
|
|
346
|
+
// suggestions in the original bug. Pass the canonical message
|
|
347
|
+
// straight through.
|
|
348
|
+
payload.hint = tokenError;
|
|
349
|
+
}
|
|
228
350
|
if (globals.json) {
|
|
229
351
|
output(payload, true);
|
|
230
352
|
return;
|
|
@@ -239,15 +361,31 @@ program
|
|
|
239
361
|
return `${h}h${m % 60}m`;
|
|
240
362
|
return `${m}m`;
|
|
241
363
|
};
|
|
364
|
+
const renderRef = (ref) => {
|
|
365
|
+
if (!ref)
|
|
366
|
+
return "—";
|
|
367
|
+
const base = `${ref.name ?? "(name unavailable)"} (${ref.alias})`;
|
|
368
|
+
return ref.warning ? `${base} ⚠ ${ref.warning}` : base;
|
|
369
|
+
};
|
|
242
370
|
console.log(`User: ${user ? `${user.email ?? "(no email)"} (token ${user.token_valid ? "valid" : "expired"}, expires in ${fmtSeconds(user.expires_in_seconds)})` : "(not logged in — run `ish login`)"}`);
|
|
243
|
-
console.log(`Workspace: ${
|
|
244
|
-
console.log(`Study: ${
|
|
245
|
-
console.log(`Ask: ${
|
|
371
|
+
console.log(`Workspace: ${renderRef(workspace)}`);
|
|
372
|
+
console.log(`Study: ${renderRef(study)}`);
|
|
373
|
+
console.log(`Ask: ${renderRef(ask)}`);
|
|
374
|
+
console.log(`Chat ep: ${renderRef(chatEndpoint)}`);
|
|
246
375
|
console.log(`Skill: ${skillHit ? skillHit.root : "not installed (run `ish init` to install the agent skill)"}`);
|
|
247
376
|
console.log(`Home: ${ishDir()}`);
|
|
248
377
|
console.log(`API: ${apiUrl}`);
|
|
249
378
|
if (tokenError && !token)
|
|
250
379
|
console.error(`\nHint: ${payload.hint}`);
|
|
380
|
+
// Surface orphan hints once at the bottom so they're visible in human
|
|
381
|
+
// output (the JSON envelope has them inline on each ref).
|
|
382
|
+
for (const [kind, ref] of [["workspace", workspace], ["study", study], ["ask", ask], ["chat_endpoint", chatEndpoint]]) {
|
|
383
|
+
if (ref?.warning) {
|
|
384
|
+
console.error(`\n⚠ ${kind}: ${ref.warning}`);
|
|
385
|
+
if (ref.hint)
|
|
386
|
+
console.error(` → ${ref.hint}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
251
389
|
});
|
|
252
390
|
});
|
|
253
391
|
const connectCmd = program
|
|
@@ -319,9 +457,31 @@ program
|
|
|
319
457
|
.command("upgrade")
|
|
320
458
|
.description("Update ish to the latest version")
|
|
321
459
|
.option("--release <version>", "Install a specific release (e.g. 0.8.1)")
|
|
322
|
-
.addHelpText("after",
|
|
323
|
-
|
|
324
|
-
|
|
460
|
+
.addHelpText("after", `
|
|
461
|
+
Downloads the latest standalone binary from https://ishlabs.io/api/releases/
|
|
462
|
+
and atomically replaces the running executable at \`$execPath\` (typically
|
|
463
|
+
\`~/.ish/bin/ish\`). TLS-verified; no checksum. Already-up-to-date is a
|
|
464
|
+
no-op.
|
|
465
|
+
|
|
466
|
+
Pin a specific release with --release <version>. Note: --version is the
|
|
467
|
+
global CLI-version flag; use --release here.
|
|
468
|
+
|
|
469
|
+
Requires a standalone binary install (curl|sh or brew). On npm-installed
|
|
470
|
+
CLIs (\`npm install -g @ishlabs/cli\`), upgrade refuses with an exit-code-2
|
|
471
|
+
usage error and a pointer to \`npm install -g @ishlabs/cli@latest\` —
|
|
472
|
+
overwriting \`node\` would break every other Node tool.
|
|
473
|
+
|
|
474
|
+
Examples:
|
|
475
|
+
$ ish upgrade
|
|
476
|
+
$ ish upgrade --release 0.20.0`)
|
|
477
|
+
// Pattern C: wrap in runInline so errors flow through outputError +
|
|
478
|
+
// exitCodeFromError. Previously the raw Error was caught by Commander's
|
|
479
|
+
// default handler — leaked bun/node stack traces and exited 0 on
|
|
480
|
+
// validation failures like `--release "not-a-version"` (ISSUE-012).
|
|
481
|
+
.action(async (options, cmd) => {
|
|
482
|
+
await runInline(cmd, async (_globals) => {
|
|
483
|
+
await upgrade(version, options.release);
|
|
484
|
+
});
|
|
325
485
|
});
|
|
326
486
|
injectGlobalWorkspaceOption(program);
|
|
327
487
|
// L3 / Pattern J: surface --get and --fields on every verb's --help.
|
|
@@ -344,11 +504,19 @@ function injectAgentTipsFooter(cmd) {
|
|
|
344
504
|
}
|
|
345
505
|
}
|
|
346
506
|
injectAgentTipsFooter(program);
|
|
347
|
-
// Single source of `client.surface` baggage: fires before
|
|
348
|
-
// subcommand's action handler. `actionCommand.name()` is the leaf
|
|
507
|
+
// Single source of `client.surface` + `workspace_id` baggage: fires before
|
|
508
|
+
// EVERY subcommand's action handler. `actionCommand.name()` is the leaf
|
|
349
509
|
// command name (e.g. "list" inside `ish workspace list`). For groups,
|
|
350
510
|
// Commander joins names via `.parent` — flatten to the dotted path so
|
|
351
511
|
// the backend sees `client.surface=workspace.list` not just `list`.
|
|
512
|
+
//
|
|
513
|
+
// Workspace resolution mirrors `resolveWorkspace` in command-helpers.ts:
|
|
514
|
+
// (1) merged --workspace from action + program globals, (2) ISH_WORKSPACE,
|
|
515
|
+
// (3) the persisted active workspace in ~/.ish/config.json. Aliases
|
|
516
|
+
// (`w-abc`) are normalised to UUIDs via `resolveId`. Resolution is
|
|
517
|
+
// best-effort here — if the value is a bogus alias the action's own
|
|
518
|
+
// `resolveWorkspace` will throw the right error; we just skip stamping
|
|
519
|
+
// baggage rather than crashing the preAction hook.
|
|
352
520
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
353
521
|
const parts = [];
|
|
354
522
|
let cmd = actionCommand;
|
|
@@ -356,6 +524,21 @@ program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
|
356
524
|
parts.unshift(cmd.name());
|
|
357
525
|
cmd = cmd.parent;
|
|
358
526
|
}
|
|
359
|
-
|
|
527
|
+
const surface = parts.join(".") || actionCommand.name();
|
|
528
|
+
let workspaceId;
|
|
529
|
+
try {
|
|
530
|
+
const merged = actionCommand.optsWithGlobals();
|
|
531
|
+
const raw = (typeof merged.workspace === "string" && merged.workspace) ||
|
|
532
|
+
process.env.ISH_WORKSPACE ||
|
|
533
|
+
loadConfig().workspace ||
|
|
534
|
+
undefined;
|
|
535
|
+
if (raw)
|
|
536
|
+
workspaceId = resolveId(raw);
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
// Bogus alias / unresolvable: leave workspace_id off baggage; the
|
|
540
|
+
// action's own resolver will surface a clean error.
|
|
541
|
+
}
|
|
542
|
+
setSurfaceBaggage({ surface, workspaceId });
|
|
360
543
|
});
|
|
361
544
|
program.parse();
|
package/dist/lib/alias-store.js
CHANGED
|
@@ -122,7 +122,12 @@ const HYDRATE_HINT = {
|
|
|
122
122
|
s: "ish study list",
|
|
123
123
|
i: "ish iteration list --study <study-id>",
|
|
124
124
|
p: "ish person list",
|
|
125
|
-
|
|
125
|
+
// ISSUE-038 (partial): `ish source list` is not implemented in 0.19.0.
|
|
126
|
+
// Until it lands, advertise the upload-from-known-path or get-by-uuid
|
|
127
|
+
// workflows rather than the non-existent list command. The deterministic
|
|
128
|
+
// alias map at ~/.ish/aliases.json carries any sources the CLI has
|
|
129
|
+
// touched in this session.
|
|
130
|
+
ps: "ish source upload <file> # or `cat ~/.ish/aliases.json | grep ^ps-` to recover prior aliases",
|
|
126
131
|
pt: "ish participant get <participant-id>",
|
|
127
132
|
c: "ish config list",
|
|
128
133
|
a: "ish ask list",
|
|
@@ -158,11 +163,25 @@ export function resolveId(input) {
|
|
|
158
163
|
const uuid = aliases[input];
|
|
159
164
|
if (uuid)
|
|
160
165
|
return uuid;
|
|
161
|
-
|
|
166
|
+
// Pattern D + ISSUE-038 follow-through: tag as `not_found` for
|
|
167
|
+
// exit-code-4 consistency with the alias-format-unknown branch and
|
|
168
|
+
// the server's 404 path. Splits the hint onto its own line so the
|
|
169
|
+
// recovery suggestion reads cleanly even when the hint contains
|
|
170
|
+
// embedded comments (e.g. for `ps-` which has no list command yet).
|
|
171
|
+
const aliasErr = new Error(`Unknown alias "${input}". To generate aliases, run:\n ${hintForPrefix(input)}`);
|
|
172
|
+
aliasErr.error_code = "not_found";
|
|
173
|
+
throw aliasErr;
|
|
162
174
|
}
|
|
163
|
-
// 3. Anything else — fail with helpful guidance
|
|
164
|
-
|
|
175
|
+
// 3. Anything else — fail with helpful guidance.
|
|
176
|
+
// Pattern D (ISSUE-029): tag as `not_found` so the alias-format-unknown
|
|
177
|
+
// path returns the same envelope shape and exit code (4) as the server's
|
|
178
|
+
// 404 path. Previously the local pre-check returned exit 2 / client_error
|
|
179
|
+
// while the server 404 returned exit 4 / not_found — asymmetric for the
|
|
180
|
+
// same user-visible outcome ("that thing doesn't exist").
|
|
181
|
+
const err = new Error(`Invalid ID "${input}". Use a short alias (e.g. w-a3f, s-b2c) or a full UUID.\n` +
|
|
165
182
|
"Run a list command first to see available aliases:\n" +
|
|
166
183
|
" ish workspace list\n" +
|
|
167
184
|
" ish study list --workspace <id>");
|
|
185
|
+
err.error_code = "not_found";
|
|
186
|
+
throw err;
|
|
168
187
|
}
|
package/dist/lib/auth.js
CHANGED
|
@@ -77,8 +77,20 @@ export async function resolveToken(tokenArg, apiUrl, tokenFileArg) {
|
|
|
77
77
|
if (err instanceof TypeError || (err instanceof Error && err.message.includes('fetch'))) {
|
|
78
78
|
throw new Error('Could not refresh session (network error). Check your connection or run "ish login".');
|
|
79
79
|
}
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// Pattern B (ISSUE-001 / ISSUE-008): default-mode error is the
|
|
81
|
+
// clean sentence; the raw Supabase HTTP body is only useful for
|
|
82
|
+
// debugging and surfacing it in the `hint` field produces nested
|
|
83
|
+
// JSON soup in `ish status`. Gate behind `--verbose`. Detection
|
|
84
|
+
// is via argv (process.argv) rather than threading globals through
|
|
85
|
+
// because resolveToken sits below the command-helpers layer.
|
|
86
|
+
const verbose = process.argv.includes("--verbose");
|
|
87
|
+
const detail = verbose && err instanceof Error && err.message.startsWith("Token refresh failed")
|
|
88
|
+
? ` (${err.message})`
|
|
89
|
+
: "";
|
|
90
|
+
const sessionErr = new Error(`Session expired. Run "ish login" to re-authenticate.${detail}`);
|
|
91
|
+
// Pattern D continuity: tag for consistent exit code 3 mapping.
|
|
92
|
+
sessionErr.error_code = "auth_failed";
|
|
93
|
+
throw sessionErr;
|
|
82
94
|
}
|
|
83
95
|
}
|
|
84
96
|
if (await verifyToken(accessToken, apiUrl)) {
|
|
@@ -93,6 +105,12 @@ export async function resolveToken(tokenArg, apiUrl, tokenFileArg) {
|
|
|
93
105
|
}
|
|
94
106
|
throw new Error('Saved token is invalid. Run "ish login" to re-authenticate.');
|
|
95
107
|
}
|
|
96
|
-
// 6. No token found
|
|
97
|
-
|
|
108
|
+
// 6. No token found.
|
|
109
|
+
// Pattern D (ISSUE-010): tag with the same `error_code` that invalid-
|
|
110
|
+
// token errors carry (`auth_failed`) so agents branching on
|
|
111
|
+
// `error_code === "auth_failed"` to re-prompt for login handle both
|
|
112
|
+
// the no-token and invalid-token cases identically.
|
|
113
|
+
const err = new Error('No auth token found. Run "ish login", set ISH_TOKEN, or pass --token <token> / --token-file <path>.');
|
|
114
|
+
err.error_code = "auth_failed";
|
|
115
|
+
throw err;
|
|
98
116
|
}
|
package/dist/lib/baggage.d.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* Helpers for setting OTel Baggage entries and propagating them on
|
|
3
3
|
* outbound fetches.
|
|
4
4
|
*
|
|
5
|
-
* The CLI carries
|
|
6
|
-
* backend can attribute spans + Sentry events to the right surface
|
|
5
|
+
* The CLI carries four baggage entries on every backend call so the
|
|
6
|
+
* backend can attribute spans + Sentry events to the right surface and,
|
|
7
|
+
* when the command is workspace-scoped, the right workspace:
|
|
7
8
|
*
|
|
8
|
-
* baggage: client.name=ish-cli,client.surface=<command>,client.version=<x.y.z>
|
|
9
|
+
* baggage: client.name=ish-cli,client.surface=<command>,client.version=<x.y.z>[,workspace_id=<uuid>]
|
|
9
10
|
*
|
|
10
11
|
* In Node mode, `@opentelemetry/instrumentation-undici` will inject the
|
|
11
12
|
* active baggage onto outbound `fetch` automatically once
|
|
@@ -23,13 +24,21 @@ export declare const BAGGAGE_KEY_CLIENT_NAME = "client.name";
|
|
|
23
24
|
export declare const BAGGAGE_KEY_CLIENT_SURFACE = "client.surface";
|
|
24
25
|
export declare const BAGGAGE_KEY_CLIENT_VERSION = "client.version";
|
|
25
26
|
export declare const BAGGAGE_KEY_CONSENT_ANALYTICS = "consent.analytics";
|
|
27
|
+
export declare const BAGGAGE_KEY_WORKSPACE_ID = "workspace_id";
|
|
26
28
|
/** The fixed ``client.name`` we tag every CLI invocation with. The plan
|
|
27
29
|
* pins this string — backend dashboards filter on it. */
|
|
28
30
|
export declare const CLIENT_NAME = "ish-cli";
|
|
31
|
+
export interface SurfaceBaggageInput {
|
|
32
|
+
surface: string;
|
|
33
|
+
/** Resolved workspace UUID (raw, not peppered). The backend HMACs this
|
|
34
|
+
* at the PostHog boundary; baggage / Sentry / logs see the raw UUID. */
|
|
35
|
+
workspaceId?: string | null;
|
|
36
|
+
}
|
|
29
37
|
/**
|
|
30
38
|
* Stamp the active OTel baggage with ``client.name`` / ``client.surface``
|
|
31
|
-
* / ``client.version`` and
|
|
32
|
-
* manual-injection
|
|
39
|
+
* / ``client.version`` (always) and ``workspace_id`` (when the command
|
|
40
|
+
* carries one), then stash a process-local copy for the manual-injection
|
|
41
|
+
* ``withBaggage`` helper.
|
|
33
42
|
*
|
|
34
43
|
* Idempotent — subsequent calls overwrite the previous surface, which is
|
|
35
44
|
* what we want when Commander invokes ``preAction`` once per resolved
|
|
@@ -49,7 +58,7 @@ export declare const CLIENT_NAME = "ish-cli";
|
|
|
49
58
|
* Safe to call even with no OTel SDK registered — `@opentelemetry/api`
|
|
50
59
|
* ships no-op fallbacks.
|
|
51
60
|
*/
|
|
52
|
-
export declare function setSurfaceBaggage(
|
|
61
|
+
export declare function setSurfaceBaggage(input: SurfaceBaggageInput): void;
|
|
53
62
|
/**
|
|
54
63
|
* Manually inject the active baggage into a ``HeadersInit`` value.
|
|
55
64
|
*
|
package/dist/lib/baggage.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* Helpers for setting OTel Baggage entries and propagating them on
|
|
3
3
|
* outbound fetches.
|
|
4
4
|
*
|
|
5
|
-
* The CLI carries
|
|
6
|
-
* backend can attribute spans + Sentry events to the right surface
|
|
5
|
+
* The CLI carries four baggage entries on every backend call so the
|
|
6
|
+
* backend can attribute spans + Sentry events to the right surface and,
|
|
7
|
+
* when the command is workspace-scoped, the right workspace:
|
|
7
8
|
*
|
|
8
|
-
* baggage: client.name=ish-cli,client.surface=<command>,client.version=<x.y.z>
|
|
9
|
+
* baggage: client.name=ish-cli,client.surface=<command>,client.version=<x.y.z>[,workspace_id=<uuid>]
|
|
9
10
|
*
|
|
10
11
|
* In Node mode, `@opentelemetry/instrumentation-undici` will inject the
|
|
11
12
|
* active baggage onto outbound `fetch` automatically once
|
|
@@ -25,13 +26,15 @@ export const BAGGAGE_KEY_CLIENT_NAME = "client.name";
|
|
|
25
26
|
export const BAGGAGE_KEY_CLIENT_SURFACE = "client.surface";
|
|
26
27
|
export const BAGGAGE_KEY_CLIENT_VERSION = "client.version";
|
|
27
28
|
export const BAGGAGE_KEY_CONSENT_ANALYTICS = "consent.analytics";
|
|
29
|
+
export const BAGGAGE_KEY_WORKSPACE_ID = "workspace_id";
|
|
28
30
|
/** The fixed ``client.name`` we tag every CLI invocation with. The plan
|
|
29
31
|
* pins this string — backend dashboards filter on it. */
|
|
30
32
|
export const CLIENT_NAME = "ish-cli";
|
|
31
33
|
/**
|
|
32
34
|
* Stamp the active OTel baggage with ``client.name`` / ``client.surface``
|
|
33
|
-
* / ``client.version`` and
|
|
34
|
-
* manual-injection
|
|
35
|
+
* / ``client.version`` (always) and ``workspace_id`` (when the command
|
|
36
|
+
* carries one), then stash a process-local copy for the manual-injection
|
|
37
|
+
* ``withBaggage`` helper.
|
|
35
38
|
*
|
|
36
39
|
* Idempotent — subsequent calls overwrite the previous surface, which is
|
|
37
40
|
* what we want when Commander invokes ``preAction`` once per resolved
|
|
@@ -51,17 +54,20 @@ export const CLIENT_NAME = "ish-cli";
|
|
|
51
54
|
* Safe to call even with no OTel SDK registered — `@opentelemetry/api`
|
|
52
55
|
* ships no-op fallbacks.
|
|
53
56
|
*/
|
|
54
|
-
export function setSurfaceBaggage(
|
|
57
|
+
export function setSurfaceBaggage(input) {
|
|
55
58
|
const existing = propagation.getActiveBaggage() ?? propagation.createBaggage();
|
|
56
|
-
|
|
59
|
+
let next = existing
|
|
57
60
|
.setEntry(BAGGAGE_KEY_CLIENT_NAME, { value: CLIENT_NAME })
|
|
58
|
-
.setEntry(BAGGAGE_KEY_CLIENT_SURFACE, { value:
|
|
61
|
+
.setEntry(BAGGAGE_KEY_CLIENT_SURFACE, { value: input.surface })
|
|
59
62
|
.setEntry(BAGGAGE_KEY_CLIENT_VERSION, { value: pkg.version })
|
|
60
63
|
// CLI runs on a developer's machine with no client-side consent UI.
|
|
61
64
|
// Emit "unknown" so the backend resolver falls through to the
|
|
62
65
|
// authenticated user's persisted consent rather than default-denying
|
|
63
66
|
// via baggage absence.
|
|
64
67
|
.setEntry(BAGGAGE_KEY_CONSENT_ANALYTICS, { value: "unknown" });
|
|
68
|
+
if (input.workspaceId) {
|
|
69
|
+
next = next.setEntry(BAGGAGE_KEY_WORKSPACE_ID, { value: input.workspaceId });
|
|
70
|
+
}
|
|
65
71
|
_activeBaggage = next;
|
|
66
72
|
}
|
|
67
73
|
/** Process-local copy of the active baggage for the manual-injection path.
|
|
@@ -23,6 +23,7 @@ export interface PersonResolveOpts {
|
|
|
23
23
|
/** Profile IDs to exclude from the fetched pool (e.g. participants already on an ask). */
|
|
24
24
|
excludeProfileIds?: Set<string>;
|
|
25
25
|
}
|
|
26
|
+
export declare function normalizeVisibility(raw: string | undefined): string | undefined;
|
|
26
27
|
/** True when any person-selection flag is set on the command. */
|
|
27
28
|
export declare function hasPersonFlags(flags: PersonFilterOpts): boolean;
|
|
28
29
|
/**
|