promptdock 0.1.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.
@@ -0,0 +1,61 @@
1
+ import { spawn } from "node:child_process";
2
+ import { homedir } from "node:os";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { readFileSync } from "node:fs";
5
+ /** Read the package's own version (dist/ and src/ both sit one level under the root). */
6
+ export function readOwnVersion() {
7
+ try {
8
+ const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
9
+ return pkg.version ?? "0.0.0";
10
+ }
11
+ catch {
12
+ return "0.0.0";
13
+ }
14
+ }
15
+ /** Best-effort platform browser open (darwin `open`, win32 `start`, else xdg-open). */
16
+ export function openUrlBestEffort(url, platform) {
17
+ try {
18
+ const [cmd, args] = platform === "darwin"
19
+ ? ["open", [url]]
20
+ : platform === "win32"
21
+ ? // `start` is a cmd.exe builtin; the empty "" is its window-title slot.
22
+ ["cmd", ["/c", "start", "", url]]
23
+ : ["xdg-open", [url]];
24
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
25
+ child.on("error", () => undefined);
26
+ child.unref();
27
+ }
28
+ catch {
29
+ /* best-effort only */
30
+ }
31
+ }
32
+ export function realContext(argv) {
33
+ const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
34
+ return {
35
+ io: {
36
+ out: (s) => process.stdout.write(s + "\n"),
37
+ err: (s) => process.stderr.write(s + "\n"),
38
+ write: (s) => process.stdout.write(s),
39
+ isTTY,
40
+ question: async (prompt) => {
41
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
42
+ try {
43
+ return await rl.question(prompt);
44
+ }
45
+ finally {
46
+ rl.close();
47
+ }
48
+ },
49
+ },
50
+ env: process.env,
51
+ cwd: process.cwd(),
52
+ home: homedir(),
53
+ platform: process.platform,
54
+ fetch: globalThis.fetch,
55
+ version: readOwnVersion(),
56
+ argsLine: argv.join(" "),
57
+ now: () => Date.now(),
58
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
59
+ openUrl: (url) => openUrlBestEffort(url, process.platform),
60
+ };
61
+ }
@@ -0,0 +1,62 @@
1
+ export type AuthStartResponse = {
2
+ device_code: string;
3
+ /** display form XXXX-XXXX */
4
+ user_code: string;
5
+ verification_url: string;
6
+ /** RFC-8628 minimum poll interval, secs */
7
+ interval: number;
8
+ expires_in: number;
9
+ };
10
+ /** Poll success (the exactly-once CAS delivery). */
11
+ export type AuthPollToken = {
12
+ access_token: string;
13
+ token_type: "Bearer";
14
+ expires_at: string | null;
15
+ };
16
+ /** RFC-8628 verbs the poll speaks at the TOP level (the one envelope exception). */
17
+ export type PollVerb = "authorization_pending" | "slow_down" | "access_denied" | "expired_token";
18
+ export type WhoamiResponse = {
19
+ handle: string | null;
20
+ role: string;
21
+ expires_at: string | null;
22
+ };
23
+ export type ResolveVerdict = "ok" | "already_entitled" | "insufficient_tier" | "cap_hit" | "not_found";
24
+ export type ResolveResponse = {
25
+ verdict: ResolveVerdict;
26
+ skill_id?: string;
27
+ version_id?: string;
28
+ version?: number;
29
+ title?: string;
30
+ author?: string;
31
+ is_free?: boolean;
32
+ file_count?: number;
33
+ total_bytes?: number;
34
+ description?: string;
35
+ license?: string;
36
+ /** insufficient_tier */
37
+ required?: string[];
38
+ current?: string;
39
+ /** cap_hit */
40
+ retry_after_secs?: number;
41
+ };
42
+ export type ManifestEntry = {
43
+ path: string;
44
+ bytes: number;
45
+ sha256: string;
46
+ content_type: string;
47
+ };
48
+ export type InstallResponse = {
49
+ skill: {
50
+ id: string;
51
+ slug: string;
52
+ title: string;
53
+ version: number;
54
+ version_id: string;
55
+ };
56
+ manifest: ManifestEntry[];
57
+ files: {
58
+ path: string;
59
+ url: string;
60
+ }[];
61
+ url_ttl_secs: number;
62
+ };
@@ -0,0 +1,4 @@
1
+ // Wire types for the COMMITTED server contract (app/api/v1/cli/** — Stage 3a).
2
+ // Every field is treated as untrusted at the call sites; these types describe
3
+ // the happy shape, they do not promise it.
4
+ export {};
@@ -0,0 +1,38 @@
1
+ export declare const EXIT: {
2
+ readonly OK: 0;
3
+ /** bad flags/args, non-TTY without --target/--dir, missing -y in CI */
4
+ readonly USAGE: 1;
5
+ /** not logged in / token rejected */
6
+ readonly AUTH: 2;
7
+ /** server gate: tier, cap, denial, rate limit, version floor, not found */
8
+ readonly DENIED: 3;
9
+ /** sha256 mismatch, unsafe manifest path, receipt schema problems */
10
+ readonly INTEGRITY: 4;
11
+ /** filesystem: permissions, disk, existing dir */
12
+ readonly IO: 5;
13
+ /** couldn't reach the API */
14
+ readonly NETWORK: 6;
15
+ };
16
+ export type ExitCode = (typeof EXIT)[keyof typeof EXIT];
17
+ /** DX3: every named error footer links its docs anchor. */
18
+ export declare function footerUrl(code: string): string;
19
+ export declare class CliError extends Error {
20
+ readonly exitCode: ExitCode;
21
+ /** docs anchor slug (renders the DX3 footer); omit for plain usage errors */
22
+ readonly footer?: string;
23
+ /** one-line "what to do" printed after the message */
24
+ readonly hint?: string;
25
+ constructor(message: string, exitCode: ExitCode, opts?: {
26
+ footer?: string;
27
+ hint?: string;
28
+ });
29
+ }
30
+ export declare function usageError(message: string, hint?: string): CliError;
31
+ /**
32
+ * DX3 per-OS filesystem error mapping. THREE distinct messages; the --force
33
+ * hint belongs ONLY to the existing-dir refusal (which is not an errno — see
34
+ * install.ts), never to EACCES/ENOSPC (--force cannot fix those).
35
+ */
36
+ export declare function mapFsError(err: unknown, path: string): CliError;
37
+ /** DX3 network wrap (+ proxy note: Node's fetch ignores proxy env vars). */
38
+ export declare function networkError(baseUrl: string, env: Record<string, string | undefined>): CliError;
package/dist/errors.js ADDED
@@ -0,0 +1,72 @@
1
+ // Stable exit codes (DX3) — scripts and CI depend on these; never renumber.
2
+ export const EXIT = {
3
+ OK: 0,
4
+ /** bad flags/args, non-TTY without --target/--dir, missing -y in CI */
5
+ USAGE: 1,
6
+ /** not logged in / token rejected */
7
+ AUTH: 2,
8
+ /** server gate: tier, cap, denial, rate limit, version floor, not found */
9
+ DENIED: 3,
10
+ /** sha256 mismatch, unsafe manifest path, receipt schema problems */
11
+ INTEGRITY: 4,
12
+ /** filesystem: permissions, disk, existing dir */
13
+ IO: 5,
14
+ /** couldn't reach the API */
15
+ NETWORK: 6,
16
+ };
17
+ /** DX3: every named error footer links its docs anchor. */
18
+ export function footerUrl(code) {
19
+ return `https://promptdock.ai/docs/cli/errors#${code}`;
20
+ }
21
+ export class CliError extends Error {
22
+ exitCode;
23
+ /** docs anchor slug (renders the DX3 footer); omit for plain usage errors */
24
+ footer;
25
+ /** one-line "what to do" printed after the message */
26
+ hint;
27
+ constructor(message, exitCode, opts) {
28
+ super(message);
29
+ this.name = "CliError";
30
+ this.exitCode = exitCode;
31
+ this.footer = opts?.footer;
32
+ this.hint = opts?.hint;
33
+ }
34
+ }
35
+ export function usageError(message, hint) {
36
+ return new CliError(message, EXIT.USAGE, { hint, footer: "usage" });
37
+ }
38
+ /**
39
+ * DX3 per-OS filesystem error mapping. THREE distinct messages; the --force
40
+ * hint belongs ONLY to the existing-dir refusal (which is not an errno — see
41
+ * install.ts), never to EACCES/ENOSPC (--force cannot fix those).
42
+ */
43
+ export function mapFsError(err, path) {
44
+ const code = err?.code;
45
+ if (code === "EACCES" || code === "EPERM") {
46
+ return new CliError(`permission denied: ${path} — check permissions or choose a different --dir`, EXIT.IO, { footer: "eacces" });
47
+ }
48
+ if (code === "ENOSPC") {
49
+ return new CliError(`not enough disk space writing ${path} — free up space`, EXIT.IO, {
50
+ footer: "enospc",
51
+ });
52
+ }
53
+ const detail = err instanceof Error ? err.message : String(err);
54
+ return new CliError(`filesystem error at ${path}: ${detail}`, EXIT.IO, { footer: "io" });
55
+ }
56
+ /** DX3 network wrap (+ proxy note: Node's fetch ignores proxy env vars). */
57
+ export function networkError(baseUrl, env) {
58
+ let host = baseUrl;
59
+ try {
60
+ host = new URL(baseUrl).host;
61
+ }
62
+ catch {
63
+ /* keep raw */
64
+ }
65
+ const proxyVar = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"].find((k) => env[k]);
66
+ const proxyNote = proxyVar
67
+ ? ` (proxy env detected: ${proxyVar} — the CLI uses Node's built-in fetch, which does not read proxy settings)`
68
+ : "";
69
+ return new CliError(`couldn't reach ${host} — check your connection${proxyNote}`, EXIT.NETWORK, {
70
+ footer: "network",
71
+ });
72
+ }
@@ -0,0 +1,9 @@
1
+ export declare const MAX_SKILL_FILES = 25;
2
+ export declare const MAX_SKILL_TOTAL_BYTES = 5242880;
3
+ export declare const MAX_SKILL_FILE_BYTES = 1048576;
4
+ export declare const MAX_SKILL_IMAGES = 5;
5
+ export declare const SKILL_REVEAL_DAILY_CAP = 15;
6
+ export declare const CLI_MIN_VERSION = "0.1.0";
7
+ export declare const SKILL_SLUG_RE_SOURCE = "^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$";
8
+ export declare const SKILL_HANDLE_RE_SOURCE = "^[a-z0-9][a-z0-9_-]{0,62}$";
9
+ export declare const CANONICAL_INSTALL_COMMAND = "npx promptdock@latest install";
@@ -0,0 +1,11 @@
1
+ // GENERATED by scripts/gen-cli-constants.mjs from lib/validation/skills-constants.json
2
+ // DO NOT EDIT — edit the JSON source and run `pnpm run gen:cli-constants`.
3
+ export const MAX_SKILL_FILES = 25;
4
+ export const MAX_SKILL_TOTAL_BYTES = 5242880;
5
+ export const MAX_SKILL_FILE_BYTES = 1048576;
6
+ export const MAX_SKILL_IMAGES = 5;
7
+ export const SKILL_REVEAL_DAILY_CAP = 15;
8
+ export const CLI_MIN_VERSION = "0.1.0";
9
+ export const SKILL_SLUG_RE_SOURCE = "^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$";
10
+ export const SKILL_HANDLE_RE_SOURCE = "^[a-z0-9][a-z0-9_-]{0,62}$";
11
+ export const CANONICAL_INSTALL_COMMAND = "npx promptdock@latest install";
package/dist/help.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function globalHelp(version: string): string;
2
+ export declare const COMMAND_HELP: Record<string, string>;
package/dist/help.js ADDED
@@ -0,0 +1,84 @@
1
+ // DX4: `promptdock --help` is a COMPLETE zero-config map; every command also
2
+ // answers `promptdock <cmd> --help`.
3
+ import { CANONICAL_INSTALL_COMMAND } from "./generated/constants.js";
4
+ import { TARGETS } from "./registry.js";
5
+ export function globalHelp(version) {
6
+ const targets = TARGETS.map((t) => ` ${t.id.padEnd(9)} ${t.localBase}/<slug> (-g: ~/${t.globalBase}/<slug>)${t.experimental ? " (experimental)" : ""}`).join("\n");
7
+ return `promptdock v${version} — install AI agent skills from promptdock.ai
8
+
9
+ Usage
10
+ ${CANONICAL_INSTALL_COMMAND} <handle>/<slug>
11
+
12
+ Commands
13
+ login [--token pdk_…] log in (browser hand-off, or store a CI token)
14
+ logout remove the local token (revoke in Settings to kill it server-side)
15
+ whoami [--json] show the signed-in account
16
+ install <ref> [options] install a skill (ref = handle/slug, @handle/slug, or a pasted URL)
17
+ uninstall <ref|--all> remove an installed skill (receipt-driven; never touches unmanaged files)
18
+ update [ref] [--all] update installed skills to the latest approved version
19
+ upgrade alias of update
20
+ list [--json] show installed skills (receipts)
21
+ help this map
22
+
23
+ Install options
24
+ -g, --global install into the tool's per-user dir instead of the project
25
+ --target <tool> skip the picker (${TARGETS.map((t) => t.id).join(", ")})
26
+ --dir <path> install into an explicit directory
27
+ -y, --yes skip prompts (needs an unambiguous target; NEVER implies --force)
28
+ --force replace a non-empty foreign directory / overwrite local edits
29
+ --dry-run resolve + print what would happen; installs nothing
30
+ --json machine-readable output
31
+
32
+ Update/uninstall options
33
+ --check (update) report available updates without applying
34
+ --all operate across project-local AND global scopes
35
+ -g, --global operate on the global scope (bare commands are project-local)
36
+
37
+ Targets
38
+ ${targets}
39
+
40
+ Environment
41
+ PROMPTDOCK_TOKEN bearer token for CI/non-interactive use (Settings → CLI sessions → Generate token)
42
+ PROMPTDOCK_API_BASE API origin override (default https://promptdock.ai)
43
+ NO_COLOR disable colored output
44
+
45
+ Exit codes
46
+ 0 ok · 1 usage · 2 auth · 3 denied/gated · 4 integrity · 5 filesystem · 6 network
47
+
48
+ Docs: https://promptdock.ai/docs/cli`;
49
+ }
50
+ export const COMMAND_HELP = {
51
+ login: `promptdock login [--token pdk_…]
52
+
53
+ Log in via the browser hand-off: the CLI prints a code, opens promptdock.ai,
54
+ you approve there, and the terminal continues. --token stores a token minted in
55
+ Settings → Account → CLI sessions (the CI path; or set PROMPTDOCK_TOKEN).`,
56
+ logout: `promptdock logout
57
+
58
+ Removes the locally-stored token (~/.promptdock/config.json). The server-side
59
+ session stays valid until revoked in Settings → Account → CLI sessions.`,
60
+ whoami: `promptdock whoami [--json]
61
+
62
+ Shows the signed-in handle, role, and token expiry.`,
63
+ install: `promptdock install <handle>/<slug> [-g] [--target <tool>] [--dir <path>] [-y] [--force] [--dry-run] [--json]
64
+
65
+ Installs a skill. Non-interactive sessions (CI) must pass --target or --dir
66
+ plus -y, and authenticate via PROMPTDOCK_TOKEN. -y skips the target picker
67
+ ONLY — a non-empty foreign directory still requires --force. --dry-run
68
+ resolves and prints the plan without installing (and without spending any
69
+ premium-unlock slot).`,
70
+ uninstall: `promptdock uninstall <handle>/<slug> | --all [-g] [--target <tool>] [--dir <path>] [--force] [-y] [--json]
71
+
72
+ Removes an installed skill using its .promptdock.json receipt — only files the
73
+ receipt lists are ever deleted. Locally-modified files block removal without
74
+ --force (copy your changes out first).`,
75
+ update: `promptdock update [<handle>/<slug>] [--all] [--check] [-g] [-y] [--force] [--json]
76
+
77
+ Updates installed skills to the latest approved version. Bare \`update\` covers
78
+ the project-local scope; -g the global scope; --all both. --check reports
79
+ without applying. Locally-modified files block an update without --force.`,
80
+ list: `promptdock list [--json] [-g] [--all]
81
+
82
+ Lists installed skills from their receipts: ref, version, target, installed-at.`,
83
+ help: `promptdock help — prints the global command map.`,
84
+ };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ // promptdock — the PromptDock skills CLI (Stage 3b).
3
+ // Contract: app/api/v1/cli/** (server), docs/plans/skills-marketplace.md.
4
+ import { parseArgs } from "./args.js";
5
+ import { realContext } from "./context.js";
6
+ import { CliError, EXIT, footerUrl } from "./errors.js";
7
+ import { COMMAND_HELP, globalHelp } from "./help.js";
8
+ import { colors } from "./ui.js";
9
+ import { runInstall } from "./commands/install.js";
10
+ import { runLogin, runLogout, runWhoami } from "./commands/login.js";
11
+ import { runList, runUninstall, runUpdate } from "./commands/lifecycle.js";
12
+ async function main() {
13
+ const argv = process.argv.slice(2);
14
+ const ctx = realContext(argv);
15
+ const c = colors(ctx.env, Boolean(process.stderr.isTTY));
16
+ try {
17
+ const { command, positionals, flags } = parseArgs(argv);
18
+ if (command === "version") {
19
+ ctx.io.out(ctx.version);
20
+ return EXIT.OK;
21
+ }
22
+ if (command === "help" || command === null) {
23
+ ctx.io.out(globalHelp(ctx.version));
24
+ return EXIT.OK;
25
+ }
26
+ if (flags.help === true) {
27
+ ctx.io.out(COMMAND_HELP[command] ?? globalHelp(ctx.version));
28
+ return EXIT.OK;
29
+ }
30
+ switch (command) {
31
+ case "login":
32
+ await runLogin(ctx, flags);
33
+ return EXIT.OK;
34
+ case "logout":
35
+ await runLogout(ctx);
36
+ return EXIT.OK;
37
+ case "whoami":
38
+ await runWhoami(ctx, flags);
39
+ return EXIT.OK;
40
+ case "install":
41
+ await runInstall(ctx, positionals, flags);
42
+ return EXIT.OK;
43
+ case "uninstall":
44
+ await runUninstall(ctx, positionals, flags);
45
+ return EXIT.OK;
46
+ case "update":
47
+ await runUpdate(ctx, positionals, flags);
48
+ return EXIT.OK;
49
+ case "list":
50
+ await runList(ctx, flags);
51
+ return EXIT.OK;
52
+ default:
53
+ ctx.io.err(`unknown command "${command}" — run: promptdock --help`);
54
+ return EXIT.USAGE;
55
+ }
56
+ }
57
+ catch (err) {
58
+ if (err instanceof CliError) {
59
+ ctx.io.err(c.red(err.message));
60
+ if (err.hint)
61
+ ctx.io.err(err.hint);
62
+ if (err.footer)
63
+ ctx.io.err(c.dim(footerUrl(err.footer)));
64
+ return err.exitCode;
65
+ }
66
+ const detail = err instanceof Error ? err.message : String(err);
67
+ ctx.io.err(c.red(`unexpected error: ${detail}`));
68
+ ctx.io.err(c.dim(footerUrl("internal")));
69
+ return EXIT.USAGE;
70
+ }
71
+ }
72
+ main().then((code) => process.exit(code), () => process.exit(EXIT.USAGE));
@@ -0,0 +1,43 @@
1
+ import type { Api } from "./api.js";
2
+ import type { CliContext } from "./context.js";
3
+ import type { InstallResponse, ManifestEntry } from "./contract.js";
4
+ import { type Receipt } from "./receipts.js";
5
+ /** Narrow the untrusted install response; throws INTEGRITY on a bad shape. */
6
+ export declare function checkInstallResponse(resp: unknown): InstallResponse;
7
+ /**
8
+ * E8 client-side re-validation + the shared D7 bounds. A server compromise must
9
+ * not become an arbitrary file write OR a disk-filling download.
10
+ */
11
+ export declare function assertSafeManifest(manifest: ManifestEntry[]): void;
12
+ export type StagedInstall = {
13
+ tempDir: string;
14
+ files: {
15
+ path: string;
16
+ sha256: string;
17
+ }[];
18
+ };
19
+ /**
20
+ * Download every manifest file into `<parent>/.promptdock-staging-*` (SIBLING of
21
+ * the target so the final rename never crosses filesystems), verifying sha256 +
22
+ * byte length against the manifest. Throws (and discards the temp dir) on ANY
23
+ * mismatch — the sha in the manifest is the transport-integrity anchor.
24
+ */
25
+ export declare function downloadAndStage(ctx: CliContext, api: Api, resp: InstallResponse, targetDir: string): Promise<StagedInstall>;
26
+ /**
27
+ * Atomic-ish commit: fresh target → one rename; existing target → staged
28
+ * replace (target → .promptdock-old-*, temp → target, drop old; on failure the
29
+ * old dir is restored). Same-parent renames only.
30
+ */
31
+ export declare function commitStaged(tempDir: string, targetDir: string): void;
32
+ /**
33
+ * The full phase-2: download → verify → commit → receipt → best-effort complete.
34
+ * `refString` is the canonical handle/slug; `prior` carries a matching update's
35
+ * receipt so unknown fields survive the rewrite (DX F16).
36
+ */
37
+ export declare function performInstall(ctx: CliContext, api: Api, opts: {
38
+ resp: InstallResponse;
39
+ targetDir: string;
40
+ targetId: string;
41
+ refString: string;
42
+ prior?: Receipt;
43
+ }): Promise<Receipt>;