driggsby 0.1.42 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +59 -0
  2. package/bin/driggsby.js +2 -0
  3. package/dist/api/base-url.js +31 -0
  4. package/dist/api/network-error.js +55 -0
  5. package/dist/args-deploy.js +94 -0
  6. package/dist/args-init.js +29 -0
  7. package/dist/args-mcp-setup.js +258 -0
  8. package/dist/args-shared.js +28 -0
  9. package/dist/args.js +177 -0
  10. package/dist/clap-suggestions.js +66 -0
  11. package/dist/cli-error.js +12 -0
  12. package/dist/credentials/clear-outcome.js +1 -0
  13. package/dist/credentials/file-store.js +77 -0
  14. package/dist/credentials/keychain.js +54 -0
  15. package/dist/credentials/secret-tool.js +48 -0
  16. package/dist/credentials/store.js +164 -0
  17. package/dist/credentials/tool-runner.js +55 -0
  18. package/dist/deploy/api-session.js +85 -0
  19. package/dist/deploy/deploy-command.js +126 -0
  20. package/dist/deploy/rollback-command.js +100 -0
  21. package/dist/deploy/versions-command.js +97 -0
  22. package/dist/dev/dev-command.js +106 -0
  23. package/dist/dev/dev-servers.js +274 -0
  24. package/dist/dev/host-page.js +147 -0
  25. package/dist/dev/mcp-broker.js +149 -0
  26. package/dist/dev/static-files.js +104 -0
  27. package/dist/dev/tool-allowlist.js +18 -0
  28. package/dist/dev/watcher.js +76 -0
  29. package/dist/help.js +180 -0
  30. package/dist/init/init-command.js +114 -0
  31. package/dist/init/templates.js +286 -0
  32. package/dist/login/claim-client.js +151 -0
  33. package/dist/login/login.js +146 -0
  34. package/dist/login/logout.js +37 -0
  35. package/dist/login/open-url.js +79 -0
  36. package/dist/main.js +57 -0
  37. package/dist/mcp-setup/classify.js +37 -0
  38. package/dist/mcp-setup/client-command.js +133 -0
  39. package/dist/mcp-setup/commands.js +53 -0
  40. package/dist/mcp-setup/known-client.js +38 -0
  41. package/dist/mcp-setup/setup.js +137 -0
  42. package/dist/mcp-setup/spawn-plan.js +91 -0
  43. package/dist/mcp-setup/text.js +83 -0
  44. package/dist/terminal-text.js +19 -0
  45. package/dist/version.js +6 -0
  46. package/package.json +22 -41
  47. package/bin/driggsby +0 -10
  48. package/install.js +0 -159
  49. package/lib/artifacts.js +0 -49
  50. package/lib/process.js +0 -65
package/README.md CHANGED
@@ -29,6 +29,65 @@ OAuth-based remote MCP at:
29
29
  https://app.driggsby.com/mcp
30
30
  ```
31
31
 
32
+ Some driggsby commands work with your Driggsby account directly and need
33
+ an app token saved on your machine: run
34
+ `npx driggsby@latest login` and approve the page it opens. The token is
35
+ stored in your platform keychain when one is available, otherwise in
36
+ `~/.driggsby/credentials.json`, and is never printed.
37
+ `npx driggsby@latest logout` removes it.
38
+
39
+ ## Building an app
40
+
41
+ Driggsby hosts small static apps — dashboards, visualizations — each at
42
+ its own `driggsby.dev` address. Start one with:
43
+
44
+ ```bash
45
+ npx driggsby@latest init your-app-name
46
+ ```
47
+
48
+ This creates a working app — `index.html`, `app.js`, `styles.css`, and the
49
+ `driggsby.json` that names it. The app reads live data with one call,
50
+ `driggsby.watch(tool, params, callback)`, and shows sample data when opened
51
+ on its own.
52
+
53
+ Preview it with your real Driggsby data:
54
+
55
+ ```bash
56
+ npx driggsby@latest dev
57
+ ```
58
+
59
+ This runs the app locally, embedded exactly the way Driggsby embeds it
60
+ after a deploy, and reloads the page when you save a file. While dev runs,
61
+ the preview serves your financial data to this machine — other websites
62
+ can't reach it, but anything running locally can, so stop it when you're
63
+ done.
64
+
65
+ ## Deploying an app
66
+
67
+ From a folder holding a `driggsby.json`
68
+ (`{ "slug": "your-app-name", "serve": "." }`) and an `index.html`:
69
+
70
+ ```bash
71
+ npx driggsby@latest deploy
72
+ ```
73
+
74
+ The first deploy creates the app: Driggsby assigns its address — the name
75
+ you picked plus a unique ending, like
76
+ `your-app-name-x7k2qf.driggsby.dev` — and saves the assigned name back
77
+ into `driggsby.json`, so any readable name works and names never collide.
78
+ If the project lives in git, commit the updated `driggsby.json` — it is the
79
+ only record of your app's address, and a fresh checkout without it would
80
+ create a second app.
81
+ Every deploy after that uploads only the files that changed. Deploy with
82
+ `--preview` to upload a version without
83
+ changing what visitors see, `npx driggsby@latest versions` to list every
84
+ kept version, and `npx driggsby@latest rollback` to switch which one is
85
+ live — switching re-uploads nothing.
86
+
87
+ The CLI is pure TypeScript and installs from the npm registry alone — no
88
+ binary downloads, no install scripts — and runs on macOS, Linux, and Windows
89
+ with Node.js 18 or newer.
90
+
32
91
  See the [GitHub repo](https://github.com/driggsby/driggsby) for
33
92
  tools, examples, and full documentation.
34
93
 
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/main.js";
@@ -0,0 +1,31 @@
1
+ // Where the CLI's own API calls go. DRIGGSBY_BASE_URL is a maintainer/test
2
+ // override (pointing e2e runs at a local server); it is deliberately absent
3
+ // from public documentation. The policy itself — which URLs are acceptable,
4
+ // and which may receive credentials — lives once, in @driggsby/deploy's
5
+ // base-url module; this file only maps its failures to this CLI's error
6
+ // type.
7
+ import { DeployError, isLoopbackBaseUrl, PUBLIC_BASE_URL, resolveBaseUrl } from "@driggsby/deploy";
8
+ import { CliError } from "../cli-error.js";
9
+ export function apiBaseUrl(env = process.env) {
10
+ try {
11
+ return resolveBaseUrl(env);
12
+ }
13
+ catch (error) {
14
+ if (error instanceof DeployError) {
15
+ throw new CliError(error.message, 1);
16
+ }
17
+ throw error;
18
+ }
19
+ }
20
+ export function apiHost(baseUrl) {
21
+ return new URL(baseUrl).host;
22
+ }
23
+ // Whether the machine's SAVED sign-in (keychain/keyring/credentials file)
24
+ // may be sent to this base URL: Driggsby itself, or a dev server on this
25
+ // machine. Any other DRIGGSBY_BASE_URL must bring its own token via
26
+ // DRIGGSBY_TOKEN — otherwise setting a single environment variable (a
27
+ // .envrc in a cloned repo is enough) would redirect the saved credential,
28
+ // which unlocks the person's financial data, to an arbitrary host.
29
+ export function baseUrlMayReceiveSavedSignIn(baseUrl) {
30
+ return baseUrl === PUBLIC_BASE_URL || isLoopbackBaseUrl(baseUrl);
31
+ }
@@ -0,0 +1,55 @@
1
+ // Turns a failed fetch to the Driggsby API into one exact-fix message for
2
+ // restricted-network environments (agent sandboxes with egress allowlists
3
+ // are the common case). Written for a zero-context agent: it names the one
4
+ // domain to allow and the one command to rerun. Anything that is not a
5
+ // connection-level failure returns null so the caller's own handling runs.
6
+ import { CliError } from "../cli-error.js";
7
+ import { wrapProse } from "../terminal-text.js";
8
+ const CONNECTION_FAILURE_CODES = new Set([
9
+ "ENOTFOUND",
10
+ "EAI_AGAIN",
11
+ "ECONNREFUSED",
12
+ "ECONNRESET",
13
+ "ETIMEDOUT",
14
+ "EHOSTUNREACH",
15
+ "ENETUNREACH",
16
+ "EPIPE",
17
+ "UND_ERR_CONNECT_TIMEOUT",
18
+ "UND_ERR_HEADERS_TIMEOUT",
19
+ "UND_ERR_BODY_TIMEOUT",
20
+ "UND_ERR_SOCKET",
21
+ ]);
22
+ export function blockedNetworkError(error, context) {
23
+ if (!hasConnectionFailure(error, 0)) {
24
+ return null;
25
+ }
26
+ // The host is dynamic, so both sentences go through wrapProse: an agent
27
+ // reading this to build an egress allowlist must never see the hostname
28
+ // split across a soft-wrapped line.
29
+ return new CliError(`${wrapProse(`We couldn't reach ${context.host} from this machine.`)}\n\n` +
30
+ `${wrapProse(`If this environment restricts network access, allow HTTPS access to ${context.host} and try again:`)}\n` +
31
+ ` ${context.retryCommand}`, 1);
32
+ }
33
+ // Walks the error's cause chain (and AggregateError members — undici reports
34
+ // multi-address connection failures that way) looking for a known
35
+ // connection-level errno code.
36
+ function hasConnectionFailure(error, depth) {
37
+ if (depth > 6 || typeof error !== "object" || error === null) {
38
+ return false;
39
+ }
40
+ const code = error.code;
41
+ if (typeof code === "string" && CONNECTION_FAILURE_CODES.has(code)) {
42
+ return true;
43
+ }
44
+ // An AbortSignal.timeout fires as a DOMException named TimeoutError: the
45
+ // stalled-but-connected shape (an egress proxy that accepts and never
46
+ // replies) that the errno codes above don't cover.
47
+ if (error.name === "TimeoutError") {
48
+ return true;
49
+ }
50
+ if (error instanceof AggregateError && error.errors.some((inner) => hasConnectionFailure(inner, depth + 1))) {
51
+ return true;
52
+ }
53
+ const cause = error.cause;
54
+ return hasConnectionFailure(cause, depth + 1);
55
+ }
@@ -0,0 +1,94 @@
1
+ // Argv parsing for the deploy command family: deploy, rollback, versions.
2
+ // Same behavior contract as the rest of the tree — help on -h/--help, usage
3
+ // errors exit 2, echoed argv is sanitized — without chasing clap byte quirks.
4
+ import { unexpectedArgument, unexpectedFlagValue, } from "./args-shared.js";
5
+ import { CliError } from "./cli-error.js";
6
+ import { DEPLOY_HELP, ROLLBACK_HELP, VERSIONS_HELP } from "./help.js";
7
+ import { sanitizeForTerminal } from "./terminal-text.js";
8
+ const DEPLOY_USAGE = "Usage: npx driggsby@latest deploy [--preview]";
9
+ const ROLLBACK_USAGE = "Usage: npx driggsby@latest rollback [--to <VERSION>]";
10
+ const VERSIONS_USAGE = "Usage: npx driggsby@latest versions";
11
+ export function parseDeploy(argv) {
12
+ let preview = false;
13
+ let optionsEnded = false;
14
+ for (const token of argv) {
15
+ if (!optionsEnded && token === "--") {
16
+ optionsEnded = true;
17
+ continue;
18
+ }
19
+ if (!optionsEnded && (token === "-h" || token === "--help")) {
20
+ return helpCommand(DEPLOY_HELP);
21
+ }
22
+ if (!optionsEnded && token === "--preview") {
23
+ preview = true;
24
+ continue;
25
+ }
26
+ if (!optionsEnded && token.startsWith("--preview=")) {
27
+ throw unexpectedFlagValue("--preview", token.slice("--preview=".length), DEPLOY_USAGE);
28
+ }
29
+ throw unexpectedArgument(token, DEPLOY_USAGE);
30
+ }
31
+ return { kind: "deploy", preview };
32
+ }
33
+ export function parseRollback(argv) {
34
+ let toVersion = null;
35
+ let optionsEnded = false;
36
+ for (let index = 0; index < argv.length; index += 1) {
37
+ const token = argv[index] ?? "";
38
+ if (!optionsEnded && token === "--") {
39
+ optionsEnded = true;
40
+ continue;
41
+ }
42
+ if (!optionsEnded && (token === "-h" || token === "--help")) {
43
+ return helpCommand(ROLLBACK_HELP);
44
+ }
45
+ if (!optionsEnded && token === "--to") {
46
+ const value = argv[index + 1];
47
+ if (value === undefined) {
48
+ throw missingToValue();
49
+ }
50
+ toVersion = parseVersionNumber(value);
51
+ index += 1;
52
+ continue;
53
+ }
54
+ if (!optionsEnded && token.startsWith("--to=")) {
55
+ toVersion = parseVersionNumber(token.slice("--to=".length));
56
+ continue;
57
+ }
58
+ throw unexpectedArgument(token, ROLLBACK_USAGE);
59
+ }
60
+ return { kind: "rollback", toVersion };
61
+ }
62
+ export function parseVersions(argv) {
63
+ let optionsEnded = false;
64
+ for (const token of argv) {
65
+ if (!optionsEnded && token === "--") {
66
+ optionsEnded = true;
67
+ continue;
68
+ }
69
+ if (!optionsEnded && (token === "-h" || token === "--help")) {
70
+ return helpCommand(VERSIONS_HELP);
71
+ }
72
+ throw unexpectedArgument(token, VERSIONS_USAGE);
73
+ }
74
+ return { kind: "versions" };
75
+ }
76
+ function helpCommand(text) {
77
+ return { kind: "print-help", text, stream: "stdout", exitCode: 0 };
78
+ }
79
+ // Version numbers are the small positive integers shown by `versions` (v1,
80
+ // v2, ...). Anything else is a usage mistake, reported like other bad argv —
81
+ // including a digit string too large for an exact integer, which would
82
+ // otherwise serialize in scientific notation and reach the server as
83
+ // something like 1e+23.
84
+ function parseVersionNumber(rawValue) {
85
+ const parsed = Number.parseInt(rawValue, 10);
86
+ if (!/^[0-9]+$/.test(rawValue) || !Number.isSafeInteger(parsed) || parsed < 1) {
87
+ throw new CliError(`error: invalid value '${sanitizeForTerminal(rawValue)}' for '--to': expected a version number like 3\n\n` +
88
+ `${ROLLBACK_USAGE}\n\nFor more information, try '--help'.`, 2);
89
+ }
90
+ return parsed;
91
+ }
92
+ function missingToValue() {
93
+ return new CliError(`error: a value is required for '--to' but none was supplied\n\n${ROLLBACK_USAGE}\n\nFor more information, try '--help'.`, 2);
94
+ }
@@ -0,0 +1,29 @@
1
+ // Argv parsing for driggsby init: one optional positional (the app's name),
2
+ // help, and the shared usage-error shape — usage errors exit 2, echoed argv
3
+ // is sanitized by the shared builders.
4
+ import { unexpectedArgument } from "./args-shared.js";
5
+ import { INIT_HELP } from "./help.js";
6
+ const INIT_USAGE = "Usage: npx driggsby@latest init [NAME]";
7
+ export function parseInit(argv) {
8
+ let slug = null;
9
+ let optionsEnded = false;
10
+ for (const token of argv) {
11
+ if (!optionsEnded && token === "--") {
12
+ optionsEnded = true;
13
+ continue;
14
+ }
15
+ if (!optionsEnded && (token === "-h" || token === "--help")) {
16
+ return { kind: "print-help", text: INIT_HELP, stream: "stdout", exitCode: 0 };
17
+ }
18
+ // Any other flag-looking token is a mistake, not a name; after "--"
19
+ // everything is positional and slug validation refuses bad names.
20
+ if (!optionsEnded && token.startsWith("-") && token !== "-") {
21
+ throw unexpectedArgument(token, INIT_USAGE);
22
+ }
23
+ if (slug !== null) {
24
+ throw unexpectedArgument(token, INIT_USAGE);
25
+ }
26
+ slug = token;
27
+ }
28
+ return { kind: "init", slug };
29
+ }
@@ -0,0 +1,258 @@
1
+ // The mcp setup argv parser, byte-matching the original clap 4.6 behavior it
2
+ // was ported from: pending-argument resolution order, matcher-derived usage
3
+ // lines, duplicate detection, and the "--" escape quirks. New commands do NOT
4
+ // copy this style — clap byte-parity is a frozen contract for the mcp setup
5
+ // surface only.
6
+ import { passAsValueTip, similarArgumentTip, unexpectedArgument, unexpectedFlagValue, } from "./args-shared.js";
7
+ import { didYouMean } from "./clap-suggestions.js";
8
+ import { CliError } from "./cli-error.js";
9
+ import { MCP_SETUP_HELP_LONG, MCP_SETUP_HELP_SHORT } from "./help.js";
10
+ import {} from "./mcp-setup/known-client.js";
11
+ import { sanitizeForTerminal } from "./terminal-text.js";
12
+ const SETUP_USAGE = "Usage: npx driggsby@latest mcp setup [OPTIONS] [CLIENT]";
13
+ const SETUP_LONG_FLAGS = ["print", "help"];
14
+ export function parseMcpSetup(argv) {
15
+ let client;
16
+ let print = false;
17
+ let scope;
18
+ let optionsEnded = false;
19
+ // The matcher state clap re-renders error usage lines from: flags that have
20
+ // RESOLVED into the matcher, in argv order. Unknown-long-flag errors render
21
+ // these (plus <CLIENT> once the positional resolved); "--print=x"/"--help=x"
22
+ // render the derive group form once anything resolved. Shorts, positionals,
23
+ // and duplicate errors keep the static usage. A second slot-form -s removes
24
+ // the earlier "-s <MCP_SCOPE>" entry while its own value is still pending
25
+ // (clap's Set action self-override), and a duplicate never re-adds it.
26
+ const seenFlags = [];
27
+ let committedArgs = 0;
28
+ let clientInMatcher = false;
29
+ let pending = null;
30
+ // Resolution, byte-verified against the Rust binary: recognized argument
31
+ // starts (--print, -s, --help/-h, an accepted positional, end of argv)
32
+ // PROPAGATE the pending arg's errors — duplication first, then emptiness,
33
+ // then value validity — while error paths (unknown long flag) resolve in
34
+ // DISCARD mode: the token's own error wins, but a non-duplicate entry still
35
+ // lands in the matcher and shows up in the rebuilt usage line.
36
+ const resolvePending = (propagateErrors) => {
37
+ if (pending === null) {
38
+ return;
39
+ }
40
+ const resolved = pending;
41
+ pending = null;
42
+ if (resolved.kind === "client") {
43
+ clientInMatcher = true;
44
+ committedArgs += 1;
45
+ return;
46
+ }
47
+ if (resolved.kind === "scope-missing-value") {
48
+ if (propagateErrors) {
49
+ throw scopeValueRequired();
50
+ }
51
+ return;
52
+ }
53
+ if (resolved.isDuplicate) {
54
+ if (propagateErrors) {
55
+ throw usedMultipleTimes("-s <MCP_SCOPE>");
56
+ }
57
+ return;
58
+ }
59
+ seenFlags.push("-s <MCP_SCOPE>");
60
+ committedArgs += 1;
61
+ if (resolved.value === "") {
62
+ if (propagateErrors) {
63
+ throw scopeValueRequired();
64
+ }
65
+ return;
66
+ }
67
+ if (resolved.value !== "local" && resolved.value !== "user") {
68
+ if (propagateErrors) {
69
+ throw invalidScopeValue(resolved.value);
70
+ }
71
+ return;
72
+ }
73
+ scope = resolved.value;
74
+ };
75
+ const acceptPositional = (token) => {
76
+ // clap flags the extra positional eagerly, before resolving the pending
77
+ // arg (codex -s bogus -- y reports 'y', not the invalid scope).
78
+ if (client !== undefined) {
79
+ throw unexpectedArgument(token, SETUP_USAGE);
80
+ }
81
+ resolvePending(true);
82
+ client = token;
83
+ pending = { kind: "client" };
84
+ };
85
+ for (let index = 0; index < argv.length; index += 1) {
86
+ const token = argv[index];
87
+ if (token === undefined) {
88
+ break;
89
+ }
90
+ if (optionsEnded) {
91
+ acceptPositional(token);
92
+ continue;
93
+ }
94
+ if (token === "--") {
95
+ optionsEnded = true;
96
+ continue;
97
+ }
98
+ if (token === "--help") {
99
+ resolvePending(true);
100
+ return { kind: "print-help", text: MCP_SETUP_HELP_LONG, stream: "stdout", exitCode: 0 };
101
+ }
102
+ if (token === "--print") {
103
+ resolvePending(true);
104
+ if (print) {
105
+ throw usedMultipleTimes("--print");
106
+ }
107
+ print = true;
108
+ seenFlags.push("--print");
109
+ committedArgs += 1;
110
+ continue;
111
+ }
112
+ if (token.startsWith("--help=") || token.startsWith("--print=")) {
113
+ // clap computes this error's usage BEFORE resolving the pending arg,
114
+ // so a pending -s or positional never influences the group form.
115
+ const equalsAt = token.indexOf("=");
116
+ const flag = token.slice(0, equalsAt);
117
+ throw unexpectedFlagValue(flag, token.slice(equalsAt + 1), setupEqualsErrorUsage(flag, committedArgs > 0));
118
+ }
119
+ if (token.startsWith("-s")) {
120
+ resolvePending(true);
121
+ if (token === "-s") {
122
+ // A MISSING value (nothing next, or a RECOGNIZED flag token next —
123
+ // "--", exact --help/--print, or a short cluster starting with 'h' or
124
+ // 's') reports "a value is required"; an UNRECOGNIZED dash-leading
125
+ // token abandons this -s and is reported through its own error path.
126
+ const next = argv[index + 1];
127
+ if (next === undefined) {
128
+ throw scopeValueRequired();
129
+ }
130
+ if (next === "--") {
131
+ // clap's escape does not feed -s: it flips trailing-values mode
132
+ // and leaves the -s pending with no value. That error surfaces at
133
+ // the next token — propagated while the [CLIENT] slot is free,
134
+ // discarded in favor of the extra-positional error once taken.
135
+ pending = { kind: "scope-missing-value" };
136
+ continue;
137
+ }
138
+ if (next !== "-" && next.startsWith("-")) {
139
+ if (isRecognizedSetupFlagToken(next)) {
140
+ throw scopeValueRequired();
141
+ }
142
+ continue;
143
+ }
144
+ // Slot-form value (bare "-" included): validation is DEFERRED to
145
+ // resolution. A repeat occurrence also evicts the earlier matcher
146
+ // entry now, while its own value is still pending.
147
+ if (scope !== undefined) {
148
+ const at = seenFlags.indexOf("-s <MCP_SCOPE>");
149
+ if (at !== -1) {
150
+ seenFlags.splice(at, 1);
151
+ }
152
+ }
153
+ pending = { kind: "scope", value: next, isDuplicate: scope !== undefined };
154
+ index += 1;
155
+ continue;
156
+ }
157
+ // Attached form (-s=x / -sx): duplication, emptiness, and validity all
158
+ // report eagerly, in that order.
159
+ const value = token.startsWith("-s=") ? token.slice(3) : token.slice(2);
160
+ if (scope !== undefined) {
161
+ throw usedMultipleTimes("-s <MCP_SCOPE>");
162
+ }
163
+ if (value === "") {
164
+ throw scopeValueRequired();
165
+ }
166
+ if (value !== "local" && value !== "user") {
167
+ throw invalidScopeValue(value);
168
+ }
169
+ scope = value;
170
+ seenFlags.push("-s <MCP_SCOPE>");
171
+ committedArgs += 1;
172
+ continue;
173
+ }
174
+ if (token.startsWith("--")) {
175
+ resolvePending(false);
176
+ throw unknownSetupLongFlag(token, seenFlags, clientInMatcher);
177
+ }
178
+ if (token.startsWith("-") && token.length > 1) {
179
+ // Short cluster: -h... prints the short help (after resolving the
180
+ // pending arg, so -s bogus -h reports the invalid scope); anything else
181
+ // errors naming the first short (one code point, like Rust's char) with
182
+ // the pass-as-value tip (-py → '-p'), discarding any pending error.
183
+ const firstShort = Array.from(token.slice(1))[0] ?? "";
184
+ if (firstShort === "h") {
185
+ resolvePending(true);
186
+ return { kind: "print-help", text: MCP_SETUP_HELP_SHORT, stream: "stdout", exitCode: 0 };
187
+ }
188
+ throw unexpectedArgument(`-${firstShort}`, SETUP_USAGE, passAsValueTip(`-${firstShort}`));
189
+ }
190
+ acceptPositional(token);
191
+ }
192
+ resolvePending(true);
193
+ return { kind: "mcp-setup", client, print, scope };
194
+ }
195
+ function invalidScopeValue(value) {
196
+ const shown = sanitizeForTerminal(value);
197
+ const similar = didYouMean(value, ["local", "user"]);
198
+ const tip = similar === undefined ? "" : `\n\n tip: a similar value exists: '${similar}'`;
199
+ return new CliError(`error: invalid value '${shown}' for '-s <MCP_SCOPE>'\n [possible values: local, user]${tip}\n\nFor more information, try '--help'.`, 2);
200
+ }
201
+ // A dash-leading token clap would recognize as one of mcp setup's own flags:
202
+ // an exact long flag, or a short cluster led by a known short ('h' or 's').
203
+ // Long forms with an attached =value are NOT recognized here — clap reports
204
+ // those through their own unexpected-value error instead — and "--" is
205
+ // handled separately (it parks the -s as pending-with-no-value).
206
+ function isRecognizedSetupFlagToken(token) {
207
+ if (token === "--help" || token === "--print") {
208
+ return true;
209
+ }
210
+ if (token.startsWith("--")) {
211
+ return false;
212
+ }
213
+ const firstShort = Array.from(token.slice(1))[0] ?? "";
214
+ return firstShort === "h" || firstShort === "s";
215
+ }
216
+ function unknownSetupLongFlag(token, seenFlags, clientSeen) {
217
+ const flagName = token.slice(2).split("=", 1)[0] ?? "";
218
+ const shown = sanitizeForTerminal(`--${flagName}`);
219
+ const similar = didYouMean(flagName, SETUP_LONG_FLAGS);
220
+ if (similar !== undefined) {
221
+ // clap appends the suggested flag to the seen list unless already there
222
+ // (--print --pront renders "--print [CLIENT]", not "--print --print ...").
223
+ const rendered = seenFlags.includes(`--${similar}`) ? seenFlags : [...seenFlags, `--${similar}`];
224
+ return unexpectedArgument(shown, setupUsageFromSeen(rendered, clientSeen), similarArgumentTip(similar));
225
+ }
226
+ return unexpectedArgument(shown, setupUsageFromSeen(seenFlags, clientSeen), passAsValueTip(shown));
227
+ }
228
+ // clap re-renders the usage line on unknown-long-flag errors from what it has
229
+ // already parsed: the completed flags in argv order, then <CLIENT> once the
230
+ // positional was consumed ([CLIENT] otherwise). With nothing consumed and no
231
+ // suggestion it falls back to the generic "[OPTIONS] [CLIENT]" form.
232
+ function setupUsageFromSeen(flags, clientSeen) {
233
+ if (flags.length === 0 && !clientSeen) {
234
+ return SETUP_USAGE;
235
+ }
236
+ const parts = ["Usage: npx driggsby@latest mcp setup", ...flags, clientSeen ? "<CLIENT>" : "[CLIENT]"];
237
+ return parts.join(" ");
238
+ }
239
+ // The usage clap renders for "--print=x"/"--help=x" at the setup level: with
240
+ // nothing committed, the errored flag plus "[CLIENT]"; once any setup arg is
241
+ // committed, the derive-generated group "<CLIENT|--print|-s <MCP_SCOPE>>",
242
+ // prefixed by "--help" only (--print is a group member and collapses into it).
243
+ function setupEqualsErrorUsage(flag, anyArgCommitted) {
244
+ if (!anyArgCommitted) {
245
+ return `Usage: npx driggsby@latest mcp setup ${flag} [CLIENT]`;
246
+ }
247
+ const group = "<CLIENT|--print|-s <MCP_SCOPE>>";
248
+ const prefix = flag === "--help" ? "--help " : "";
249
+ return `Usage: npx driggsby@latest mcp setup ${prefix}${group}`;
250
+ }
251
+ function usedMultipleTimes(argumentName) {
252
+ // Callers pass literals only; sanitizing anyway keeps the module-wide
253
+ // no-control-bytes guarantee independent of each call site.
254
+ return new CliError(`error: the argument '${sanitizeForTerminal(argumentName)}' cannot be used multiple times\n\n${SETUP_USAGE}\n\nFor more information, try '--help'.`, 2);
255
+ }
256
+ function scopeValueRequired() {
257
+ return new CliError(`error: a value is required for '-s <MCP_SCOPE>' but none was supplied\n [possible values: local, user]\n\nFor more information, try '--help'.`, 2);
258
+ }
@@ -0,0 +1,28 @@
1
+ // The parsed-command union and the CliError builders shared by the level
2
+ // dispatcher (args.ts) and the mcp setup parser (args-mcp-setup.ts). Every
3
+ // builder sanitizes what it interpolates, so no control bytes can reach the
4
+ // terminal through an echoed argument, tip, or usage line.
5
+ import { CliError } from "./cli-error.js";
6
+ import {} from "./mcp-setup/known-client.js";
7
+ import { sanitizeForTerminal } from "./terminal-text.js";
8
+ // "--print=true" / "--help=x" / "--version=x": these flags take no value.
9
+ export function unexpectedFlagValue(flag, rawValue, usage) {
10
+ const value = sanitizeForTerminal(rawValue);
11
+ return new CliError(`error: unexpected value '${value}' for '${sanitizeForTerminal(flag)}' found; no more were expected\n\n${sanitizeForTerminal(usage)}\n\nFor more information, try '--help'.`, 2);
12
+ }
13
+ export function unexpectedArgument(argument, usage, tipLine) {
14
+ const shown = sanitizeForTerminal(argument);
15
+ // The whole tip line is sanitized here, in one place — its dynamic parts
16
+ // carry user argv, and the sanitizer leaves the line's own spacing alone.
17
+ const tip = tipLine === undefined ? "" : `${sanitizeForTerminal(tipLine)}\n\n`;
18
+ return new CliError(`error: unexpected argument '${shown}' found\n\n${tip}${sanitizeForTerminal(usage)}\n\nFor more information, try '--help'.`, 2);
19
+ }
20
+ export function similarArgumentTip(flagName) {
21
+ return ` tip: a similar argument exists: '--${flagName}'`;
22
+ }
23
+ export function passAsValueTip(shownArgument) {
24
+ return ` tip: to pass '${shownArgument}' as a value, use '-- ${shownArgument}'`;
25
+ }
26
+ export function removeDashesTip(subcommand) {
27
+ return ` tip: subcommand '${subcommand}' exists; to use it, remove the '--' before it`;
28
+ }