driggsby 0.1.41 → 0.2.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 +59 -0
- package/bin/driggsby.js +2 -0
- package/dist/api/base-url.js +31 -0
- package/dist/api/network-error.js +55 -0
- package/dist/args-deploy.js +94 -0
- package/dist/args-init.js +29 -0
- package/dist/args-mcp-setup.js +258 -0
- package/dist/args-shared.js +28 -0
- package/dist/args.js +177 -0
- package/dist/clap-suggestions.js +66 -0
- package/dist/cli-error.js +12 -0
- package/dist/credentials/clear-outcome.js +1 -0
- package/dist/credentials/file-store.js +77 -0
- package/dist/credentials/keychain.js +54 -0
- package/dist/credentials/secret-tool.js +48 -0
- package/dist/credentials/store.js +164 -0
- package/dist/credentials/tool-runner.js +55 -0
- package/dist/deploy/api-session.js +85 -0
- package/dist/deploy/deploy-command.js +126 -0
- package/dist/deploy/rollback-command.js +100 -0
- package/dist/deploy/versions-command.js +97 -0
- package/dist/dev/dev-command.js +106 -0
- package/dist/dev/dev-servers.js +274 -0
- package/dist/dev/host-page.js +147 -0
- package/dist/dev/mcp-broker.js +149 -0
- package/dist/dev/static-files.js +104 -0
- package/dist/dev/tool-allowlist.js +18 -0
- package/dist/dev/watcher.js +76 -0
- package/dist/help.js +180 -0
- package/dist/init/init-command.js +114 -0
- package/dist/init/templates.js +271 -0
- package/dist/login/claim-client.js +151 -0
- package/dist/login/login.js +146 -0
- package/dist/login/logout.js +37 -0
- package/dist/login/open-url.js +79 -0
- package/dist/main.js +57 -0
- package/dist/mcp-setup/classify.js +37 -0
- package/dist/mcp-setup/client-command.js +133 -0
- package/dist/mcp-setup/commands.js +53 -0
- package/dist/mcp-setup/known-client.js +38 -0
- package/dist/mcp-setup/setup.js +137 -0
- package/dist/mcp-setup/spawn-plan.js +91 -0
- package/dist/mcp-setup/text.js +83 -0
- package/dist/terminal-text.js +19 -0
- package/dist/version.js +6 -0
- package/package.json +22 -41
- package/bin/driggsby +0 -10
- package/install.js +0 -159
- package/lib/artifacts.js +0 -49
- 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
|
|
package/bin/driggsby.js
ADDED
|
@@ -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
|
+
}
|