promptdock 1.2.0 → 1.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.
- package/README.md +111 -13
- package/dist/api.d.ts +44 -1
- package/dist/api.js +102 -21
- package/dist/args.js +2 -2
- package/dist/auth.js +7 -2
- package/dist/commands/install.js +47 -13
- package/dist/commands/lifecycle.d.ts +14 -0
- package/dist/commands/lifecycle.js +330 -121
- package/dist/commands/login.js +8 -4
- package/dist/config.d.ts +1 -1
- package/dist/config.js +9 -3
- package/dist/context.d.ts +24 -1
- package/dist/context.js +79 -7
- package/dist/errors.d.ts +87 -0
- package/dist/errors.js +107 -3
- package/dist/generated/constants.d.ts +3 -1
- package/dist/generated/constants.js +3 -1
- package/dist/help.js +47 -11
- package/dist/index.js +46 -7
- package/dist/installer.js +9 -1
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +5 -1
- package/dist/terminal-text.d.ts +213 -0
- package/dist/terminal-text.js +368 -0
- package/dist/ui.js +7 -4
- package/dist/verdicts.d.ts +4 -1
- package/dist/verdicts.js +25 -9
- package/package.json +5 -4
package/dist/context.js
CHANGED
|
@@ -88,15 +88,87 @@ export function readOwnVersion() {
|
|
|
88
88
|
return "0.0.0";
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
/**
|
|
91
|
+
/**
|
|
92
|
+
* The ONLY shape allowed to reach a platform opener, or null to refuse.
|
|
93
|
+
*
|
|
94
|
+
* ⚠️ THE URL IS UNTRUSTED. `verification_url` comes from the UNAUTHENTICATED
|
|
95
|
+
* `/api/v1/cli/auth/start` response through `Api.request`, which is `res.body as T` — a
|
|
96
|
+
* bare cast, as contract.ts says in its own header. The OS opener is a far more
|
|
97
|
+
* dangerous sink than the terminal: it is the one path in this CLI with an effect
|
|
98
|
+
* outside the process.
|
|
99
|
+
*
|
|
100
|
+
* Measured on darwin: `open` treats a non-URL argument as a FILE PATH (so a value like
|
|
101
|
+
* `/Applications/Calculator.app` launches it), reads a leading `-` as a FLAG, and
|
|
102
|
+
* dispatches any scheme to its registered handler — which is how `file:`, `smb:` and
|
|
103
|
+
* `ms-msdt:` become interesting.
|
|
104
|
+
*/
|
|
105
|
+
export function openableUrl(raw) {
|
|
106
|
+
let u;
|
|
107
|
+
try {
|
|
108
|
+
u = new URL(raw);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null; // not a URL at all — a UNC path and a bare `-h` both land here
|
|
112
|
+
}
|
|
113
|
+
const loopback = u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "[::1]";
|
|
114
|
+
// Mirrors config.ts's TLS floor (`assertSecureBase`). Blocks file:, smb:, javascript:,
|
|
115
|
+
// vbscript: and every app-registered scheme from reaching the platform opener.
|
|
116
|
+
if (u.protocol !== "https:" && !(u.protocol === "http:" && loopback))
|
|
117
|
+
return null;
|
|
118
|
+
return u.toString();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The `[command, args]` a platform open would spawn, or null when it is refused.
|
|
122
|
+
*
|
|
123
|
+
* Split out from the spawn PURELY so the decision is testable: the whole matrix can be
|
|
124
|
+
* asserted with no child process, on any host OS. There was no test referencing
|
|
125
|
+
* `openUrlBestEffort` at all, which is how the win32 defect below survived.
|
|
126
|
+
*/
|
|
127
|
+
export function openUrlCommand(url, platform) {
|
|
128
|
+
const safe = openableUrl(url);
|
|
129
|
+
if (!safe)
|
|
130
|
+
return null;
|
|
131
|
+
if (platform === "darwin") {
|
|
132
|
+
// `--` ends option parsing: measured, `open` reads a leading-dash first argument as
|
|
133
|
+
// a flag. Belt-and-braces over the scheme gate above, which a future edit could drift
|
|
134
|
+
// from.
|
|
135
|
+
return ["open", ["--", safe]];
|
|
136
|
+
}
|
|
137
|
+
if (platform === "win32") {
|
|
138
|
+
// ⚠️ NOT `cmd /c start`, WHICH WAS A COMMAND-INJECTION PRIMITIVE.
|
|
139
|
+
//
|
|
140
|
+
// libuv quotes arguments for the MSVCRT argv parser (`quote_cmd_arg`, src/win/
|
|
141
|
+
// process.c) and copies any argument containing no space, tab or quote VERBATIM.
|
|
142
|
+
// cmd.exe then parses that string by ITS OWN rules, where `&`, `|` and `^` are
|
|
143
|
+
// command separators and `\"` is not an escape. So a perfectly well-formed https URL
|
|
144
|
+
// — `https://host/cli-auth?code=A&calc` — reached cmd.exe as two commands.
|
|
145
|
+
//
|
|
146
|
+
// ⚠️ AND A SCHEME CHECK ALONE DOES NOT CLOSE IT: `new URL(u).toString()`
|
|
147
|
+
// percent-encodes space, quotes and control characters but PRESERVES `&`, `|` and
|
|
148
|
+
// `^`, and `&` is ordinary in a query string — so no URL-shape validation short of a
|
|
149
|
+
// character allowlist (which would break legitimate multi-parameter URLs) helps. The
|
|
150
|
+
// fix has to be not going through cmd.exe.
|
|
151
|
+
//
|
|
152
|
+
// This is the BatBadBut root cause (CVE-2024-27980). Node's fix does NOT cover it:
|
|
153
|
+
// that throws EINVAL only when the spawn TARGET is a .bat/.cmd file, and the target
|
|
154
|
+
// here was cmd.exe itself — so no Node version protected this call shape.
|
|
155
|
+
//
|
|
156
|
+
// explorer.exe is a normal PE, so libuv's CRT-style quoting is the CORRECT quoting
|
|
157
|
+
// for it and no second parser ever sees the string. (It always exits 1; the status
|
|
158
|
+
// was already ignored.)
|
|
159
|
+
return ["explorer.exe", [safe]];
|
|
160
|
+
}
|
|
161
|
+
return ["xdg-open", ["--", safe]];
|
|
162
|
+
}
|
|
163
|
+
/** Best-effort platform browser open (darwin `open`, win32 `explorer`, else xdg-open). */
|
|
92
164
|
export function openUrlBestEffort(url, platform) {
|
|
165
|
+
const decided = openUrlCommand(url, platform);
|
|
166
|
+
// Refusing is SAFE and silent by design: auth.ts has already PRINTED the URL and tells
|
|
167
|
+
// the user to paste it if the browser did not open.
|
|
168
|
+
if (!decided)
|
|
169
|
+
return;
|
|
93
170
|
try {
|
|
94
|
-
const [cmd, args] =
|
|
95
|
-
? ["open", [url]]
|
|
96
|
-
: platform === "win32"
|
|
97
|
-
? // `start` is a cmd.exe builtin; the empty "" is its window-title slot.
|
|
98
|
-
["cmd", ["/c", "start", "", url]]
|
|
99
|
-
: ["xdg-open", [url]];
|
|
171
|
+
const [cmd, args] = decided;
|
|
100
172
|
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
101
173
|
child.on("error", () => undefined);
|
|
102
174
|
child.unref();
|
package/dist/errors.d.ts
CHANGED
|
@@ -19,6 +19,14 @@ export declare const EXIT: {
|
|
|
19
19
|
readonly INTERRUPT: 130;
|
|
20
20
|
};
|
|
21
21
|
export type ExitCode = (typeof EXIT)[keyof typeof EXIT];
|
|
22
|
+
/** The error catalogue itself — the anchor-less landing page for a failure with no slug.
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ DERIVED from `CANONICAL_ORIGIN`, not a literal, and the reason is not tidiness:
|
|
25
|
+
* this URL is printed under EVERY error — including the one that says the apex redirect
|
|
26
|
+
* drops your body and your token. It shipped naming the apex, so that error handed the
|
|
27
|
+
* user a redirecting link while explaining that redirects break things. It also rides
|
|
28
|
+
* `toFailureJson`'s `doc_url`, which a `--json` consumer can fetch. */
|
|
29
|
+
export declare const CLI_ERRORS_URL = "https://www.promptdock.ai/docs/cli/errors";
|
|
22
30
|
/** DX3: every named error footer links its docs anchor. */
|
|
23
31
|
export declare function footerUrl(code: string): string;
|
|
24
32
|
export declare class CliError extends Error {
|
|
@@ -27,11 +35,90 @@ export declare class CliError extends Error {
|
|
|
27
35
|
readonly footer?: string;
|
|
28
36
|
/** one-line "what to do" printed after the message */
|
|
29
37
|
readonly hint?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Stable machine-readable slug for `--json` consumers (`error.code`).
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ DERIVED from `footer`, with no constructor option of its own — deliberately.
|
|
42
|
+
* Two independently-set fields could disagree, and one of them is the docs anchor a
|
|
43
|
+
* stuck user clicks. It also keeps ONE source of slugs in this package, which is what
|
|
44
|
+
* `lib/cli/error-docs.test.ts` scans for (it reads the literal slug off every
|
|
45
|
+
* construction site) to prove every printed anchor has a page entry; a second slug
|
|
46
|
+
* channel would be invisible to that guard.
|
|
47
|
+
*
|
|
48
|
+
* The 9 CliErrors that carry no footer (base-url config, the batch roll-up, the
|
|
49
|
+
* picker's Cancelled/Interrupted) report `"error"` and point at the index page.
|
|
50
|
+
*/
|
|
51
|
+
readonly code: string;
|
|
52
|
+
/**
|
|
53
|
+
* ⚠️ `message` and `hint` are passed through {@link terminalBlock} — the
|
|
54
|
+
* defence-in-depth BACKSTOP for terminal-control injection, not the primary control.
|
|
55
|
+
*
|
|
56
|
+
* The primary control is `terminalLine` at the trust boundary (api.ts's
|
|
57
|
+
* `Api.envelope`, and the 426 remedy), which is where an untrusted fragment is still
|
|
58
|
+
* identifiable as untrusted and can be bounded and collapsed to one line. This
|
|
59
|
+
* constructor cannot tell a server's prose from the CLI's own, so it only strips what
|
|
60
|
+
* is never legitimate ANYWHERE: escape introducers (C0 except newline, DEL, C1) and
|
|
61
|
+
* bidi/zero-width code points.
|
|
62
|
+
*
|
|
63
|
+
* It earns its place by covering what a boundary fix structurally cannot — the NEXT
|
|
64
|
+
* call site. A future command that reads a new server field and throws a CliError
|
|
65
|
+
* built from it is repaint-safe on the day it is written, which is precisely the
|
|
66
|
+
* failure this whole change exists to close.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ NEWLINE SURVIVES AND THERE IS NO LENGTH CAP, both deliberate and both load-
|
|
69
|
+
* bearing — see `terminalBlock`. Adding either here silently breaks the 426's
|
|
70
|
+
* two-line layout and truncates the `--json` batch roll-up.
|
|
71
|
+
*/
|
|
30
72
|
constructor(message: string, exitCode: ExitCode, opts?: {
|
|
31
73
|
footer?: string;
|
|
32
74
|
hint?: string;
|
|
33
75
|
});
|
|
34
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The `--json` shape of a TOP-LEVEL failure (Vercel's `{status, reason, hint, next}`
|
|
79
|
+
* lineage, keyed to this CLI's vocabulary). Stable: keys may be added in a patch,
|
|
80
|
+
* existing ones never change meaning — the same promise README makes for `results` rows.
|
|
81
|
+
*/
|
|
82
|
+
export type CliFailure = {
|
|
83
|
+
error: {
|
|
84
|
+
/** the docs anchor slug, or "error" when the failure has none */
|
|
85
|
+
code: string;
|
|
86
|
+
message: string;
|
|
87
|
+
exit_code: ExitCode;
|
|
88
|
+
/** always present — the anchor when there is one, else the errors index */
|
|
89
|
+
doc_url: string;
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* ONE place that turns any thrown value into the failure envelope, so the exit code a
|
|
94
|
+
* script reads and the exit code the process returns cannot drift (index.ts returns
|
|
95
|
+
* `failure.error.exit_code`).
|
|
96
|
+
*
|
|
97
|
+
* ⚠️ The `footerUrl` call below, with its inline `internal` slug, is load-bearing beyond
|
|
98
|
+
* this function: `lib/cli/error-docs.test.ts` scans packages/cli/src for that call shape
|
|
99
|
+
* and asserts it finds `internal` — an assertion that guards the SCANNER itself. If this
|
|
100
|
+
* moves, the literal call must stay somewhere under src/.
|
|
101
|
+
*
|
|
102
|
+
* ⚠️ AND THAT SCANNER IS A PLAIN TEXT SCAN, COMMENTS INCLUDED. Spelling either slug
|
|
103
|
+
* pattern out in prose registers a slug that has no docs entry and fails the guard the
|
|
104
|
+
* prose is explaining (it cost one red run writing this file). Name the channels
|
|
105
|
+
* indirectly, as above — never paste the shape.
|
|
106
|
+
*/
|
|
107
|
+
export declare function toFailureJson(err: unknown): CliFailure;
|
|
108
|
+
/**
|
|
109
|
+
* Does this invocation want machine-readable output? Read from RAW argv, never from the
|
|
110
|
+
* parsed flags.
|
|
111
|
+
*
|
|
112
|
+
* ⚠️ `parseArgs` runs INSIDE index.ts's `try`, so `flags` does not exist in the `catch`
|
|
113
|
+
* at all — and a malformed invocation (`install a/b --json --bogus`) throws before it
|
|
114
|
+
* ever could. Those are precisely the failures a CI job most needs machine-readable, so
|
|
115
|
+
* the detection has to be independent of parsing succeeding.
|
|
116
|
+
*
|
|
117
|
+
* `--json` is boolean-only in COMMAND_SPECS (`--json=true` is a usage error), and
|
|
118
|
+
* everything after a bare `--` is a positional, so an exact scan of the pre-`--` slice
|
|
119
|
+
* is the whole rule.
|
|
120
|
+
*/
|
|
121
|
+
export declare function wantsJsonOutput(argv: readonly string[]): boolean;
|
|
35
122
|
export declare function usageError(message: string, hint?: string): CliError;
|
|
36
123
|
/**
|
|
37
124
|
* DX3 per-OS filesystem error mapping. THREE distinct messages; the --force
|
package/dist/errors.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CANONICAL_ORIGIN } from "./generated/constants.js";
|
|
2
|
+
import { SERVER_MESSAGE_MAX, terminalBlock, terminalLine } from "./terminal-text.js";
|
|
1
3
|
// Stable exit codes (DX3) — scripts and CI depend on these; never renumber.
|
|
2
4
|
export const EXIT = {
|
|
3
5
|
OK: 0,
|
|
@@ -19,9 +21,17 @@ export const EXIT = {
|
|
|
19
21
|
* in-UI cancel) stays USAGE=1. Scripts never reach pickers (TTY-only). */
|
|
20
22
|
INTERRUPT: 130,
|
|
21
23
|
};
|
|
24
|
+
/** The error catalogue itself — the anchor-less landing page for a failure with no slug.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ DERIVED from `CANONICAL_ORIGIN`, not a literal, and the reason is not tidiness:
|
|
27
|
+
* this URL is printed under EVERY error — including the one that says the apex redirect
|
|
28
|
+
* drops your body and your token. It shipped naming the apex, so that error handed the
|
|
29
|
+
* user a redirecting link while explaining that redirects break things. It also rides
|
|
30
|
+
* `toFailureJson`'s `doc_url`, which a `--json` consumer can fetch. */
|
|
31
|
+
export const CLI_ERRORS_URL = `${CANONICAL_ORIGIN}/docs/cli/errors`;
|
|
22
32
|
/** DX3: every named error footer links its docs anchor. */
|
|
23
33
|
export function footerUrl(code) {
|
|
24
|
-
return
|
|
34
|
+
return `${CLI_ERRORS_URL}#${code}`;
|
|
25
35
|
}
|
|
26
36
|
export class CliError extends Error {
|
|
27
37
|
exitCode;
|
|
@@ -29,14 +39,108 @@ export class CliError extends Error {
|
|
|
29
39
|
footer;
|
|
30
40
|
/** one-line "what to do" printed after the message */
|
|
31
41
|
hint;
|
|
42
|
+
/**
|
|
43
|
+
* Stable machine-readable slug for `--json` consumers (`error.code`).
|
|
44
|
+
*
|
|
45
|
+
* ⚠️ DERIVED from `footer`, with no constructor option of its own — deliberately.
|
|
46
|
+
* Two independently-set fields could disagree, and one of them is the docs anchor a
|
|
47
|
+
* stuck user clicks. It also keeps ONE source of slugs in this package, which is what
|
|
48
|
+
* `lib/cli/error-docs.test.ts` scans for (it reads the literal slug off every
|
|
49
|
+
* construction site) to prove every printed anchor has a page entry; a second slug
|
|
50
|
+
* channel would be invisible to that guard.
|
|
51
|
+
*
|
|
52
|
+
* The 9 CliErrors that carry no footer (base-url config, the batch roll-up, the
|
|
53
|
+
* picker's Cancelled/Interrupted) report `"error"` and point at the index page.
|
|
54
|
+
*/
|
|
55
|
+
code;
|
|
56
|
+
/**
|
|
57
|
+
* ⚠️ `message` and `hint` are passed through {@link terminalBlock} — the
|
|
58
|
+
* defence-in-depth BACKSTOP for terminal-control injection, not the primary control.
|
|
59
|
+
*
|
|
60
|
+
* The primary control is `terminalLine` at the trust boundary (api.ts's
|
|
61
|
+
* `Api.envelope`, and the 426 remedy), which is where an untrusted fragment is still
|
|
62
|
+
* identifiable as untrusted and can be bounded and collapsed to one line. This
|
|
63
|
+
* constructor cannot tell a server's prose from the CLI's own, so it only strips what
|
|
64
|
+
* is never legitimate ANYWHERE: escape introducers (C0 except newline, DEL, C1) and
|
|
65
|
+
* bidi/zero-width code points.
|
|
66
|
+
*
|
|
67
|
+
* It earns its place by covering what a boundary fix structurally cannot — the NEXT
|
|
68
|
+
* call site. A future command that reads a new server field and throws a CliError
|
|
69
|
+
* built from it is repaint-safe on the day it is written, which is precisely the
|
|
70
|
+
* failure this whole change exists to close.
|
|
71
|
+
*
|
|
72
|
+
* ⚠️ NEWLINE SURVIVES AND THERE IS NO LENGTH CAP, both deliberate and both load-
|
|
73
|
+
* bearing — see `terminalBlock`. Adding either here silently breaks the 426's
|
|
74
|
+
* two-line layout and truncates the `--json` batch roll-up.
|
|
75
|
+
*/
|
|
32
76
|
constructor(message, exitCode, opts) {
|
|
33
|
-
super(message);
|
|
77
|
+
super(terminalBlock(message));
|
|
34
78
|
this.name = "CliError";
|
|
35
79
|
this.exitCode = exitCode;
|
|
36
80
|
this.footer = opts?.footer;
|
|
37
|
-
this.hint = opts?.hint;
|
|
81
|
+
this.hint = opts?.hint === undefined ? undefined : terminalBlock(opts.hint);
|
|
82
|
+
this.code = opts?.footer ?? "error";
|
|
38
83
|
}
|
|
39
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* ONE place that turns any thrown value into the failure envelope, so the exit code a
|
|
87
|
+
* script reads and the exit code the process returns cannot drift (index.ts returns
|
|
88
|
+
* `failure.error.exit_code`).
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ The `footerUrl` call below, with its inline `internal` slug, is load-bearing beyond
|
|
91
|
+
* this function: `lib/cli/error-docs.test.ts` scans packages/cli/src for that call shape
|
|
92
|
+
* and asserts it finds `internal` — an assertion that guards the SCANNER itself. If this
|
|
93
|
+
* moves, the literal call must stay somewhere under src/.
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ AND THAT SCANNER IS A PLAIN TEXT SCAN, COMMENTS INCLUDED. Spelling either slug
|
|
96
|
+
* pattern out in prose registers a slug that has no docs entry and fails the guard the
|
|
97
|
+
* prose is explaining (it cost one red run writing this file). Name the channels
|
|
98
|
+
* indirectly, as above — never paste the shape.
|
|
99
|
+
*/
|
|
100
|
+
export function toFailureJson(err) {
|
|
101
|
+
if (err instanceof CliError) {
|
|
102
|
+
return {
|
|
103
|
+
error: {
|
|
104
|
+
code: err.code,
|
|
105
|
+
message: err.message,
|
|
106
|
+
exit_code: err.exitCode,
|
|
107
|
+
doc_url: err.footer ? footerUrl(err.footer) : CLI_ERRORS_URL,
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// ⚠️ THE ONE SINK WITH NO BACKSTOP. Everything else that reaches the terminal is
|
|
112
|
+
// either a CliError (sanitized in its constructor) or a display site that now calls
|
|
113
|
+
// `terminalLine`. This branch is neither: the thrown value is arbitrary — a library's
|
|
114
|
+
// Error message, a rejected fetch, a string from anywhere — and index.ts prints it.
|
|
115
|
+
// Bounded as well as stripped, since a stack-shaped message can run for pages.
|
|
116
|
+
const detail = terminalLine(err instanceof Error ? err.message : String(err), SERVER_MESSAGE_MAX);
|
|
117
|
+
return {
|
|
118
|
+
error: {
|
|
119
|
+
code: "internal",
|
|
120
|
+
message: `unexpected error: ${detail}`,
|
|
121
|
+
exit_code: EXIT.USAGE,
|
|
122
|
+
doc_url: footerUrl("internal"),
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Does this invocation want machine-readable output? Read from RAW argv, never from the
|
|
128
|
+
* parsed flags.
|
|
129
|
+
*
|
|
130
|
+
* ⚠️ `parseArgs` runs INSIDE index.ts's `try`, so `flags` does not exist in the `catch`
|
|
131
|
+
* at all — and a malformed invocation (`install a/b --json --bogus`) throws before it
|
|
132
|
+
* ever could. Those are precisely the failures a CI job most needs machine-readable, so
|
|
133
|
+
* the detection has to be independent of parsing succeeding.
|
|
134
|
+
*
|
|
135
|
+
* `--json` is boolean-only in COMMAND_SPECS (`--json=true` is a usage error), and
|
|
136
|
+
* everything after a bare `--` is a positional, so an exact scan of the pre-`--` slice
|
|
137
|
+
* is the whole rule.
|
|
138
|
+
*/
|
|
139
|
+
export function wantsJsonOutput(argv) {
|
|
140
|
+
const sep = argv.indexOf("--");
|
|
141
|
+
const scanned = sep === -1 ? argv : argv.slice(0, sep);
|
|
142
|
+
return scanned.includes("--json");
|
|
143
|
+
}
|
|
40
144
|
export function usageError(message, hint) {
|
|
41
145
|
return new CliError(message, EXIT.USAGE, { hint, footer: "usage" });
|
|
42
146
|
}
|
|
@@ -13,7 +13,9 @@ export declare const CLI_FUSE_HISTORY: {
|
|
|
13
13
|
export declare const MAX_SKILL_IMAGES = 5;
|
|
14
14
|
export declare const SKILL_REVEAL_DAILY_CAP = 15;
|
|
15
15
|
export declare const CLI_MIN_VERSION = "1.0.1";
|
|
16
|
-
export declare const PUBLISHED_CLI_VERSION = "1.
|
|
16
|
+
export declare const PUBLISHED_CLI_VERSION = "1.2.0";
|
|
17
17
|
export declare const SKILL_SLUG_RE_SOURCE = "^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$";
|
|
18
18
|
export declare const SKILL_HANDLE_RE_SOURCE = "^[a-z0-9][a-z0-9_-]{0,62}$";
|
|
19
19
|
export declare const CANONICAL_INSTALL_COMMAND = "npx promptdock@latest install";
|
|
20
|
+
export declare const CANONICAL_ORIGIN = "https://www.promptdock.ai";
|
|
21
|
+
export declare const CANONICAL_CONTACT_PATH = "/contact";
|
|
@@ -10,7 +10,9 @@ export const CLI_FUSE_HISTORY = [{ "cli_version": "0.0.0", "max_files": 25, "max
|
|
|
10
10
|
export const MAX_SKILL_IMAGES = 5;
|
|
11
11
|
export const SKILL_REVEAL_DAILY_CAP = 15;
|
|
12
12
|
export const CLI_MIN_VERSION = "1.0.1";
|
|
13
|
-
export const PUBLISHED_CLI_VERSION = "1.
|
|
13
|
+
export const PUBLISHED_CLI_VERSION = "1.2.0";
|
|
14
14
|
export const SKILL_SLUG_RE_SOURCE = "^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$";
|
|
15
15
|
export const SKILL_HANDLE_RE_SOURCE = "^[a-z0-9][a-z0-9_-]{0,62}$";
|
|
16
16
|
export const CANONICAL_INSTALL_COMMAND = "npx promptdock@latest install";
|
|
17
|
+
export const CANONICAL_ORIGIN = "https://www.promptdock.ai";
|
|
18
|
+
export const CANONICAL_CONTACT_PATH = "/contact";
|
package/dist/help.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
// DX4: `promptdock --help` is a COMPLETE zero-config map; every command also
|
|
2
2
|
// answers `promptdock <cmd> --help`.
|
|
3
|
-
import { CANONICAL_INSTALL_COMMAND } from "./generated/constants.js";
|
|
3
|
+
import { CANONICAL_CONTACT_PATH, CANONICAL_INSTALL_COMMAND, CANONICAL_ORIGIN } from "./generated/constants.js";
|
|
4
4
|
import { TARGETS } from "./registry.js";
|
|
5
|
+
/**
|
|
6
|
+
* ⚠️ THE HELP FOOTER NAMES URLs, NEVER AN EMAIL ADDRESS — and that is the whole design,
|
|
7
|
+
* not a style preference. A published binary is frozen on someone's disk forever: an
|
|
8
|
+
* address baked into 1.2.1 is still being printed years after the mailbox moves, with no
|
|
9
|
+
* way to correct it. A URL is indirection: the PAGE decides which mailbox is current, so
|
|
10
|
+
* support routing changes without an npm publish. (The single exception is
|
|
11
|
+
* package.json's `bugs.email`, which npm's metadata schema has no URL form for — and
|
|
12
|
+
* that field IS re-published with every release.)
|
|
13
|
+
*
|
|
14
|
+
* `CANONICAL_CONTACT_PATH` comes from lib/validation/skills-constants.json via
|
|
15
|
+
* `pnpm run gen:cli-constants`, so the web route and this footer cannot drift apart.
|
|
16
|
+
* These lines are also captured verbatim into lib/cli/reference.generated.json when
|
|
17
|
+
* PUBLISHED_CLI_VERSION catches up — /docs/cli/reference keeps showing 1.2.0's footer
|
|
18
|
+
* (no Support line) until then, by design.
|
|
19
|
+
*/
|
|
20
|
+
/** ⚠️ DERIVED from `CANONICAL_ORIGIN` (same shared JSON as CANONICAL_CONTACT_PATH above),
|
|
21
|
+
* so the Docs / Errors / Support lines cannot name a host the API is not served on. */
|
|
22
|
+
const SITE = CANONICAL_ORIGIN;
|
|
5
23
|
export function globalHelp(version) {
|
|
6
24
|
const targets = TARGETS.map((t) => ` ${t.id.padEnd(9)} ${t.localBase}/<slug> (-g: ~/${t.globalBase}/<slug>)${t.experimental ? " (experimental)" : ""}`).join("\n");
|
|
7
25
|
return `promptdock v${version} — install AI agent skills from promptdock.ai
|
|
@@ -11,7 +29,7 @@ export function globalHelp(version) {
|
|
|
11
29
|
|
|
12
30
|
Commands
|
|
13
31
|
login [--token pdk_…] log in (browser hand-off, or store a CI token)
|
|
14
|
-
logout
|
|
32
|
+
logout log out (revokes the session server-side, clears the local token)
|
|
15
33
|
whoami [--json] show the signed-in account
|
|
16
34
|
install <ref> [options] install a skill (ref = handle/slug, @handle/slug, or a pasted URL)
|
|
17
35
|
uninstall <ref|--all> remove an installed skill (receipt-driven; never touches unmanaged files)
|
|
@@ -33,19 +51,23 @@ export function globalHelp(version) {
|
|
|
33
51
|
--check (update) report available updates without applying
|
|
34
52
|
--all operate across project-local AND global scopes
|
|
35
53
|
-g, --global operate on the global scope (bare commands are project-local)
|
|
54
|
+
--allow-partial exit 0 even when an item failed (default: exit non-zero)
|
|
36
55
|
|
|
37
56
|
Targets
|
|
38
57
|
${targets}
|
|
39
58
|
|
|
40
59
|
Environment
|
|
41
|
-
PROMPTDOCK_TOKEN bearer token
|
|
60
|
+
PROMPTDOCK_TOKEN CI bearer token (Settings → Account → CLI sessions)
|
|
42
61
|
PROMPTDOCK_API_BASE API origin override (default https://www.promptdock.ai)
|
|
43
62
|
NO_COLOR disable colored output
|
|
44
63
|
|
|
45
64
|
Exit codes
|
|
46
|
-
0 ok · 1 usage · 2 auth · 3 denied/gated · 4 integrity · 5 filesystem
|
|
65
|
+
0 ok · 1 usage · 2 auth · 3 denied/gated · 4 integrity · 5 filesystem
|
|
66
|
+
6 network · 130 interrupted (Ctrl-C in a prompt; Esc cancels with 1)
|
|
47
67
|
|
|
48
|
-
Docs:
|
|
68
|
+
Docs: ${SITE}/docs/cli
|
|
69
|
+
Errors: ${SITE}/docs/cli/errors
|
|
70
|
+
Support: ${SITE}${CANONICAL_CONTACT_PATH}`;
|
|
49
71
|
}
|
|
50
72
|
export const COMMAND_HELP = {
|
|
51
73
|
login: `promptdock login [--token pdk_…]
|
|
@@ -55,8 +77,12 @@ you approve there, and the terminal continues. --token stores a token minted in
|
|
|
55
77
|
Settings → Account → CLI sessions (the CI path; or set PROMPTDOCK_TOKEN).`,
|
|
56
78
|
logout: `promptdock logout
|
|
57
79
|
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
Revokes the session server-side FIRST — the token this machine holds stops
|
|
81
|
+
working immediately — then removes the local copy from ~/.promptdock/config.json.
|
|
82
|
+
|
|
83
|
+
If the revoke cannot reach the server (offline), the local token is still
|
|
84
|
+
removed and the CLI says so on stderr: that session stays valid until it
|
|
85
|
+
expires, so revoke it at Settings → Account → CLI sessions.`,
|
|
60
86
|
whoami: `promptdock whoami [--json]
|
|
61
87
|
|
|
62
88
|
Shows the signed-in handle, role, and token expiry.`,
|
|
@@ -71,16 +97,26 @@ authenticate via PROMPTDOCK_TOKEN. -y skips the prompts ONLY — a non-empty
|
|
|
71
97
|
foreign directory still requires --force, and scope stays project-local
|
|
72
98
|
unless -g is passed. --dry-run resolves and prints the plan without
|
|
73
99
|
installing (and without spending any premium-unlock slot).`,
|
|
74
|
-
uninstall: `promptdock uninstall <handle>/<slug> | --all [-g] [--target <tool>] [--dir <path>] [--force] [-y] [--json]
|
|
100
|
+
uninstall: `promptdock uninstall <handle>/<slug> | --all [-g] [--target <tool>] [--dir <path>] [--force] [-y] [--allow-partial] [--json]
|
|
75
101
|
|
|
76
102
|
Removes an installed skill using its .promptdock.json receipt — only files the
|
|
77
103
|
receipt lists are ever deleted. Locally-modified files block removal without
|
|
78
|
-
--force (copy your changes out first)
|
|
79
|
-
|
|
104
|
+
--force (copy your changes out first).
|
|
105
|
+
|
|
106
|
+
Across a batch (--all, or a scope holding several skills) one bad item no
|
|
107
|
+
longer aborts the rest: the others are still removed and the command exits
|
|
108
|
+
with that item's code at the end. --allow-partial exits 0 instead. Declining
|
|
109
|
+
a confirm is not a failure.`,
|
|
110
|
+
update: `promptdock update [<handle>/<slug>] [--all] [--check] [-g] [-y] [--force] [--allow-partial] [--json]
|
|
80
111
|
|
|
81
112
|
Updates installed skills to the latest approved version. Bare \`update\` covers
|
|
82
113
|
the project-local scope; -g the global scope; --all both. --check reports
|
|
83
|
-
without applying. Locally-modified files block an update without --force
|
|
114
|
+
without applying. Locally-modified files block an update without --force.
|
|
115
|
+
|
|
116
|
+
An item that FAILS (unreachable, gated, blocked by local edits) no longer
|
|
117
|
+
stops the batch — the rest still update — but the command exits non-zero at
|
|
118
|
+
the end so CI cannot go green on an update that did nothing. --allow-partial
|
|
119
|
+
exits 0 instead. "Up to date" and a declined confirm are not failures.`,
|
|
84
120
|
list: `promptdock list [--json] [-g] [--all]
|
|
85
121
|
|
|
86
122
|
Lists installed skills from their receipts: ref, version, target, installed-at.`,
|
package/dist/index.js
CHANGED
|
@@ -3,15 +3,46 @@
|
|
|
3
3
|
// Contract: app/api/v1/cli/** (server), docs/plans/skills-marketplace.md.
|
|
4
4
|
import { parseArgs } from "./args.js";
|
|
5
5
|
import { realContext } from "./context.js";
|
|
6
|
-
import { CliError, EXIT, footerUrl } from "./errors.js";
|
|
6
|
+
import { CliError, EXIT, footerUrl, toFailureJson, wantsJsonOutput } from "./errors.js";
|
|
7
7
|
import { COMMAND_HELP, globalHelp } from "./help.js";
|
|
8
8
|
import { colors } from "./ui.js";
|
|
9
9
|
import { runInstall } from "./commands/install.js";
|
|
10
10
|
import { runLogin, runLogout, runWhoami } from "./commands/login.js";
|
|
11
11
|
import { runList, runUninstall, runUpdate } from "./commands/lifecycle.js";
|
|
12
|
+
/**
|
|
13
|
+
* SINGLE-OWNER stdout serialization for `--json`.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ THE BUG THIS PREVENTS: `update`/`uninstall` flush their complete report from a
|
|
16
|
+
* `finally` and THEN throw (lifecycle.ts — a failing item must never cost the caller the
|
|
17
|
+
* list of what did succeed). A top-level catch that unconditionally printed its own
|
|
18
|
+
* `{error:…}` would put TWO JSON documents on stdout, and `jq` reads exactly one — so
|
|
19
|
+
* the fix for "failures are invisible under --json" would have broken every batch
|
|
20
|
+
* consumer that already works (`batch-exit.test.ts` does a bare `JSON.parse(stdout)`).
|
|
21
|
+
*
|
|
22
|
+
* So stdout has one owner per run: whoever writes first keeps it. This wrapper records
|
|
23
|
+
* whether anything reached stdout at all, which is exactly the right predicate because
|
|
24
|
+
* under `--json` every stdout write in this CLI IS a complete document followed by a
|
|
25
|
+
* return (install.ts, lifecycle.ts, login.ts all gate their human progress lines on
|
|
26
|
+
* `!json`). `write` is tracked too: the countdown repaint is TTY-only, but a partial
|
|
27
|
+
* line on stdout means the same thing — the stream is no longer ours to own.
|
|
28
|
+
*/
|
|
29
|
+
function trackStdout(base) {
|
|
30
|
+
let used = false;
|
|
31
|
+
const mark = (fn) => (s) => {
|
|
32
|
+
used = true;
|
|
33
|
+
return fn(s);
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
ctx: { ...base, io: { ...base.io, out: mark(base.io.out), write: mark(base.io.write) } },
|
|
37
|
+
used: () => used,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
12
40
|
async function main() {
|
|
13
41
|
const argv = process.argv.slice(2);
|
|
14
|
-
|
|
42
|
+
// From RAW argv: `flags` is scoped to the try below and a parse error throws before it
|
|
43
|
+
// exists, yet a malformed invocation is exactly what CI needs machine-readable.
|
|
44
|
+
const json = wantsJsonOutput(argv);
|
|
45
|
+
const { ctx, used: stdoutUsed } = trackStdout(realContext(argv));
|
|
15
46
|
const c = colors(ctx.env, Boolean(process.stderr.isTTY));
|
|
16
47
|
try {
|
|
17
48
|
const { command, positionals, flags } = parseArgs(argv);
|
|
@@ -55,18 +86,26 @@ async function main() {
|
|
|
55
86
|
}
|
|
56
87
|
}
|
|
57
88
|
catch (err) {
|
|
89
|
+
const failure = toFailureJson(err);
|
|
90
|
+
// Human text ALWAYS goes to stderr, `--json` or not: under --json stdout is reserved
|
|
91
|
+
// for at most one machine-readable document, and a person watching a piped run still
|
|
92
|
+
// needs to see what happened. Byte-identical to the pre-1.2.1 output.
|
|
58
93
|
if (err instanceof CliError) {
|
|
59
94
|
ctx.io.err(c.red(err.message));
|
|
60
95
|
if (err.hint)
|
|
61
96
|
ctx.io.err(err.hint);
|
|
62
97
|
if (err.footer)
|
|
63
98
|
ctx.io.err(c.dim(footerUrl(err.footer)));
|
|
64
|
-
return err.exitCode;
|
|
65
99
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
100
|
+
else {
|
|
101
|
+
ctx.io.err(c.red(failure.error.message));
|
|
102
|
+
ctx.io.err(c.dim(failure.error.doc_url));
|
|
103
|
+
}
|
|
104
|
+
// The report a batch command already flushed IS the machine-readable answer (its
|
|
105
|
+
// rows carry `failed`/`exit` per item). Appending a second document would corrupt it.
|
|
106
|
+
if (json && !stdoutUsed())
|
|
107
|
+
ctx.io.out(JSON.stringify(failure));
|
|
108
|
+
return failure.error.exit_code;
|
|
70
109
|
}
|
|
71
110
|
}
|
|
72
111
|
main().then((code) => process.exit(code), () => process.exit(EXIT.USAGE));
|
package/dist/installer.js
CHANGED
|
@@ -9,6 +9,7 @@ import { randomBytes } from "node:crypto";
|
|
|
9
9
|
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
11
|
import { CliError, EXIT, mapFsError, networkError } from "./errors.js";
|
|
12
|
+
import { SERVER_PATH_MAX, terminalLine } from "./terminal-text.js";
|
|
12
13
|
import { CLI_DOS_MAX_SKILL_FILES as MAX_SKILL_FILES, CLI_DOS_MAX_SKILL_FILE_BYTES as MAX_SKILL_FILE_BYTES, CLI_DOS_MAX_SKILL_TOTAL_BYTES as MAX_SKILL_TOTAL_BYTES, } from "./generated/constants.js";
|
|
13
14
|
import { sha256Hex, sha256Matches } from "./integrity.js";
|
|
14
15
|
import { validateSkillPaths } from "./paths.js";
|
|
@@ -98,7 +99,14 @@ export function assertSafeManifest(manifest) {
|
|
|
98
99
|
const issues = validateSkillPaths(manifest.map((e) => e.path));
|
|
99
100
|
if (issues.length > 0) {
|
|
100
101
|
const first = issues[0];
|
|
101
|
-
throw new CliError(
|
|
102
|
+
throw new CliError(
|
|
103
|
+
// ⚠️ THE SECURITY CONTROL'S OWN MESSAGE WAS THE INJECTION CHANNEL.
|
|
104
|
+
// `validateSkillPaths` checks length → backslash → absolute → control chars in
|
|
105
|
+
// that order, so a path rejected on any of the first three is echoed here having
|
|
106
|
+
// never been control-char checked. The CliError backstop strips escapes, but only
|
|
107
|
+
// this bounds it to one line — and 256 is validateSkillPaths' own length rule, so
|
|
108
|
+
// it can never clamp a path this function would have accepted.
|
|
109
|
+
`unsafe file path in manifest ("${terminalLine(first.path, SERVER_PATH_MAX)}": ${first.reason}) — refusing to install`, EXIT.INTEGRITY, { footer: "manifest_path" });
|
|
102
110
|
}
|
|
103
111
|
if (manifest.length === 0 || manifest.length > MAX_SKILL_FILES) {
|
|
104
112
|
throw new CliError(`manifest lists ${manifest.length} files (limit ${MAX_SKILL_FILES}) — refusing to install`, EXIT.INTEGRITY, { footer: "manifest_bounds" });
|
package/dist/registry.d.ts
CHANGED
|
@@ -96,4 +96,4 @@ export declare function detectTargets(opts: {
|
|
|
96
96
|
exists?: (p: string) => boolean;
|
|
97
97
|
}): DetectedTarget[];
|
|
98
98
|
/** DX1/DX F2/F16 — the per-target next step, incl. the RELOAD caveat. */
|
|
99
|
-
export declare function nextStepLine(targetId: string,
|
|
99
|
+
export declare function nextStepLine(targetId: string, rawSlug: string): string;
|
package/dist/registry.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// floats detected tools to the top and preselects the first (D-UX11).
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
+
import { SERVER_LABEL_MAX, terminalLine } from "./terminal-text.js";
|
|
7
8
|
export const TARGETS = [
|
|
8
9
|
{
|
|
9
10
|
id: "claude",
|
|
@@ -158,7 +159,10 @@ export function detectTargets(opts) {
|
|
|
158
159
|
return [...all.filter((t) => t.detected), ...all.filter((t) => !t.detected)];
|
|
159
160
|
}
|
|
160
161
|
/** DX1/DX F2/F16 — the per-target next step, incl. the RELOAD caveat. */
|
|
161
|
-
export function nextStepLine(targetId,
|
|
162
|
+
export function nextStepLine(targetId, rawSlug) {
|
|
163
|
+
// Sanitized INSIDE rather than at the call site: this function's whole output is
|
|
164
|
+
// display, so every present and future caller is covered by one line.
|
|
165
|
+
const slug = terminalLine(rawSlug, SERVER_LABEL_MAX);
|
|
162
166
|
switch (targetId) {
|
|
163
167
|
case "claude":
|
|
164
168
|
return `Next: start a NEW session in Claude Code (open sessions don't see new skills) and type /${slug}`;
|