promptdock 1.1.0 → 1.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 +21 -1
- package/dist/commands/install.js +1 -1
- package/dist/commands/lifecycle.js +49 -13
- package/dist/contract.d.ts +7 -0
- package/dist/generated/constants.d.ts +10 -0
- package/dist/generated/constants.js +5 -0
- package/dist/installer.d.ts +12 -2
- package/dist/installer.js +88 -8
- package/dist/verdicts.d.ts +1 -1
- package/dist/verdicts.js +43 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -128,7 +128,27 @@ path is re-validated before any write; a failed check aborts **all-or-nothing**
|
|
|
128
128
|
| 6 | network |
|
|
129
129
|
| 130 | interrupted (`Ctrl-C` during an interactive prompt; `Esc` — a deliberate in-UI cancel — exits 1) |
|
|
130
130
|
|
|
131
|
-
Every named error links `https://promptdock.ai/docs/cli/errors#<code>`.
|
|
131
|
+
Every named error links `https://promptdock.ai/docs/cli/errors#<code>`. That page
|
|
132
|
+
lists every code with what happened, why, and what to do.
|
|
133
|
+
|
|
134
|
+
## Package compatibility
|
|
135
|
+
|
|
136
|
+
This CLI carries its own **install-safety limits** — how many files it will write and how
|
|
137
|
+
much it will download for one skill. They exist so a compromised server cannot fill your
|
|
138
|
+
disk or write outside the target directory, and they are deliberately far above anything
|
|
139
|
+
the platform accepts, so a normal skill never comes near them.
|
|
140
|
+
|
|
141
|
+
Occasionally a skill is published that is larger than the version of the CLI you have.
|
|
142
|
+
When that happens you get, for **that skill only**:
|
|
143
|
+
|
|
144
|
+
```
|
|
145
|
+
"Big Skill" needs promptdock CLI 1.2.0 or newer — you are on 1.0.1.
|
|
146
|
+
Update: npx promptdock@latest install …
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
It exits `3` (denied), not `4` (integrity) — the package is fine, your CLI is just older
|
|
150
|
+
than it. Your login, your other installs and `promptdock update` are unaffected;
|
|
151
|
+
`promptdock update` skips that one skill and continues with the rest.
|
|
132
152
|
|
|
133
153
|
## Notes
|
|
134
154
|
|
package/dist/commands/install.js
CHANGED
|
@@ -27,7 +27,7 @@ export async function runInstall(ctx, positionals, flags) {
|
|
|
27
27
|
const c = colors(ctx.env, ctx.io.isTTY);
|
|
28
28
|
const { api } = await ensureAuth(ctx);
|
|
29
29
|
const resolve = await api.request("GET", `/api/v1/cli/skills/resolve?ref=${encodeURIComponent(refString)}`);
|
|
30
|
-
assertInstallable(resolve, refString);
|
|
30
|
+
assertInstallable(resolve, refString, ctx.version);
|
|
31
31
|
if (typeof resolve.version_id !== "string" || typeof resolve.skill_id !== "string") {
|
|
32
32
|
throw new CliError("malformed resolve response — update the CLI and retry", EXIT.DENIED, {
|
|
33
33
|
footer: "verdict",
|
|
@@ -138,6 +138,17 @@ function removeReceiptOwned(dir, receipt) {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
/* ── update ─────────────────────────────────────────────────────────────────── */
|
|
141
|
+
/**
|
|
142
|
+
* Errors no later item can recover from either — these still abort the batch.
|
|
143
|
+
*
|
|
144
|
+
* USAGE means the invocation itself is wrong (a confirmation needed in a non-interactive
|
|
145
|
+
* session), so every subsequent item would fail identically. AUTH means the token is gone.
|
|
146
|
+
* Everything else — an integrity refusal, an over-fuse package, a network blip on one
|
|
147
|
+
* download — is this item's problem and the next item deserves its turn.
|
|
148
|
+
*/
|
|
149
|
+
function isFatalForBatch(err) {
|
|
150
|
+
return err instanceof CliError && (err.exitCode === EXIT.USAGE || err.exitCode === EXIT.AUTH);
|
|
151
|
+
}
|
|
141
152
|
export async function runUpdate(ctx, positionals, flags) {
|
|
142
153
|
const c = colors(ctx.env, ctx.io.isTTY);
|
|
143
154
|
const checkOnly = flags.check === true;
|
|
@@ -159,7 +170,7 @@ export async function runUpdate(ctx, positionals, flags) {
|
|
|
159
170
|
let resolved;
|
|
160
171
|
try {
|
|
161
172
|
resolved = await api.request("GET", `/api/v1/cli/skills/resolve?ref=${encodeURIComponent(receipt.ref)}`);
|
|
162
|
-
assertInstallable(resolved, receipt.ref);
|
|
173
|
+
assertInstallable(resolved, receipt.ref, ctx.version);
|
|
163
174
|
}
|
|
164
175
|
catch (err) {
|
|
165
176
|
const reason = err instanceof CliError ? err.message : String(err);
|
|
@@ -201,18 +212,43 @@ export async function runUpdate(ctx, positionals, flags) {
|
|
|
201
212
|
}
|
|
202
213
|
if (typeof resolved.skill_id !== "string" || typeof resolved.version_id !== "string")
|
|
203
214
|
continue;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
/**
|
|
216
|
+
* ⚠️ THE INSTALL MUST BE INSIDE THE PER-ITEM CATCH TOO, or "one bad package skips and
|
|
217
|
+
* the rest continue" is false for the population that matters.
|
|
218
|
+
*
|
|
219
|
+
* The catch above wraps only resolve + `assertInstallable` — and `assertInstallable`
|
|
220
|
+
* is exactly what OLDER CLIs do not have. On those, the only thing that refuses an
|
|
221
|
+
* over-fuse package is `assertSafeManifest`, deep inside `performInstall`, i.e. out
|
|
222
|
+
* here beyond the guard. So the CliError propagated out of the loop and killed the
|
|
223
|
+
* whole run: one oversized skill in a directory of twenty aborted the other nineteen.
|
|
224
|
+
* That is the population the package-scoped `min_cli_version` design exists to serve,
|
|
225
|
+
* so the guarantee was false precisely where it was load-bearing.
|
|
226
|
+
*
|
|
227
|
+
* Genuinely fatal classes still propagate — a usage error or a newer-schema receipt
|
|
228
|
+
* is not something the next item can recover from either.
|
|
229
|
+
*/
|
|
230
|
+
try {
|
|
231
|
+
const respRaw = await api.request("POST", `/api/v1/cli/skills/${resolved.skill_id}/install`, { version_id: resolved.version_id });
|
|
232
|
+
const resp = checkInstallResponse(respRaw);
|
|
233
|
+
await performInstall(ctx, api, {
|
|
234
|
+
resp,
|
|
235
|
+
targetDir: item.dir,
|
|
236
|
+
targetId: item.targetId,
|
|
237
|
+
refString: receipt.ref,
|
|
238
|
+
prior: receipt,
|
|
239
|
+
});
|
|
240
|
+
report.push({ ref: receipt.ref, dir: item.dir, from: receipt.version, to: resp.skill.version, status: "updated" });
|
|
241
|
+
if (!json)
|
|
242
|
+
ctx.io.out(`${c.green("✓")} ${receipt.ref}: v${receipt.version} → v${resp.skill.version}`);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
if (isFatalForBatch(err))
|
|
246
|
+
throw err;
|
|
247
|
+
const reason = err instanceof CliError ? err.message : String(err);
|
|
248
|
+
report.push({ ref: receipt.ref, dir: item.dir, from: receipt.version, to: toVersion, status: "skipped", reason });
|
|
249
|
+
if (!json)
|
|
250
|
+
ctx.io.out(`${c.yellow("!")} ${receipt.ref}: ${reason}`);
|
|
251
|
+
}
|
|
216
252
|
}
|
|
217
253
|
if (json)
|
|
218
254
|
ctx.io.out(JSON.stringify({ results: report }));
|
package/dist/contract.d.ts
CHANGED
|
@@ -31,6 +31,13 @@ export type ResolveResponse = {
|
|
|
31
31
|
is_free?: boolean;
|
|
32
32
|
file_count?: number;
|
|
33
33
|
total_bytes?: number;
|
|
34
|
+
/**
|
|
35
|
+
* The oldest CLI generation whose install-safety fuses admit this package, or
|
|
36
|
+
* null/absent when every published CLI can install it. Lets an old client refuse
|
|
37
|
+
* ONE oversized package with an upgrade prompt instead of the server bumping a
|
|
38
|
+
* global version floor that would 426 every request from every old client.
|
|
39
|
+
*/
|
|
40
|
+
min_cli_version?: string | null;
|
|
34
41
|
description?: string;
|
|
35
42
|
license?: string;
|
|
36
43
|
/** insufficient_tier */
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
export declare const MAX_SKILL_FILES = 25;
|
|
2
2
|
export declare const MAX_SKILL_TOTAL_BYTES = 5242880;
|
|
3
3
|
export declare const MAX_SKILL_FILE_BYTES = 1048576;
|
|
4
|
+
export declare const CLI_DOS_MAX_SKILL_FILES = 200;
|
|
5
|
+
export declare const CLI_DOS_MAX_SKILL_TOTAL_BYTES = 52428800;
|
|
6
|
+
export declare const CLI_DOS_MAX_SKILL_FILE_BYTES = 5242880;
|
|
7
|
+
export declare const CLI_FUSE_HISTORY: {
|
|
8
|
+
cli_version: string;
|
|
9
|
+
max_files: number;
|
|
10
|
+
max_file_bytes: number;
|
|
11
|
+
max_total_bytes: number;
|
|
12
|
+
}[];
|
|
4
13
|
export declare const MAX_SKILL_IMAGES = 5;
|
|
5
14
|
export declare const SKILL_REVEAL_DAILY_CAP = 15;
|
|
6
15
|
export declare const CLI_MIN_VERSION = "1.0.1";
|
|
16
|
+
export declare const PUBLISHED_CLI_VERSION = "1.1.0";
|
|
7
17
|
export declare const SKILL_SLUG_RE_SOURCE = "^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$";
|
|
8
18
|
export declare const SKILL_HANDLE_RE_SOURCE = "^[a-z0-9][a-z0-9_-]{0,62}$";
|
|
9
19
|
export declare const CANONICAL_INSTALL_COMMAND = "npx promptdock@latest install";
|
|
@@ -3,9 +3,14 @@
|
|
|
3
3
|
export const MAX_SKILL_FILES = 25;
|
|
4
4
|
export const MAX_SKILL_TOTAL_BYTES = 5242880;
|
|
5
5
|
export const MAX_SKILL_FILE_BYTES = 1048576;
|
|
6
|
+
export const CLI_DOS_MAX_SKILL_FILES = 200;
|
|
7
|
+
export const CLI_DOS_MAX_SKILL_TOTAL_BYTES = 52428800;
|
|
8
|
+
export const CLI_DOS_MAX_SKILL_FILE_BYTES = 5242880;
|
|
9
|
+
export const CLI_FUSE_HISTORY = [{ "cli_version": "0.0.0", "max_files": 25, "max_file_bytes": 1048576, "max_total_bytes": 5242880 }, { "cli_version": "1.2.0", "max_files": 200, "max_file_bytes": 5242880, "max_total_bytes": 52428800 }];
|
|
6
10
|
export const MAX_SKILL_IMAGES = 5;
|
|
7
11
|
export const SKILL_REVEAL_DAILY_CAP = 15;
|
|
8
12
|
export const CLI_MIN_VERSION = "1.0.1";
|
|
13
|
+
export const PUBLISHED_CLI_VERSION = "1.1.0";
|
|
9
14
|
export const SKILL_SLUG_RE_SOURCE = "^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$";
|
|
10
15
|
export const SKILL_HANDLE_RE_SOURCE = "^[a-z0-9][a-z0-9_-]{0,62}$";
|
|
11
16
|
export const CANONICAL_INSTALL_COMMAND = "npx promptdock@latest install";
|
package/dist/installer.d.ts
CHANGED
|
@@ -5,8 +5,18 @@ import { type Receipt } from "./receipts.js";
|
|
|
5
5
|
/** Narrow the untrusted install response; throws INTEGRITY on a bad shape. */
|
|
6
6
|
export declare function checkInstallResponse(resp: unknown): InstallResponse;
|
|
7
7
|
/**
|
|
8
|
-
* E8 client-side re-validation
|
|
9
|
-
*
|
|
8
|
+
* E8 client-side re-validation against this CLI's own INSTALL-SAFETY FUSES.
|
|
9
|
+
*
|
|
10
|
+
* ⚠️ These bounds are SELF-PROTECTION, not a mirror of the platform's policy: "a
|
|
11
|
+
* server compromise must not become an arbitrary file write OR a disk-filling
|
|
12
|
+
* download." They are sized from memory/disk/runtime budgets and are deliberately far
|
|
13
|
+
* above any policy the platform would set, so a legitimate policy raise never needs a
|
|
14
|
+
* CLI release. Reaching one of them means the server sent something this CLI considers
|
|
15
|
+
* impossible — hence EXIT.INTEGRITY.
|
|
16
|
+
*
|
|
17
|
+
* The FRIENDLY, package-scoped refusal ("this skill needs a newer CLI") lives in
|
|
18
|
+
* verdicts.ts::assertInstallable and fires BEFORE the metered install POST. This
|
|
19
|
+
* function is the untrusted-server backstop behind it and must never be removed.
|
|
10
20
|
*/
|
|
11
21
|
export declare function assertSafeManifest(manifest: ManifestEntry[]): void;
|
|
12
22
|
export type StagedInstall = {
|
package/dist/installer.js
CHANGED
|
@@ -9,7 +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 { MAX_SKILL_FILES, MAX_SKILL_FILE_BYTES, MAX_SKILL_TOTAL_BYTES, } from "./generated/constants.js";
|
|
12
|
+
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
13
|
import { sha256Hex, sha256Matches } from "./integrity.js";
|
|
14
14
|
import { validateSkillPaths } from "./paths.js";
|
|
15
15
|
import { writeReceipt, RECEIPT_SCHEMA } from "./receipts.js";
|
|
@@ -37,8 +37,62 @@ export function checkInstallResponse(resp) {
|
|
|
37
37
|
return r;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* Read a response body, aborting the moment it exceeds `limit`.
|
|
41
|
+
*
|
|
42
|
+
* `res.arrayBuffer()` has no ceiling: it reads whatever the server sends. Streaming and
|
|
43
|
+
* cancelling means a hostile or broken server cannot make this process hold more than
|
|
44
|
+
* one file's worth of bytes, which is the property the install-safety fuse is claiming.
|
|
45
|
+
*
|
|
46
|
+
* Falls back to `arrayBuffer()` only when the runtime gives us no readable stream (older
|
|
47
|
+
* Node fetch shims) — with the post-hoc size check still applied, so the bound holds
|
|
48
|
+
* either way.
|
|
49
|
+
*/
|
|
50
|
+
async function readBounded(res, limit, path) {
|
|
51
|
+
const tooBig = () => new CliError(`${INTEGRITY_ABORT} ("${path}" oversized)`, EXIT.INTEGRITY, { footer: "integrity" });
|
|
52
|
+
const body = res.body;
|
|
53
|
+
if (!body || typeof body.getReader !== "function") {
|
|
54
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
55
|
+
if (buf.byteLength > limit)
|
|
56
|
+
throw tooBig();
|
|
57
|
+
return buf;
|
|
58
|
+
}
|
|
59
|
+
const reader = body.getReader();
|
|
60
|
+
const chunks = [];
|
|
61
|
+
let total = 0;
|
|
62
|
+
try {
|
|
63
|
+
for (;;) {
|
|
64
|
+
const { done, value } = await reader.read();
|
|
65
|
+
if (done)
|
|
66
|
+
break;
|
|
67
|
+
if (!value)
|
|
68
|
+
continue;
|
|
69
|
+
total += value.byteLength;
|
|
70
|
+
if (total > limit) {
|
|
71
|
+
// Stop pulling bytes immediately — this is the whole point.
|
|
72
|
+
await reader.cancel().catch(() => { });
|
|
73
|
+
throw tooBig();
|
|
74
|
+
}
|
|
75
|
+
chunks.push(value);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
reader.releaseLock?.();
|
|
80
|
+
}
|
|
81
|
+
return Buffer.concat(chunks.map((c) => Buffer.from(c)), total);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* E8 client-side re-validation against this CLI's own INSTALL-SAFETY FUSES.
|
|
85
|
+
*
|
|
86
|
+
* ⚠️ These bounds are SELF-PROTECTION, not a mirror of the platform's policy: "a
|
|
87
|
+
* server compromise must not become an arbitrary file write OR a disk-filling
|
|
88
|
+
* download." They are sized from memory/disk/runtime budgets and are deliberately far
|
|
89
|
+
* above any policy the platform would set, so a legitimate policy raise never needs a
|
|
90
|
+
* CLI release. Reaching one of them means the server sent something this CLI considers
|
|
91
|
+
* impossible — hence EXIT.INTEGRITY.
|
|
92
|
+
*
|
|
93
|
+
* The FRIENDLY, package-scoped refusal ("this skill needs a newer CLI") lives in
|
|
94
|
+
* verdicts.ts::assertInstallable and fires BEFORE the metered install POST. This
|
|
95
|
+
* function is the untrusted-server backstop behind it and must never be removed.
|
|
42
96
|
*/
|
|
43
97
|
export function assertSafeManifest(manifest) {
|
|
44
98
|
const issues = validateSkillPaths(manifest.map((e) => e.path));
|
|
@@ -79,6 +133,18 @@ export async function downloadAndStage(ctx, api, resp, targetDir) {
|
|
|
79
133
|
}
|
|
80
134
|
const tempDir = join(parent, `.promptdock-staging-${randomBytes(6).toString("hex")}`);
|
|
81
135
|
const files = [];
|
|
136
|
+
/**
|
|
137
|
+
* Bytes actually RECEIVED, not bytes the server said it would send.
|
|
138
|
+
*
|
|
139
|
+
* ⚠️ `assertSafeManifest` enforces the aggregate fuse against `entry.bytes` — a field
|
|
140
|
+
* the untrusted server supplies and that the response check does not even require. So
|
|
141
|
+
* before this counter the only bound the wire really carried was PER FILE, and a
|
|
142
|
+
* hostile server could declare 1 KB per entry and stream the per-file maximum for every
|
|
143
|
+
* one of them: at the raised fuses that is 200 × 5 MB ≈ 1 GB written to disk, which is
|
|
144
|
+
* precisely the "disk-filling download" this module's own docstring says it prevents.
|
|
145
|
+
* The manifest check is a fast pre-flight; THIS is the enforcement.
|
|
146
|
+
*/
|
|
147
|
+
let received = 0;
|
|
82
148
|
try {
|
|
83
149
|
mkdirSync(tempDir, { recursive: true });
|
|
84
150
|
for (const entry of resp.manifest) {
|
|
@@ -99,11 +165,25 @@ export async function downloadAndStage(ctx, api, resp, targetDir) {
|
|
|
99
165
|
// A lapsed signed URL is a free re-request (audit row 13) — say so.
|
|
100
166
|
throw new CliError(`download failed for "${entry.path}" (HTTP ${res.status}) — re-run the install (the download link may have expired; retrying is free)`, EXIT.NETWORK, { footer: "download" });
|
|
101
167
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
168
|
+
// ⚠️ CHECK BEFORE BUFFERING, then check again after.
|
|
169
|
+
//
|
|
170
|
+
// The fuse exists so a compromised server cannot fill your disk, and
|
|
171
|
+
// `await res.arrayBuffer()` reads the ENTIRE body into memory first — so a check
|
|
172
|
+
// that only runs afterwards has already let the download happen. A hostile server
|
|
173
|
+
// could stream gigabytes and the guard would fire, too late, on a machine that had
|
|
174
|
+
// already paid for it.
|
|
175
|
+
//
|
|
176
|
+
// So: refuse on a declared `content-length` over the fuse without reading a byte,
|
|
177
|
+
// and refuse again on the real length (content-length is server-supplied and may
|
|
178
|
+
// be absent or lie, which is exactly why the second check stays).
|
|
179
|
+
const declared = Number(res.headers.get("content-length"));
|
|
180
|
+
if (Number.isFinite(declared) && declared > MAX_SKILL_FILE_BYTES) {
|
|
181
|
+
throw new CliError(`${INTEGRITY_ABORT} ("${entry.path}" declares ${declared} bytes, over the limit)`, EXIT.INTEGRITY, { footer: "integrity" });
|
|
182
|
+
}
|
|
183
|
+
const buf = await readBounded(res, MAX_SKILL_FILE_BYTES, entry.path);
|
|
184
|
+
received += buf.byteLength;
|
|
185
|
+
if (received > MAX_SKILL_TOTAL_BYTES) {
|
|
186
|
+
throw new CliError(`${INTEGRITY_ABORT} (the package sent more than the ${MAX_SKILL_TOTAL_BYTES}-byte install limit)`, EXIT.INTEGRITY, { footer: "integrity" });
|
|
107
187
|
}
|
|
108
188
|
if (!sha256Matches(buf, entry.sha256)) {
|
|
109
189
|
throw new CliError(`${INTEGRITY_ABORT} (sha256 mismatch on "${entry.path}")`, EXIT.INTEGRITY, {
|
package/dist/verdicts.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ResolveResponse } from "./contract.js";
|
|
2
2
|
export declare const UPGRADE_URL = "https://promptdock.ai/pricing";
|
|
3
3
|
/** Throws the DX3-copy CliError for a deny verdict; returns for ok/already_entitled. */
|
|
4
|
-
export declare function assertInstallable(resolve: ResolveResponse, refString: string): void;
|
|
4
|
+
export declare function assertInstallable(resolve: ResolveResponse, refString: string, cliVersion: string): void;
|
package/dist/verdicts.js
CHANGED
|
@@ -1,11 +1,53 @@
|
|
|
1
1
|
import { CliError, EXIT } from "./errors.js";
|
|
2
2
|
import { retryHours } from "./ui.js";
|
|
3
|
+
import { CANONICAL_INSTALL_COMMAND, CLI_DOS_MAX_SKILL_FILES, CLI_DOS_MAX_SKILL_TOTAL_BYTES, } from "./generated/constants.js";
|
|
3
4
|
export const UPGRADE_URL = "https://promptdock.ai/pricing";
|
|
5
|
+
/** Numeric semver compare; mirrors compareCliVersions in lib/validation/skills.ts. */
|
|
6
|
+
function cmpVersion(a, b) {
|
|
7
|
+
const pa = a.split(".").map((n) => Number(n) || 0);
|
|
8
|
+
const pb = b.split(".").map((n) => Number(n) || 0);
|
|
9
|
+
return (pa[0] - pb[0]) || (pa[1] - pb[1]) || (pa[2] - pb[2]);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* PACKAGE-SCOPED compatibility, checked BEFORE the metered install POST.
|
|
13
|
+
*
|
|
14
|
+
* Two reasons this lives here and not only in `assertSafeManifest`:
|
|
15
|
+
* 1. `POST /cli/skills/{id}/install` mints an entitlement ticket under the shared
|
|
16
|
+
* 15/day premium cap. Failing after it burns one of the user's daily slots on a
|
|
17
|
+
* provably-doomed install.
|
|
18
|
+
* 2. A package this CLI is too old to install is a POLICY refusal (EXIT.DENIED — the
|
|
19
|
+
* code whose own doc string reads "tier, cap, denial, rate limit, version floor"),
|
|
20
|
+
* not a supply-chain signal. `assertSafeManifest` throws EXIT.INTEGRITY because
|
|
21
|
+
* there it means the server sent something impossible; here it means "update me".
|
|
22
|
+
*
|
|
23
|
+
* `assertSafeManifest` stays as the untrusted-server backstop — this check makes it
|
|
24
|
+
* unreachable in practice, never redundant.
|
|
25
|
+
*/
|
|
26
|
+
function assertPackageCompatible(resolve, cliVersion) {
|
|
27
|
+
const upgrade = `Update: ${CANONICAL_INSTALL_COMMAND} … (or: npm i -g promptdock@latest)`;
|
|
28
|
+
const title = resolve.title ? `"${resolve.title}"` : "This skill";
|
|
29
|
+
const min = resolve.min_cli_version;
|
|
30
|
+
if (typeof min === "string" && min.length > 0 && cmpVersion(cliVersion, min) < 0) {
|
|
31
|
+
throw new CliError(`${title} needs promptdock CLI ${min} or newer — you are on ${cliVersion}.`, EXIT.DENIED, { footer: "upgrade_required", hint: upgrade });
|
|
32
|
+
}
|
|
33
|
+
// Belt and braces for a server too old to send min_cli_version: compare the shape
|
|
34
|
+
// the resolve response already reports against this CLI's own fuses.
|
|
35
|
+
const files = Number(resolve.file_count);
|
|
36
|
+
if (Number.isFinite(files) && files > CLI_DOS_MAX_SKILL_FILES) {
|
|
37
|
+
throw new CliError(`${title} has ${files} files — this CLI installs at most ${CLI_DOS_MAX_SKILL_FILES}.`, EXIT.DENIED, { footer: "upgrade_required", hint: upgrade });
|
|
38
|
+
}
|
|
39
|
+
const total = Number(resolve.total_bytes);
|
|
40
|
+
if (Number.isFinite(total) && total > CLI_DOS_MAX_SKILL_TOTAL_BYTES) {
|
|
41
|
+
const mb = (n) => `${(n / (1024 * 1024)).toFixed(1)}MB`;
|
|
42
|
+
throw new CliError(`${title} is ${mb(total)} — this CLI installs at most ${mb(CLI_DOS_MAX_SKILL_TOTAL_BYTES)}.`, EXIT.DENIED, { footer: "upgrade_required", hint: upgrade });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
4
45
|
/** Throws the DX3-copy CliError for a deny verdict; returns for ok/already_entitled. */
|
|
5
|
-
export function assertInstallable(resolve, refString) {
|
|
46
|
+
export function assertInstallable(resolve, refString, cliVersion) {
|
|
6
47
|
switch (resolve.verdict) {
|
|
7
48
|
case "ok":
|
|
8
49
|
case "already_entitled":
|
|
50
|
+
assertPackageCompatible(resolve, cliVersion);
|
|
9
51
|
return;
|
|
10
52
|
case "not_found":
|
|
11
53
|
throw new CliError(`skill not found: ${refString} — check the ref (the format is handle/slug; a pasted promptdock.ai skill URL also works)`, EXIT.DENIED, { footer: "not_found" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "promptdock",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Install AI agent skills from PromptDock — npx promptdock@latest install <handle>/<slug>",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"promptdock",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"typescript": "^5",
|
|
43
43
|
"vitest": "^4.1.8"
|
|
44
44
|
},
|
|
45
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "53545d74fc0baeec63684c96b49134e93625d63a"
|
|
46
46
|
}
|