@trawlme/cli 1.20.0 → 1.21.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/README.md +40 -26
- package/dist/commands/login.js +35 -11
- package/dist/commands/scraps.d.ts +32 -0
- package/dist/commands/scraps.js +256 -79
- package/dist/commands/skills.js +52 -6
- package/dist/commands/telemetry.js +23 -3
- package/dist/commands/token.js +9 -3
- package/dist/lib/confirm.d.ts +70 -0
- package/dist/lib/confirm.js +79 -0
- package/dist/lib/errors.d.ts +13 -0
- package/dist/lib/errors.js +19 -0
- package/package.json +1 -1
package/dist/commands/skills.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { listBundledSkills, installSkill, uninstallSkill, getBundledSkillsVersion, getInstalledVersion, isSkillInstalled, removeOrphanedSkills, } from '../lib/skills.js';
|
|
4
4
|
import { UsageError } from '../lib/errors.js';
|
|
5
|
+
import { json } from '../lib/format.js';
|
|
5
6
|
function pickScope(opts) {
|
|
6
7
|
return opts.local ? 'local' : 'user';
|
|
7
8
|
}
|
|
@@ -20,14 +21,21 @@ export const skills = new Command('skills').description('Manage Claude Code skil
|
|
|
20
21
|
skills
|
|
21
22
|
.command('list')
|
|
22
23
|
.description('List bundled skills and their installed status')
|
|
23
|
-
.
|
|
24
|
+
.option('--json', 'Output as JSON')
|
|
25
|
+
.action((opts) => {
|
|
24
26
|
const bundled = listBundledSkills();
|
|
27
|
+
const version = getBundledSkillsVersion();
|
|
25
28
|
if (!bundled.length) {
|
|
29
|
+
if (opts.json) {
|
|
30
|
+
json({ version, skills: [] });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
26
33
|
console.log(chalk.dim('No bundled skills.'));
|
|
27
34
|
return;
|
|
28
35
|
}
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
if (!opts.json)
|
|
37
|
+
console.log(chalk.dim(`@trawlme/skills@${version}\n`));
|
|
38
|
+
const rows = [];
|
|
31
39
|
for (const name of bundled) {
|
|
32
40
|
const userInstalled = isSkillInstalled(name, 'user');
|
|
33
41
|
const localInstalled = isSkillInstalled(name, 'local');
|
|
@@ -59,7 +67,16 @@ skills
|
|
|
59
67
|
installedVersion = null;
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
|
-
const stale = installedVersion && installedVersion !== version;
|
|
70
|
+
const stale = Boolean(installedVersion && installedVersion !== version);
|
|
71
|
+
if (opts.json) {
|
|
72
|
+
rows.push({
|
|
73
|
+
name,
|
|
74
|
+
installed: { user: userInstalled, local: localInstalled },
|
|
75
|
+
installedVersion,
|
|
76
|
+
outdated: stale,
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
63
80
|
const tag = userInstalled
|
|
64
81
|
? localInstalled
|
|
65
82
|
? '(user + local)'
|
|
@@ -70,46 +87,72 @@ skills
|
|
|
70
87
|
const staleNote = stale ? chalk.yellow(` outdated: ${installedVersion} → ${version}`) : '';
|
|
71
88
|
console.log(` ${name} ${chalk.dim(tag)}${staleNote}`);
|
|
72
89
|
}
|
|
90
|
+
if (opts.json)
|
|
91
|
+
json({ version, skills: rows });
|
|
73
92
|
});
|
|
74
93
|
skills
|
|
75
94
|
.command('install [skill]')
|
|
76
95
|
.description('Install one or all bundled skills')
|
|
77
96
|
.option('--local', 'Install at project level (./.claude/skills) instead of user level (~/.claude/skills)')
|
|
78
97
|
.option('--force', 'Overwrite a pre-existing dir even if trawl did not install it (no .version marker)')
|
|
98
|
+
.option('--json', 'Output as JSON')
|
|
79
99
|
.action((skill, opts) => {
|
|
80
100
|
const scope = pickScope(opts);
|
|
81
101
|
const targets = pickSkills(skill);
|
|
102
|
+
const results = [];
|
|
82
103
|
for (const name of targets) {
|
|
83
104
|
const dest = installSkill(name, scope, { force: opts.force });
|
|
105
|
+
if (opts.json) {
|
|
106
|
+
results.push({ name, scope, dest });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
84
109
|
console.log(chalk.green(`✓ Installed "${name}"`) + chalk.dim(` at ${dest}`));
|
|
85
110
|
}
|
|
111
|
+
if (opts.json) {
|
|
112
|
+
json({ installed: results });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
86
115
|
console.log(chalk.dim(' Restart Claude Code if it was already running.'));
|
|
87
116
|
});
|
|
88
117
|
skills
|
|
89
118
|
.command('uninstall [skill]')
|
|
90
119
|
.description('Remove one or all bundled skills')
|
|
91
120
|
.option('--local', 'Remove from project level (./.claude/skills)')
|
|
121
|
+
.option('--json', 'Output as JSON')
|
|
92
122
|
.action((skill, opts) => {
|
|
93
123
|
const scope = pickScope(opts);
|
|
94
124
|
const targets = pickSkills(skill);
|
|
125
|
+
const results = [];
|
|
95
126
|
for (const name of targets) {
|
|
96
127
|
const dest = uninstallSkill(name, scope);
|
|
128
|
+
if (opts.json) {
|
|
129
|
+
results.push({ name, scope, dest, removed: dest !== null });
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
97
132
|
if (dest)
|
|
98
133
|
console.log(chalk.green(`✓ Removed "${name}"`) + chalk.dim(` from ${dest}`));
|
|
99
134
|
else
|
|
100
135
|
console.log(chalk.dim(` No "${name}" found`));
|
|
101
136
|
}
|
|
137
|
+
if (opts.json)
|
|
138
|
+
json({ uninstalled: results });
|
|
102
139
|
});
|
|
103
140
|
skills
|
|
104
141
|
.command('update [skill]')
|
|
105
142
|
.description('Reinstall over the existing skill (force sync with CLI version)')
|
|
106
143
|
.option('--local', 'Update at project level')
|
|
107
144
|
.option('--force', 'Overwrite a pre-existing dir even if trawl did not install it (no .version marker)')
|
|
145
|
+
.option('--json', 'Output as JSON')
|
|
108
146
|
.action((skill, opts) => {
|
|
109
147
|
const scope = pickScope(opts);
|
|
110
148
|
const targets = pickSkills(skill);
|
|
149
|
+
const results = [];
|
|
111
150
|
for (const name of targets) {
|
|
112
151
|
const dest = installSkill(name, scope, { force: opts.force });
|
|
152
|
+
if (opts.json) {
|
|
153
|
+
results.push({ name, scope, dest });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
113
156
|
console.log(chalk.green(`✓ Updated "${name}"`) + chalk.dim(` at ${dest}`));
|
|
114
157
|
}
|
|
115
158
|
// #86 review — same orphan sweep as the startup auto-sync: an explicit
|
|
@@ -117,6 +160,9 @@ skills
|
|
|
117
160
|
// or removed upstream (e.g. 1.0.0's `trawl` → 1.3.1's `trawl-cli`),
|
|
118
161
|
// instead of leaving a stale ghost teaching outdated usage. Only the
|
|
119
162
|
// scope being updated is swept; marker-less dirs are never touched.
|
|
120
|
-
// removeOrphanedSkills prints its own honest stderr line per removal
|
|
121
|
-
|
|
163
|
+
// removeOrphanedSkills prints its own honest stderr line per removal
|
|
164
|
+
// (stderr, so it's safe under --json too).
|
|
165
|
+
const orphansRemoved = removeOrphanedSkills(scope);
|
|
166
|
+
if (opts.json)
|
|
167
|
+
json({ updated: results, orphansRemoved });
|
|
122
168
|
});
|
|
@@ -2,33 +2,53 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import config from '../lib/config.js';
|
|
4
4
|
import { initPostHog } from '../lib/posthog.js';
|
|
5
|
+
import { json } from '../lib/format.js';
|
|
5
6
|
export const telemetry = new Command('telemetry').description('Manage CLI telemetry (usage data collection)');
|
|
6
7
|
telemetry
|
|
7
8
|
.command('on')
|
|
8
9
|
.description('Enable telemetry (default)')
|
|
9
|
-
.
|
|
10
|
+
.option('--json', 'Output as JSON')
|
|
11
|
+
.action((opts) => {
|
|
10
12
|
config.set('telemetry', true);
|
|
11
13
|
// Ensure a telemetryUserId is created on opt-in
|
|
12
14
|
initPostHog();
|
|
15
|
+
if (opts.json) {
|
|
16
|
+
json({ telemetry: true });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
13
19
|
console.log(chalk.green('✓ Telemetry enabled'));
|
|
14
20
|
console.log(chalk.dim(' Anonymous usage data will be collected. Run `trawl telemetry status` to view.'));
|
|
15
21
|
});
|
|
16
22
|
telemetry
|
|
17
23
|
.command('off')
|
|
18
24
|
.description('Disable telemetry')
|
|
19
|
-
.
|
|
25
|
+
.option('--json', 'Output as JSON')
|
|
26
|
+
.action((opts) => {
|
|
20
27
|
config.set('telemetry', false);
|
|
28
|
+
if (opts.json) {
|
|
29
|
+
json({ telemetry: false });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
21
32
|
console.log(chalk.green('✓ Telemetry disabled'));
|
|
22
33
|
console.log(chalk.dim(' No data will be sent. Your telemetry ID is kept for if you re-enable later.'));
|
|
23
34
|
});
|
|
24
35
|
telemetry
|
|
25
36
|
.command('status')
|
|
26
37
|
.description('Show current telemetry state')
|
|
27
|
-
.
|
|
38
|
+
.option('--json', 'Output as JSON')
|
|
39
|
+
.action((opts) => {
|
|
28
40
|
const enabled = config.get('telemetry') !== false;
|
|
29
41
|
const userId = config.get('telemetryUserId') || '(not yet generated)';
|
|
30
42
|
const envOverride = process.env['TRAWL_TELEMETRY'] === '0';
|
|
31
43
|
const doNotTrack = process.env['DO_NOT_TRACK'] === '1';
|
|
44
|
+
// The effective on/off state factoring in both env overrides — matches
|
|
45
|
+
// the human-mode "State:" line's own precedence (DO_NOT_TRACK >
|
|
46
|
+
// TRAWL_TELEMETRY > config).
|
|
47
|
+
const effectiveEnabled = enabled && !envOverride && !doNotTrack;
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
json({ enabled: effectiveEnabled, configEnabled: enabled, telemetryUserId: config.get('telemetryUserId') || null, envOverride, doNotTrack });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
32
52
|
console.log(chalk.bold('Telemetry status'));
|
|
33
53
|
console.log(chalk.dim(' State: ') +
|
|
34
54
|
(doNotTrack
|
package/dist/commands/token.js
CHANGED
|
@@ -4,9 +4,11 @@ import { getToken } from '../lib/config.js';
|
|
|
4
4
|
import { AuthError, notLoggedInError } from '../lib/api.js';
|
|
5
5
|
import { reportError } from '../lib/errors.js';
|
|
6
6
|
import { decodeExp } from '../lib/jwt.js';
|
|
7
|
+
import { json } from '../lib/format.js';
|
|
7
8
|
export const token = new Command('token')
|
|
8
9
|
.description('Print the stored session JWT (for MCP Bearer auth)')
|
|
9
|
-
.
|
|
10
|
+
.option('--json', 'Output as JSON ({token, exp, expiresAt}) instead of the raw JWT')
|
|
11
|
+
.action((opts) => {
|
|
10
12
|
// getToken() resolves TRAWL_TOKEN env first, then the stored config
|
|
11
13
|
// token (see config.ts:47-51) — matching every other token consumer in
|
|
12
14
|
// the CLI instead of reading the config store directly. (#86 finding 1)
|
|
@@ -15,7 +17,7 @@ export const token = new Command('token')
|
|
|
15
17
|
// Auth-classified (ApiError 401 → exit 3, kind:"auth"), not a generic
|
|
16
18
|
// exit 1 — an agent scripting `trawl token` needs to tell "not logged
|
|
17
19
|
// in" apart from an arbitrary bug. (#86 finding 1)
|
|
18
|
-
process.exitCode = reportError(notLoggedInError());
|
|
20
|
+
process.exitCode = reportError(notLoggedInError(), { json: opts.json });
|
|
19
21
|
return;
|
|
20
22
|
}
|
|
21
23
|
const exp = decodeExp(stored);
|
|
@@ -24,7 +26,11 @@ export const token = new Command('token')
|
|
|
24
26
|
// Decoded entirely client-side (no HTTP call made) — AuthError, not a
|
|
25
27
|
// fabricated ApiError(401): the server never actually said this. (#88
|
|
26
28
|
// item 4)
|
|
27
|
-
process.exitCode = reportError(new AuthError('Session token expired. Run: trawl login to refresh.'));
|
|
29
|
+
process.exitCode = reportError(new AuthError('Session token expired. Run: trawl login to refresh.'), { json: opts.json });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (opts.json) {
|
|
33
|
+
json({ token: stored, exp, expiresAt: exp !== null ? new Date(exp * 1000).toISOString() : null });
|
|
28
34
|
return;
|
|
29
35
|
}
|
|
30
36
|
// Print the raw token first (so it can be piped / copied)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #107 — the machine contract's non-interactive rule, in one place. True only
|
|
3
|
+
* when a blocking interactive prompt is safe to show: NOT `--json` (a machine
|
|
4
|
+
* consumer needs pure stdout, and there's no human reading a prompt anyway)
|
|
5
|
+
* AND both stdin/stdout are real TTYs. A pipe/redirect/CI runner — including
|
|
6
|
+
* an agent driving this CLI as a subprocess — has nowhere for a human to type
|
|
7
|
+
* an answer; a blocking `readline` prompt in that situation hangs forever
|
|
8
|
+
* instead of ever returning.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isInteractive(opts?: {
|
|
11
|
+
json?: boolean;
|
|
12
|
+
}): boolean;
|
|
13
|
+
export interface ConfirmOutcome {
|
|
14
|
+
/** True when the caller should proceed with the guarded action. */
|
|
15
|
+
proceed: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* True when refused because the invocation is non-interactive (`--json`,
|
|
18
|
+
* or stdin/stdout isn't a real TTY) — a structured usage error has ALREADY
|
|
19
|
+
* been reported (the stderr line and/or the `--json` envelope) and
|
|
20
|
+
* `process.exitCode` set to 2. The caller must return immediately without
|
|
21
|
+
* printing anything else (mirrors every other `usageError()` call site).
|
|
22
|
+
*/
|
|
23
|
+
blocked: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Shared guard for every destructive y/N confirmation in the CLI (`scraps
|
|
27
|
+
* delete`, `scraps account delete`, …). #107 — before this, each call site
|
|
28
|
+
* hand-rolled its own `readline` question with no non-interactive escape
|
|
29
|
+
* hatch, so a scripted/agent invocation with no `-f`/`--force` would hang
|
|
30
|
+
* forever waiting for a y/N answer that could never arrive.
|
|
31
|
+
*
|
|
32
|
+
* `-f`/`--force` (`opts.force`) always pre-confirms, interactive or not — the
|
|
33
|
+
* caller already gave explicit consent up front. Otherwise:
|
|
34
|
+
* - Non-interactive (`--json`, or stdin/stdout isn't a real TTY): NEVER
|
|
35
|
+
* prompts. Reports a structured usage error (exit 2) instead, via the same
|
|
36
|
+
* central `reportError` formatting path every other error in this CLI
|
|
37
|
+
* uses (stderr human line, or the `--json` envelope on stdout).
|
|
38
|
+
* - Interactive: prompts via `readline` exactly like the pre-#107 call
|
|
39
|
+
* sites did.
|
|
40
|
+
*
|
|
41
|
+
* `message` MUST be plain, unstyled text — it feeds both the refusal
|
|
42
|
+
* UsageError's message (human stderr line AND the `--json` error envelope on
|
|
43
|
+
* stdout) and the fallback prompt text. Before this, call sites passed a
|
|
44
|
+
* `chalk.bold(id)`-styled string as `message`, which leaked raw ANSI escape
|
|
45
|
+
* bytes into the `--json` envelope (e.g. `scraps delete X --json` on the
|
|
46
|
+
* refusal path emitted a message with the raw bold-on/off escape sequence
|
|
47
|
+
* wrapped around the id) — unusable for a script/agent parsing that string.
|
|
48
|
+
* `promptMessage` is the OPTIONAL styled variant shown only for the
|
|
49
|
+
* interactive TTY `[y/N]` prompt (chalk is safe there — no machine ever
|
|
50
|
+
* reads it); it defaults to `message` when omitted. (#107 review F2)
|
|
51
|
+
*/
|
|
52
|
+
export declare function confirmDestructive(message: string, opts?: {
|
|
53
|
+
force?: boolean;
|
|
54
|
+
json?: boolean;
|
|
55
|
+
promptMessage?: string;
|
|
56
|
+
}): Promise<ConfirmOutcome>;
|
|
57
|
+
/**
|
|
58
|
+
* Guard for a required interactive value that has no flag-supplied value yet
|
|
59
|
+
* (login's email prompt, …) — for call sites that propagate errors by
|
|
60
|
+
* `throw`ing and let the top-level handler in index.ts classify + report
|
|
61
|
+
* (login.ts's existing convention), as opposed to `scraps.ts`'s local
|
|
62
|
+
* exitCode-setting `usageError()` style (which should check `isInteractive`
|
|
63
|
+
* directly instead of using this). Same non-interactive contract as
|
|
64
|
+
* `confirmDestructive`: never invokes a blocking prompt when `--json` is set
|
|
65
|
+
* or stdin/stdout isn't a real TTY — throws a `UsageError` naming the flag to
|
|
66
|
+
* pass instead. (#107)
|
|
67
|
+
*/
|
|
68
|
+
export declare function requireInteractive(message: string, opts?: {
|
|
69
|
+
json?: boolean;
|
|
70
|
+
}): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { UsageError } from './errors.js';
|
|
3
|
+
import { reportError } from './errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* #107 — the machine contract's non-interactive rule, in one place. True only
|
|
6
|
+
* when a blocking interactive prompt is safe to show: NOT `--json` (a machine
|
|
7
|
+
* consumer needs pure stdout, and there's no human reading a prompt anyway)
|
|
8
|
+
* AND both stdin/stdout are real TTYs. A pipe/redirect/CI runner — including
|
|
9
|
+
* an agent driving this CLI as a subprocess — has nowhere for a human to type
|
|
10
|
+
* an answer; a blocking `readline` prompt in that situation hangs forever
|
|
11
|
+
* instead of ever returning.
|
|
12
|
+
*/
|
|
13
|
+
export function isInteractive(opts = {}) {
|
|
14
|
+
if (opts.json)
|
|
15
|
+
return false;
|
|
16
|
+
return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Shared guard for every destructive y/N confirmation in the CLI (`scraps
|
|
20
|
+
* delete`, `scraps account delete`, …). #107 — before this, each call site
|
|
21
|
+
* hand-rolled its own `readline` question with no non-interactive escape
|
|
22
|
+
* hatch, so a scripted/agent invocation with no `-f`/`--force` would hang
|
|
23
|
+
* forever waiting for a y/N answer that could never arrive.
|
|
24
|
+
*
|
|
25
|
+
* `-f`/`--force` (`opts.force`) always pre-confirms, interactive or not — the
|
|
26
|
+
* caller already gave explicit consent up front. Otherwise:
|
|
27
|
+
* - Non-interactive (`--json`, or stdin/stdout isn't a real TTY): NEVER
|
|
28
|
+
* prompts. Reports a structured usage error (exit 2) instead, via the same
|
|
29
|
+
* central `reportError` formatting path every other error in this CLI
|
|
30
|
+
* uses (stderr human line, or the `--json` envelope on stdout).
|
|
31
|
+
* - Interactive: prompts via `readline` exactly like the pre-#107 call
|
|
32
|
+
* sites did.
|
|
33
|
+
*
|
|
34
|
+
* `message` MUST be plain, unstyled text — it feeds both the refusal
|
|
35
|
+
* UsageError's message (human stderr line AND the `--json` error envelope on
|
|
36
|
+
* stdout) and the fallback prompt text. Before this, call sites passed a
|
|
37
|
+
* `chalk.bold(id)`-styled string as `message`, which leaked raw ANSI escape
|
|
38
|
+
* bytes into the `--json` envelope (e.g. `scraps delete X --json` on the
|
|
39
|
+
* refusal path emitted a message with the raw bold-on/off escape sequence
|
|
40
|
+
* wrapped around the id) — unusable for a script/agent parsing that string.
|
|
41
|
+
* `promptMessage` is the OPTIONAL styled variant shown only for the
|
|
42
|
+
* interactive TTY `[y/N]` prompt (chalk is safe there — no machine ever
|
|
43
|
+
* reads it); it defaults to `message` when omitted. (#107 review F2)
|
|
44
|
+
*/
|
|
45
|
+
export async function confirmDestructive(message, opts = {}) {
|
|
46
|
+
if (opts.force)
|
|
47
|
+
return { proceed: true, blocked: false };
|
|
48
|
+
if (!isInteractive(opts)) {
|
|
49
|
+
process.exitCode = reportError(new UsageError(`${message} requires confirmation — refusing to block on a prompt (non-interactive). Pass -f/--force to confirm.`), { json: opts.json });
|
|
50
|
+
return { proceed: false, blocked: true };
|
|
51
|
+
}
|
|
52
|
+
const { createInterface } = await import('readline');
|
|
53
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
54
|
+
try {
|
|
55
|
+
const answer = await new Promise((resolve) => {
|
|
56
|
+
rl.question(`${opts.promptMessage ?? message} ${chalk.dim('[y/N]')} `, (a) => resolve(a.trim().toLowerCase()));
|
|
57
|
+
});
|
|
58
|
+
return { proceed: answer === 'y' || answer === 'yes', blocked: false };
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
rl.close();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Guard for a required interactive value that has no flag-supplied value yet
|
|
66
|
+
* (login's email prompt, …) — for call sites that propagate errors by
|
|
67
|
+
* `throw`ing and let the top-level handler in index.ts classify + report
|
|
68
|
+
* (login.ts's existing convention), as opposed to `scraps.ts`'s local
|
|
69
|
+
* exitCode-setting `usageError()` style (which should check `isInteractive`
|
|
70
|
+
* directly instead of using this). Same non-interactive contract as
|
|
71
|
+
* `confirmDestructive`: never invokes a blocking prompt when `--json` is set
|
|
72
|
+
* or stdin/stdout isn't a real TTY — throws a `UsageError` naming the flag to
|
|
73
|
+
* pass instead. (#107)
|
|
74
|
+
*/
|
|
75
|
+
export function requireInteractive(message, opts = {}) {
|
|
76
|
+
if (!isInteractive(opts)) {
|
|
77
|
+
throw new UsageError(message);
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -8,6 +8,19 @@
|
|
|
8
8
|
export declare class UsageError extends Error {
|
|
9
9
|
constructor(message: string);
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Thrown for a business-logic REFUSAL the server explicitly reported back
|
|
13
|
+
* (e.g. a tier-ceiling override the registry cap rejected) — distinct from
|
|
14
|
+
* an arbitrary unmapped bug. Before this, `reportTierRefusal` routed a bare
|
|
15
|
+
* `new Error(message)` through here, which fell through to the generic
|
|
16
|
+
* `kind:"unknown"` bucket — indistinguishable from a genuine crash, even
|
|
17
|
+
* though the README sells `kind` as the machine discriminant an agent
|
|
18
|
+
* branches on. Same exit code (1: a business-logic refusal, not a usage
|
|
19
|
+
* error) as before — only the `kind` differs. (#107 review F3)
|
|
20
|
+
*/
|
|
21
|
+
export declare class RefusalError extends Error {
|
|
22
|
+
constructor(message: string);
|
|
23
|
+
}
|
|
11
24
|
export interface ErrorEnvelope {
|
|
12
25
|
message: string;
|
|
13
26
|
status?: number;
|
package/dist/lib/errors.js
CHANGED
|
@@ -13,6 +13,22 @@ export class UsageError extends Error {
|
|
|
13
13
|
this.name = 'UsageError';
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* Thrown for a business-logic REFUSAL the server explicitly reported back
|
|
18
|
+
* (e.g. a tier-ceiling override the registry cap rejected) — distinct from
|
|
19
|
+
* an arbitrary unmapped bug. Before this, `reportTierRefusal` routed a bare
|
|
20
|
+
* `new Error(message)` through here, which fell through to the generic
|
|
21
|
+
* `kind:"unknown"` bucket — indistinguishable from a genuine crash, even
|
|
22
|
+
* though the README sells `kind` as the machine discriminant an agent
|
|
23
|
+
* branches on. Same exit code (1: a business-logic refusal, not a usage
|
|
24
|
+
* error) as before — only the `kind` differs. (#107 review F3)
|
|
25
|
+
*/
|
|
26
|
+
export class RefusalError extends Error {
|
|
27
|
+
constructor(message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'RefusalError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
16
32
|
/**
|
|
17
33
|
* Central status → exit-code map (#71 findings 13/14/60). Agents driving this
|
|
18
34
|
* CLI unattended need to tell "you're not logged in" (3) from "that id
|
|
@@ -43,6 +59,9 @@ export function classifyError(err) {
|
|
|
43
59
|
if (err instanceof UsageError) {
|
|
44
60
|
return { exitCode: 2, envelope: { message, kind: 'usage' } };
|
|
45
61
|
}
|
|
62
|
+
if (err instanceof RefusalError) {
|
|
63
|
+
return { exitCode: 1, envelope: { message, kind: 'refused' } };
|
|
64
|
+
}
|
|
46
65
|
return { exitCode: 1, envelope: { message, kind: 'unknown' } };
|
|
47
66
|
}
|
|
48
67
|
/**
|