promptdock 1.2.1-rc.15.1 → 1.2.2
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 +67 -21
- package/dist/auth.js +8 -2
- package/dist/commands/install.js +114 -41
- package/dist/commands/lifecycle.js +117 -19
- package/dist/context.d.ts +9 -0
- package/dist/context.js +56 -0
- package/dist/errors.d.ts +21 -1
- package/dist/errors.js +12 -1
- package/dist/help.d.ts +2 -0
- package/dist/help.js +25 -6
- package/dist/index.js +4 -1
- package/dist/installer.d.ts +88 -15
- package/dist/installer.js +356 -98
- package/dist/integrity.d.ts +13 -0
- package/dist/integrity.js +27 -0
- package/dist/leftovers.d.ts +32 -0
- package/dist/leftovers.js +175 -0
- package/dist/local-changes.d.ts +108 -0
- package/dist/local-changes.js +334 -0
- package/dist/receipts.d.ts +2 -1
- package/dist/receipts.js +10 -3
- package/dist/semver.d.ts +19 -0
- package/dist/semver.js +54 -0
- package/dist/terminal-text.d.ts +4 -3
- package/dist/terminal-text.js +4 -3
- package/dist/verdicts.d.ts +2 -0
- package/dist/verdicts.js +24 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,9 @@ Codex, Gemini CLI, Cursor, GitHub Copilot, and more.
|
|
|
7
7
|
npx promptdock@latest install <handle>/<slug>
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Quickstart
|
|
11
|
+
|
|
12
|
+
Signed in: about 1-2 minutes. New account by email: about 2-5 minutes.
|
|
11
13
|
|
|
12
14
|
Replace `<handle>/<slug>` with a real skill's ref — every skill page on
|
|
13
15
|
[promptdock.ai/skills](https://www.promptdock.ai/skills) shows its own install command,
|
|
@@ -19,17 +21,17 @@ npx promptdock@latest install <handle>/<slug>
|
|
|
19
21
|
|
|
20
22
|
# 2) Approve the login code in your browser when prompted (one time per machine)
|
|
21
23
|
# No account yet? The page it opens has a sign-up link: create the account,
|
|
22
|
-
# confirm the email it sends, then
|
|
23
|
-
#
|
|
24
|
-
# fresh one once.
|
|
24
|
+
# confirm the email it sends, then enter the code from your terminal if the
|
|
25
|
+
# page asks for it. The code lasts 15 minutes for exactly this, the terminal
|
|
26
|
+
# waits, and if it does run out the CLI issues a fresh one once.
|
|
25
27
|
|
|
26
28
|
# 3) Start a NEW session in Claude Code and type /<slug>
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
**Auth model, stated plainly: login is required.** A free PromptDock account
|
|
30
|
-
installs free skills — no card, no subscription. Premium skills
|
|
31
|
-
|
|
32
|
-
one-time code and the terminal continues. The token lives in
|
|
32
|
+
installs free skills — no card, no subscription. Premium skills come with any
|
|
33
|
+
paid plan ([pricing](https://www.promptdock.ai/pricing)). `login` opens your
|
|
34
|
+
browser; you approve a one-time code and the terminal continues. The token lives in
|
|
33
35
|
`~/.promptdock/config.json` (owner-only permissions; on Windows the user
|
|
34
36
|
profile's ACL is the boundary).
|
|
35
37
|
|
|
@@ -42,11 +44,13 @@ profile's ACL is the boundary).
|
|
|
42
44
|
| `whoami [--json]` | Show the signed-in account + token expiry |
|
|
43
45
|
| `install <ref> [options]` | Install a skill (`ref` = `handle/slug`, `@handle/slug`, or a pasted URL) |
|
|
44
46
|
| `uninstall <ref \| --all>` | Remove an installed skill (receipt-driven — see below) |
|
|
45
|
-
| `update [ref] [--all] [--check]` | Update to the latest approved version (`upgrade` is an alias) |
|
|
47
|
+
| `update [ref] [--all] [--check]` | Update installed skills to the latest approved version (`upgrade` is an alias; it updates skills, not this CLI: `npm i -g promptdock@latest`) |
|
|
46
48
|
| `list [--json] [-g] [--all]` | Show installed skills |
|
|
47
49
|
|
|
48
50
|
Install options: `-g/--global` (per-user dir), `--target <tool>`, `--dir <path>`,
|
|
49
|
-
`-y/--yes` (skip prompts — **never** implies `--force`), `--force
|
|
51
|
+
`-y/--yes` (skip prompts — **never** implies `--force`), `--force` (replace the files
|
|
52
|
+
you edited and your files at paths the new version ships, keeping every other file
|
|
53
|
+
you added; or replace another skill's folder, listing what it removed), `--dry-run`
|
|
50
54
|
(print the plan, install nothing), `--json`.
|
|
51
55
|
|
|
52
56
|
Update/uninstall options: `--check` (update only — report, apply nothing), `--all`,
|
|
@@ -149,19 +153,25 @@ confirms from painting a picker over your JSON.
|
|
|
149
153
|
| --- | --- |
|
|
150
154
|
| `whoami --json` | `{ handle, role, expires_at }` |
|
|
151
155
|
| `list --json` | `[{ dir, target, scope, ref, version, version_id, installed_at, schema }]` — an unreadable receipt yields `{ dir, target, scope, receipt: "newer_schema" \| "invalid" }` instead |
|
|
152
|
-
| `install --json` | `{ installed: true, update, ref, version, version_id, target, path, files }` |
|
|
153
|
-
| `install --json` (nothing to do) | `{ installed: false, up_to_date: true, update: true, ref, version, version_id, target, path }` |
|
|
154
|
-
| `install --dry-run --json` | `{ dry_run: true, ref, skill_id, version_id, version, title, author, is_free, file_count, total_bytes, target, path, update }` |
|
|
156
|
+
| `install --json` | `{ installed: true, update, ref, version, version_id, target, path, files, kept_files, replaced?, removed? }` — `replaced` under `--force`, `removed` when another skill's folder was replaced |
|
|
157
|
+
| `install --json` (nothing to do) | `{ installed: false, up_to_date: true, update: true, ref, version, version_id, target, path, changes? }` |
|
|
158
|
+
| `install --dry-run --json` | `{ dry_run: true, ref, skill_id, version_id, version, title, author, is_free, file_count, total_bytes, target, path, update, changes? }` |
|
|
155
159
|
| `update --json` | `{ results: [ … ] }` — always, including when nothing is installed (`{ results: [] }`) |
|
|
156
160
|
| `uninstall --json` | `{ uninstalled: [{ ref, dir }], results: [ … ] }` |
|
|
157
161
|
|
|
158
|
-
A `results` row is `{ ref, dir, status, reason?, failed?, exit?, from?, to? }`:
|
|
162
|
+
A `results` row is `{ ref, dir, status, reason?, failed?, exit?, code?, changes?, kept_files?, replaced?, from?, to? }`:
|
|
159
163
|
|
|
160
164
|
- `status` — `updated` · `up_to_date` · `available` (`--check`) · `uninstalled` ·
|
|
161
165
|
`skipped` (declined, or failed).
|
|
162
166
|
- `failed` — present and `true` **only** for a real failure. This is the field to
|
|
163
167
|
branch on; `status` keeps the vocabulary 1.2.0 shipped.
|
|
164
168
|
- `exit` — that failure's own exit code. The process exits with the first one.
|
|
169
|
+
- `code` — that failure's docs anchor (`local_changes`, `download`, …), from 1.2.2.
|
|
170
|
+
- `changes` — `[{ path, reason }]` for the local changes that blocked (or, on an
|
|
171
|
+
`available` row from `--check`, that would block); `reason` is `modified`,
|
|
172
|
+
`path_conflict` or `not_regular_file`.
|
|
173
|
+
- `kept_files` / `replaced` — on an `updated` row: the files of yours that were kept,
|
|
174
|
+
and what `--force` replaced.
|
|
165
175
|
- `from`/`to` — version numbers, on `update` rows only.
|
|
166
176
|
|
|
167
177
|
A **failure** that produced no report of its own is written to stdout as one document:
|
|
@@ -170,6 +180,8 @@ A **failure** that produced no report of its own is written to stdout as one doc
|
|
|
170
180
|
{ "error": { "code": "existing_dir", "message": "…", "exit_code": 5, "doc_url": "https://www.promptdock.ai/docs/cli/errors#existing_dir" } }
|
|
171
181
|
```
|
|
172
182
|
|
|
183
|
+
A `local_changes` refusal adds `"changes": [{ "path", "reason" }]` to `error`.
|
|
184
|
+
|
|
173
185
|
`code` is the docs anchor (`"error"` when the failure has none, and `doc_url` then
|
|
174
186
|
points at the index). `update`/`uninstall` do **not** add this — their `results` report
|
|
175
187
|
already answers the same question per item — so stdout is always exactly one JSON
|
|
@@ -189,15 +201,24 @@ skill id, ref, version, and the sha256 of every installed file. It is the
|
|
|
189
201
|
project; `-g` the global scope; `--all` both.
|
|
190
202
|
- `uninstall` removes **only** files the receipt lists, then the receipt, then
|
|
191
203
|
any emptied directories. Files you added are never touched.
|
|
192
|
-
-
|
|
193
|
-
|
|
204
|
+
- `install` and `update` **keep the files you added** (and your folders and links,
|
|
205
|
+
which are never followed) when they install a new version.
|
|
206
|
+
- If you **edited** installed files, or put a file of yours where the new version
|
|
207
|
+
ships one, `install`, `update` and `uninstall` stop with exit 4 and change nothing.
|
|
208
|
+
The message lists each file with its reason and both ways out: move the files out
|
|
209
|
+
and run the same command again, or add `--force`, which replaces the listed files
|
|
210
|
+
and keeps every other file you added. Line endings alone (a CRLF checkout) are not
|
|
211
|
+
an edit.
|
|
194
212
|
- A receipt written by a newer CLI version is left alone with a notice naming
|
|
195
213
|
the exact `npx promptdock@latest …` command to re-run — never a guessy
|
|
196
214
|
uninstall. It counts as a failed item (see [batch exit codes](#batch-exit-codes)).
|
|
197
215
|
|
|
198
216
|
Downloads are verified against the server manifest (per-file sha256) and every
|
|
199
217
|
path is re-validated before any write; a failed check aborts **all-or-nothing**
|
|
200
|
-
("integrity check failed — nothing installed").
|
|
218
|
+
("integrity check failed — nothing installed"). Files download into a temporary
|
|
219
|
+
folder above the skills folder, so an interrupted install never leaves a half-copied
|
|
220
|
+
skill where your agent looks for skills; Ctrl-C removes it, and if an update is cut
|
|
221
|
+
off mid-swap the next promptdock command puts your folder back.
|
|
201
222
|
|
|
202
223
|
## Exit codes & errors
|
|
203
224
|
|
|
@@ -207,10 +228,10 @@ path is re-validated before any write; a failed check aborts **all-or-nothing**
|
|
|
207
228
|
| 1 | usage (bad flags, missing `--target`/`-y` in CI) |
|
|
208
229
|
| 2 | auth (not logged in / token rejected) |
|
|
209
230
|
| 3 | denied (tier gate, daily cap, denial, rate limit, version floor, not found) |
|
|
210
|
-
| 4 | integrity
|
|
231
|
+
| 4 | integrity or a local-safety conflict: checksum, unsafe path, receipt schema or local changes (an edited file, a file where the new version ships one, a special file) that an install or update would otherwise destroy |
|
|
211
232
|
| 5 | filesystem (permissions, disk space, non-empty directory) |
|
|
212
|
-
| 6 | network |
|
|
213
|
-
| 130 | interrupted (`Ctrl-C` during
|
|
233
|
+
| 6 | network (including a connection that drops mid-download) |
|
|
234
|
+
| 130 | interrupted (`Ctrl-C` during a prompt or a download; `Esc` — a deliberate in-UI cancel — exits 1) |
|
|
214
235
|
|
|
215
236
|
Every named error links `https://www.promptdock.ai/docs/cli/errors#<code>`. That page
|
|
216
237
|
lists every code with what happened, why, and what to do.
|
|
@@ -252,11 +273,36 @@ updates everything else and then exits `3` for the one it could not do (see
|
|
|
252
273
|
|
|
253
274
|
- Node.js ≥ 18. Zero runtime dependencies.
|
|
254
275
|
- The CLI always installs the **latest approved** version of a skill (no semver
|
|
255
|
-
pinning)
|
|
256
|
-
|
|
276
|
+
pinning). Skills that members submit are reviewed by a person before they go
|
|
277
|
+
live; review is a screening step, not a warranty — read what you install.
|
|
257
278
|
- Skills are plain files on your machine after install; premium gates access,
|
|
258
279
|
not copying.
|
|
259
280
|
|
|
281
|
+
## Changes
|
|
282
|
+
|
|
283
|
+
- **1.2.2:**
|
|
284
|
+
- `install` refuses to overwrite files you edited (exit 4). Before, it exited 0
|
|
285
|
+
and the edits were lost.
|
|
286
|
+
- `install` and `update` keep the files you added; before, they deleted them.
|
|
287
|
+
- A connection that drops during a download exits 6 (was 1) and names the file;
|
|
288
|
+
expired download links are refreshed once, by themselves.
|
|
289
|
+
- CRLF checkouts no longer count as edits.
|
|
290
|
+
- Temporary files live above the skills folder; Ctrl-C removes them, and the next
|
|
291
|
+
command restores a folder an interrupted update set aside.
|
|
292
|
+
- `promptdock upgrade` says it updates your skills, not the CLI.
|
|
293
|
+
- The receipt schema stays 1.
|
|
294
|
+
- Why older versions are refused for installs and updates: promptdock 1.2.1 and
|
|
295
|
+
older can overwrite files you edited and delete files you added when they update
|
|
296
|
+
a skill, so installing and updating need 1.2.2 or later; login, list and
|
|
297
|
+
uninstall still work. 1.2.1's `update --all` prints that refusal once per
|
|
298
|
+
installed skill.
|
|
299
|
+
- Did 1.2.1 overwrite your edits? Re-running `install` over an edited skill
|
|
300
|
+
replaced the edited files and deleted the ones you had added. Compare the folder
|
|
301
|
+
with your version control.
|
|
302
|
+
- **1.2.1:** the premium refusal names no price: "Premium skills come with any
|
|
303
|
+
paid plan", with the pricing link (the old "$10/mo" was wrong in euros and
|
|
304
|
+
rupees).
|
|
305
|
+
|
|
260
306
|
## License
|
|
261
307
|
|
|
262
308
|
[MIT](./LICENSE) © Prompt Dock, Inc.
|
package/dist/auth.js
CHANGED
|
@@ -34,13 +34,19 @@ export async function deviceFlowLogin(ctx, baseUrl, opts) {
|
|
|
34
34
|
ctx.openUrl(start.verification_url);
|
|
35
35
|
ctx.io.out("(if the page didn't open, paste the URL into your browser)");
|
|
36
36
|
}
|
|
37
|
-
ctx.io.out("");
|
|
38
37
|
// Clamp through Number.isFinite (review find): a missing/renamed numeric in
|
|
39
38
|
// the start response made `deadline` NaN, and `remainMs <= 0` is FALSE for
|
|
40
39
|
// NaN — an infinite countdown spin. A malformed numeric now degrades to the
|
|
41
40
|
// protocol defaults instead of hanging the login.
|
|
42
41
|
const expiresIn = Number(start.expires_in);
|
|
43
|
-
const
|
|
42
|
+
const lifetimeSecs = Number.isFinite(expiresIn) && expiresIn > 0 ? expiresIn : 600;
|
|
43
|
+
// DX-A4: a member signing up on that page confirms their email first, and the
|
|
44
|
+
// confirmation link can land them on a page that asks for this code. Say so here,
|
|
45
|
+
// where they still have the code in front of them.
|
|
46
|
+
const minutes = Math.max(1, Math.floor(lifetimeSecs / 60));
|
|
47
|
+
ctx.io.out(`New here? Sign up on that page and confirm your email. If a page then asks for a code, enter ${terminalLine(start.user_code, SERVER_LABEL_MAX)}. This terminal waits up to ${minutes} ${minutes === 1 ? "minute" : "minutes"}.`);
|
|
48
|
+
ctx.io.out("");
|
|
49
|
+
const deadline = ctx.now() + lifetimeSecs * 1000;
|
|
44
50
|
const interval0 = Number(start.interval);
|
|
45
51
|
let intervalSecs = Number.isFinite(interval0) && interval0 > 0 ? interval0 : 5;
|
|
46
52
|
poll: for (;;) {
|
package/dist/commands/install.js
CHANGED
|
@@ -9,6 +9,8 @@ import { newerSchemaError, rerunCommand } from "./lifecycle.js";
|
|
|
9
9
|
import { CliError, EXIT, usageError } from "../errors.js";
|
|
10
10
|
import { SERVER_LABEL_MAX, SERVER_PATH_MAX, terminalLine } from "../terminal-text.js";
|
|
11
11
|
import { checkInstallResponse, performInstall } from "../installer.js";
|
|
12
|
+
import { sweepAndReport, tempRootsFor } from "../leftovers.js";
|
|
13
|
+
import { changeRows, changesJson, defaultPathKey, forceLines, keptLine, localChangesRefusal, receiptSideChanges, unlistedEntries, } from "../local-changes.js";
|
|
12
14
|
import { formatRef, parseSkillRef } from "../ref.js";
|
|
13
15
|
import { detectionLabel, detectTargets, detectTargetsAnyScope, nextStepLine, scopeChoices, targetById, targetInstallDir, TARGETS, } from "../registry.js";
|
|
14
16
|
import { readReceipt } from "../receipts.js";
|
|
@@ -37,6 +39,10 @@ export async function runInstall(ctx, positionals, flags) {
|
|
|
37
39
|
}
|
|
38
40
|
// ── target resolution (D-UX11) ─────────────────────────────────────────────
|
|
39
41
|
const picked = await resolveTarget(ctx, flags, ref);
|
|
42
|
+
// An interrupted install or update may have left this skill's folder set aside: put it
|
|
43
|
+
// back BEFORE reading its receipt (ENG-A4). A dry run writes nothing, so it skips this.
|
|
44
|
+
if (!dryRun)
|
|
45
|
+
sweepAndReport(ctx, tempRootsFor(picked.dir));
|
|
40
46
|
// ── receipt awareness (DX3/DX5) ────────────────────────────────────────────
|
|
41
47
|
const existing = readReceipt(picked.dir);
|
|
42
48
|
if (existing.kind === "newer_schema") {
|
|
@@ -46,26 +52,15 @@ export async function runInstall(ctx, positionals, flags) {
|
|
|
46
52
|
? existing.receipt
|
|
47
53
|
: null;
|
|
48
54
|
const isUpdate = matching !== null;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
up_to_date: true,
|
|
59
|
-
update: true,
|
|
60
|
-
ref: refString,
|
|
61
|
-
version: matching.version,
|
|
62
|
-
version_id: matching.version_id,
|
|
63
|
-
target: picked.id,
|
|
64
|
-
path: picked.dir,
|
|
65
|
-
})
|
|
66
|
-
: `"${terminalLine(resolve.title, SERVER_LABEL_MAX) || refString}" is already up to date (v${matching.version}) at ${picked.dir} — use --force to reinstall.`);
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
55
|
+
const force = flags.force === true;
|
|
56
|
+
const occupied = dirIsNonEmpty(picked.dir);
|
|
57
|
+
const existingTarget = matching
|
|
58
|
+
? { kind: "update", receipt: matching }
|
|
59
|
+
: !occupied
|
|
60
|
+
? { kind: "none" }
|
|
61
|
+
: existing.kind === "ok"
|
|
62
|
+
? { kind: "foreign", receipt: existing.receipt }
|
|
63
|
+
: { kind: "unmanaged" };
|
|
69
64
|
// ⚠️ SANITIZE THE FRAGMENTS, NOT THE LINES. The summary pads its labels into
|
|
70
65
|
// columns (" version: v3"), and `terminalLine` collapses whitespace runs — one
|
|
71
66
|
// pass over a finished line would flatten every one of them.
|
|
@@ -82,6 +77,56 @@ export async function runInstall(ctx, positionals, flags) {
|
|
|
82
77
|
// files are about to be written. A crafted directory name could erase it, which
|
|
83
78
|
// defeats confirm-before-write itself.
|
|
84
79
|
const safeDir = terminalLine(picked.dir, SERVER_PATH_MAX);
|
|
80
|
+
// ── local changes the receipt can see (I1, DX-A1): before the confirm prompt and
|
|
81
|
+
// before the metered install request. Reads only, so a dry run reports them too.
|
|
82
|
+
const localChanges = matching
|
|
83
|
+
? receiptSideChanges(picked.dir, matching, unlistedEntries(picked.dir, matching, defaultPathKey(ctx.platform)))
|
|
84
|
+
: [];
|
|
85
|
+
if (!isUpdate && !dryRun && !force && occupied) {
|
|
86
|
+
// The ONLY error whose remedy is --force (DX3). What --force does differs by what is
|
|
87
|
+
// there (DX-A1): another skill's folder is replaced and listed; files with no receipt
|
|
88
|
+
// (what an uninstall that kept your files leaves) are kept.
|
|
89
|
+
throw new CliError(existing.kind === "ok"
|
|
90
|
+
? `${picked.dir} already holds a different skill (${terminalLine(existing.receipt.ref, SERVER_LABEL_MAX)}) — pass --force to replace it (the CLI lists what it removes)`
|
|
91
|
+
: `${picked.dir} already exists and contains files — pass --force to install into it; your files are kept unless the new version ships the same path`, EXIT.IO, { footer: "existing_dir" });
|
|
92
|
+
}
|
|
93
|
+
if (isUpdate && matching.version_id === resolve.version_id && !force && !dryRun) {
|
|
94
|
+
if (json) {
|
|
95
|
+
ctx.io.out(JSON.stringify({
|
|
96
|
+
installed: false,
|
|
97
|
+
up_to_date: true,
|
|
98
|
+
update: true,
|
|
99
|
+
ref: refString,
|
|
100
|
+
version: matching.version,
|
|
101
|
+
version_id: matching.version_id,
|
|
102
|
+
target: picked.id,
|
|
103
|
+
path: picked.dir,
|
|
104
|
+
...(localChanges.length > 0 ? { changes: changesJson(localChanges) } : {}),
|
|
105
|
+
}));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (localChanges.length === 0) {
|
|
109
|
+
ctx.io.out(`"${safeTitle}" is already up to date (v${ver(matching.version)}) at ${safeDir} — use --force to reinstall.`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// DX-A1: a member with edits is NOT sent straight to the flag that replaces them.
|
|
113
|
+
const k = localChanges.length;
|
|
114
|
+
ctx.io.out(`"${safeTitle}" is up to date (v${ver(matching.version)}) at ${safeDir}, with ${k} local ${k === 1 ? "change" : "changes"}:`);
|
|
115
|
+
for (const row of changeRows(localChanges, null))
|
|
116
|
+
ctx.io.out(row);
|
|
117
|
+
ctx.io.out("--force would replace them and keep every other file you added.");
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (isUpdate && localChanges.length > 0 && !force && !dryRun) {
|
|
121
|
+
throw localChangesRefusal({
|
|
122
|
+
verb: "install",
|
|
123
|
+
title: safeTitle,
|
|
124
|
+
from: ver(matching.version),
|
|
125
|
+
to: ver(resolve.version),
|
|
126
|
+
dir: picked.dir,
|
|
127
|
+
changes: localChanges,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
85
130
|
// ── summary block (D-UX11 confirm-before-write) ────────────────────────────
|
|
86
131
|
const summary = [];
|
|
87
132
|
if (isUpdate) {
|
|
@@ -113,11 +158,18 @@ export async function runInstall(ctx, positionals, flags) {
|
|
|
113
158
|
target: picked.id,
|
|
114
159
|
path: picked.dir,
|
|
115
160
|
update: isUpdate,
|
|
161
|
+
...(localChanges.length > 0 ? { changes: changesJson(localChanges) } : {}),
|
|
116
162
|
}));
|
|
117
163
|
return;
|
|
118
164
|
}
|
|
119
165
|
for (const line of summary)
|
|
120
166
|
ctx.io.out(line);
|
|
167
|
+
if (localChanges.length > 0 && !force && matching?.version_id !== resolve.version_id) {
|
|
168
|
+
const k = localChanges.length;
|
|
169
|
+
ctx.io.out(`${k} local ${k === 1 ? "change" : "changes"} would block this install:`);
|
|
170
|
+
for (const row of changeRows(localChanges, ver(resolve.version)))
|
|
171
|
+
ctx.io.out(row);
|
|
172
|
+
}
|
|
121
173
|
ctx.io.out("");
|
|
122
174
|
ctx.io.out(c.dim("Dry run — nothing was installed (the file list is fetched and verified at install)."));
|
|
123
175
|
return;
|
|
@@ -136,31 +188,44 @@ export async function runInstall(ctx, positionals, flags) {
|
|
|
136
188
|
}
|
|
137
189
|
}
|
|
138
190
|
// ── two-phase install ──────────────────────────────────────────────────────
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
// it at the source would change what the integrity check runs on. Note these are
|
|
147
|
-
// printed BEFORE `assertSafeManifest` validates them (checkInstallResponse →
|
|
148
|
-
// print → performInstall → validate), so a path that is about to be REFUSED is
|
|
149
|
-
// rendered here; that ordering is what makes sanitizing this line necessary.
|
|
150
|
-
for (const e of shown) {
|
|
151
|
-
ctx.io.out(` ${terminalLine(e.path, SERVER_PATH_MAX)} ${c.dim(`(${formatBytes(e.bytes)})`)}`);
|
|
152
|
-
}
|
|
153
|
-
if (resp.manifest.length > shown.length) {
|
|
154
|
-
ctx.io.out(c.dim(` +${resp.manifest.length - shown.length} more`));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const receipt = await performInstall(ctx, api, {
|
|
191
|
+
// The same POST serves I9's one re-sign: `record_skill_install` is idempotent per
|
|
192
|
+
// member and skill, so a second call refreshes the links and records nothing new.
|
|
193
|
+
const postInstall = async () => checkInstallResponse(await api.request("POST", `/api/v1/cli/skills/${resolve.skill_id}/install`, {
|
|
194
|
+
version_id: resolve.version_id,
|
|
195
|
+
}));
|
|
196
|
+
const resp = await postInstall();
|
|
197
|
+
const outcome = await performInstall(ctx, api, {
|
|
158
198
|
resp,
|
|
159
199
|
targetDir: picked.dir,
|
|
160
200
|
targetId: picked.id,
|
|
161
201
|
refString,
|
|
162
|
-
|
|
202
|
+
existing: existingTarget,
|
|
203
|
+
force,
|
|
204
|
+
verb: "install",
|
|
205
|
+
title: safeTitle,
|
|
206
|
+
resign: postInstall,
|
|
207
|
+
onDownload: json
|
|
208
|
+
? undefined
|
|
209
|
+
: (r) => {
|
|
210
|
+
ctx.io.out(`Downloading ${r.manifest.length} files…`);
|
|
211
|
+
const shown = r.manifest.slice(0, 8);
|
|
212
|
+
// ⚠️ DISPLAY ONLY. `e.path` names the file `performInstall` writes and the
|
|
213
|
+
// sha256 it verifies against, so the value itself must stay byte-identical —
|
|
214
|
+
// sanitizing it at the source would change what the integrity check runs on.
|
|
215
|
+
// These are printed BEFORE `assertSafeManifest` validates them, so a path that
|
|
216
|
+
// is about to be REFUSED is rendered here; that ordering is what makes
|
|
217
|
+
// sanitizing this line necessary.
|
|
218
|
+
for (const e of shown) {
|
|
219
|
+
ctx.io.out(` ${terminalLine(e.path, SERVER_PATH_MAX)} ${c.dim(`(${formatBytes(e.bytes)})`)}`);
|
|
220
|
+
}
|
|
221
|
+
if (r.manifest.length > shown.length) {
|
|
222
|
+
ctx.io.out(c.dim(` +${r.manifest.length - shown.length} more`));
|
|
223
|
+
}
|
|
224
|
+
},
|
|
163
225
|
});
|
|
226
|
+
const { receipt } = outcome;
|
|
227
|
+
for (const w of outcome.warnings)
|
|
228
|
+
ctx.io.err(`${c.yellow("!")} ${w}`);
|
|
164
229
|
if (json) {
|
|
165
230
|
ctx.io.out(JSON.stringify({
|
|
166
231
|
installed: true,
|
|
@@ -171,11 +236,19 @@ export async function runInstall(ctx, positionals, flags) {
|
|
|
171
236
|
target: picked.id,
|
|
172
237
|
path: picked.dir,
|
|
173
238
|
files: receipt.files.length,
|
|
239
|
+
kept_files: outcome.kept,
|
|
240
|
+
...(force ? { replaced: outcome.replaced.map(({ path, reason }) => ({ path, reason })) } : {}),
|
|
241
|
+
...(outcome.removed.length > 0 ? { removed: outcome.removed } : {}),
|
|
174
242
|
}));
|
|
175
243
|
return;
|
|
176
244
|
}
|
|
177
245
|
ctx.io.out("");
|
|
178
|
-
ctx.io.out(`${c.green("✓")} ${isUpdate ? "Updated" : "Installed"} "${terminalLine(resp.skill.title, SERVER_LABEL_MAX)}" v${terminalLine(String(
|
|
246
|
+
ctx.io.out(`${c.green("✓")} ${isUpdate ? "Updated" : "Installed"} "${terminalLine(resp.skill.title, SERVER_LABEL_MAX)}" v${terminalLine(String(receipt.version), SERVER_LABEL_MAX)} → ${terminalLine(picked.dir, SERVER_PATH_MAX)}`);
|
|
247
|
+
const kept = keptLine(outcome.kept);
|
|
248
|
+
if (kept)
|
|
249
|
+
ctx.io.out(kept);
|
|
250
|
+
for (const line of forceLines(outcome.replaced, outcome.removed))
|
|
251
|
+
ctx.io.out(line);
|
|
179
252
|
ctx.io.out(nextStepLine(picked.id, resp.skill.slug));
|
|
180
253
|
}
|
|
181
254
|
/**
|