@iseoane/agent-depot 0.6.6 → 0.6.8
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/CHANGELOG.md +28 -0
- package/README.md +20 -11
- package/dist/src/cli.js +58 -17
- package/dist/src/package-bundles.js +3 -3
- package/dist/src/package-cli.js +7 -3
- package/dist/src/package-flow.js +20 -7
- package/dist/src/package-host-plans.js +18 -3
- package/dist/src/project-installation.js +66 -20
- package/dist/src/skill-removal.js +46 -9
- package/dist/src/tui/bulk-actions.js +3 -1
- package/dist/src/tui/catalog-installs.js +27 -8
- package/dist/src/tui/installations-rows.js +1 -1
- package/dist/src/tui/manage-actions.js +3 -1
- package/dist/src/tui/package-rows.js +12 -8
- package/dist/src/tui/updates-panel.js +1 -1
- package/dist/src/tui/updates.js +4 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.8] - 2026-11-04
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Versioned Claude Packages now follow the Host's literal plugin cache version,
|
|
15
|
+
with the plugin manifest taking precedence over marketplace metadata. A newer
|
|
16
|
+
Source commit at the same version is explained instead of offered as an update
|
|
17
|
+
Claude cannot apply. Successful literal-version changes are recognized even
|
|
18
|
+
when the commit is unchanged or the version strings normalize identically.
|
|
19
|
+
- Package comparisons and results in the CLI and TUI now show the observed Host
|
|
20
|
+
version and commit together. Remembered evidence is labelled as recorded;
|
|
21
|
+
Pi updates between different commits at the same manifest version remain
|
|
22
|
+
available when verified Source ancestry proves progress.
|
|
23
|
+
|
|
24
|
+
## [0.6.7] - 2026-11-04
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Claude Skill installations on Windows now use directory junctions instead of
|
|
29
|
+
privilege-sensitive directory symlinks. Installation, additional Host exposure
|
|
30
|
+
and overwrite share the same creation policy; POSIX directory symlinks remain
|
|
31
|
+
unchanged. Rollback removes the managed link without following its target.
|
|
32
|
+
- User-global Skill removal can now explicitly forget a missing installation
|
|
33
|
+
record, without removing files, so the Skill can be selected again in Catalog.
|
|
34
|
+
Recovery refuses unsafe paths, remaining Claude locations, overlapping records
|
|
35
|
+
and changes to the confirmed installation state. CLI and TUI previews and
|
|
36
|
+
completion messages distinguish record-only recovery from file removal.
|
|
37
|
+
|
|
10
38
|
## [0.6.6] - 2026-10-29
|
|
11
39
|
|
|
12
40
|
### Fixed
|
package/README.md
CHANGED
|
@@ -184,21 +184,28 @@ segment when that is unique; unknown or ambiguous selections fail before anythin
|
|
|
184
184
|
changes. It previews every deletion (with adopted/modified warnings), requires
|
|
185
185
|
`--yes`, rechecks the targets, then deletes the files and their installation
|
|
186
186
|
records. It is user-global only: project installations and manifests are never
|
|
187
|
-
read or changed, and the Git cache is retained.
|
|
187
|
+
read or changed, and the Git cache is retained. If a managed Skill's recorded
|
|
188
|
+
installation is verified absent, the preview instead offers an explicit
|
|
189
|
+
`forget missing installation record` action. Confirming removes only that stale
|
|
190
|
+
record; it never removes files. Existing files, including any Claude location,
|
|
191
|
+
unsafe ancestors, ambiguous overlapping records, or filesystem errors fail
|
|
192
|
+
closed. After the record is forgotten, the Skill can be selected and installed
|
|
193
|
+
again through the normal flow.
|
|
188
194
|
|
|
189
195
|
`skill host add <id|path> --host <host>... [--yes] [--confirm-additional-host]`
|
|
190
196
|
exposes an already installed user-global Skill to more Hosts without changing its
|
|
191
197
|
content or version, then records the new Hosts. Non-Claude Hosts share the
|
|
192
|
-
canonical `~/.agents/skills/<skill>` location and Claude
|
|
193
|
-
|
|
194
|
-
|
|
198
|
+
canonical `~/.agents/skills/<skill>` location and Claude uses a managed directory
|
|
199
|
+
link to it (relative symlink on POSIX; absolute-target junction on Windows), so
|
|
200
|
+
only a missing Claude link is ever created. The preview lists the new locations;
|
|
201
|
+
because it exposes the Skill to more Hosts it needs both `--yes` and
|
|
195
202
|
`--confirm-additional-host`. It refuses a Claude location that already holds
|
|
196
|
-
anything other than the managed
|
|
203
|
+
anything other than the managed link, and a Skill that is not installed at the
|
|
197
204
|
canonical location (exposing it would require copying it).
|
|
198
205
|
|
|
199
206
|
`skill remove <id|path> --host <host>... [--yes]` removes only those Hosts. Only
|
|
200
|
-
the managed Claude
|
|
201
|
-
every remaining Host (including the Claude
|
|
207
|
+
the managed Claude directory link can be deleted this way; the canonical directory
|
|
208
|
+
backs every remaining Host (including the Claude link) and stays until the last Host
|
|
202
209
|
is removed. Removing every recorded Host is a full removal with the usual
|
|
203
210
|
adopted/modified warnings. The targets are rechecked before deleting and the
|
|
204
211
|
record is updated afterwards. Project installations are not affected.
|
|
@@ -310,8 +317,9 @@ select candidates, or install anything automatically.
|
|
|
310
317
|
## User-global installation
|
|
311
318
|
|
|
312
319
|
User-global installations use the same canonical Host locations under the user
|
|
313
|
-
home directory (`.agents/skills/<skill>` and, when requested, a Claude
|
|
314
|
-
at `.claude/skills/<skill
|
|
320
|
+
home directory (`.agents/skills/<skill>` and, when requested, a Claude directory
|
|
321
|
+
link at `.claude/skills/<skill>`: relative symlink on POSIX, absolute-target
|
|
322
|
+
junction on Windows). They are independent from project manifests and
|
|
315
323
|
are recorded in the shared per-user Source state, so separate CLI invocations
|
|
316
324
|
reuse the same installation records. The selected Source, Skill, Hosts, and
|
|
317
325
|
actual adopted or installed location are previewed before `--yes` confirmation.
|
|
@@ -403,8 +411,9 @@ process crash after the original is moved can leave the retained
|
|
|
403
411
|
inspect the backup and target, and manually rename the retained backup back to the intended installation path when recovery is needed;
|
|
404
412
|
then rerun the install. External installation methods run only after the filesystem
|
|
405
413
|
and selected-scope state transaction succeeds.
|
|
406
|
-
Claude is exposed through a
|
|
407
|
-
installation
|
|
414
|
+
Claude is exposed through a managed directory link to the canonical
|
|
415
|
+
`.agents/skills/<skill>` installation (relative symlink on POSIX, absolute-target
|
|
416
|
+
junction on Windows).
|
|
408
417
|
|
|
409
418
|
## User-provided installation methods
|
|
410
419
|
|
package/dist/src/cli.js
CHANGED
|
@@ -15,11 +15,11 @@ import { createProgress, NO_PROGRESS } from "./progress.js";
|
|
|
15
15
|
import { skillTreeBaseline } from "./skill-discovery.js";
|
|
16
16
|
import { BUILT_IN_SOURCE, BuiltInSourceError, createSourceOperations, resolveProjectSource, SourceMetadataError, SourceNotFoundError, } from "./sources.js";
|
|
17
17
|
import { AGENT_DEPOT_PACKAGE_NAME, AGENT_DEPOT_PACKAGE_VERSION, builtinPinMismatch, defaultProjectManifestPath, parseProjectManifest, ProjectManifestStore, PROJECT_HOSTS, } from "./project-manifest.js";
|
|
18
|
-
import { inspectProjectSkillInstallation, assertNoOverlappingProjectSkillRemovalTargets, } from "./project-installation.js";
|
|
18
|
+
import { inspectAbsentManagedSkillRecord, inspectProjectSkillInstallation, assertNoOverlappingProjectSkillRemovalTargets, } from "./project-installation.js";
|
|
19
19
|
import { selectUpdateBatch, UpdateBatchSelectionError, } from "./update-batch.js";
|
|
20
20
|
import { describeUnmanagedRemoval, removeInspectedUnmanagedSkills } from "./unmanaged-removal.js";
|
|
21
21
|
import { executeHostAddition, executeHostRemoval, planHostAddition, planHostRemoval } from "./skill-hosts.js";
|
|
22
|
-
import { executeSkillRemoval, inspectSkillRemovals, planSkillRemoval, describeSkillRemoval, recheckSkillRemoval, removeSkillFiles, removeSkillsAndRecords, selectUserGlobalSkills, SkillSelectionError, userGlobalRemovalOptions, } from "./skill-removal.js";
|
|
22
|
+
import { executeSkillRemoval, inspectSkillRemovals, planSkillRemoval, describeSkillRemoval, recheckSkillRemoval, removeSkillFiles, removeSkillsAndRecords, assertManagedInstallationRecordsUnchanged, assertRemovalInspectionUnchanged, selectUserGlobalSkills, SkillSelectionError, userGlobalRemovalOptions, } from "./skill-removal.js";
|
|
23
23
|
import { defaultProjectSkillTreeAccess, executeOrRejectMethod, executeSingleInstall, externalMethodFailure, isValidFixedVersion, findSource, installOne, outputInstallationResult, outputInstallPreview, outputSingleInstallPreview, planSingleInstall, refreshResolvedSource, rejectInstallationCollision, resolveSelection, rollbackTransactions, } from "./skill-install.js";
|
|
24
24
|
import { CliUsageError } from "./usage-error.js";
|
|
25
25
|
import { createSelfUpdateOperations } from "./self-update.js";
|
|
@@ -239,7 +239,10 @@ async function runSourceRemoval(argv, operations, dependencies, output, progress
|
|
|
239
239
|
const selected = options.all
|
|
240
240
|
? dependent
|
|
241
241
|
: resolveRemovalSelections(dependent, options.requested);
|
|
242
|
-
const removalOptions = userGlobalRemovalOptions(path.resolve(dependencies.homeDirectory ?? homedir()),
|
|
242
|
+
const removalOptions = userGlobalRemovalOptions(path.resolve(dependencies.homeDirectory ?? homedir()), {
|
|
243
|
+
...dependencies.installationOptions,
|
|
244
|
+
otherInstallationPaths: installations.filter((record) => !selected.includes(record)).flatMap((record) => record.installation ? [record.installation.path] : []),
|
|
245
|
+
});
|
|
243
246
|
output(`Preview: remove Git Source ${source.id} from ${source.url}`);
|
|
244
247
|
output(`Dependent user-global Skills (${dependent.length}):`);
|
|
245
248
|
for (const [index, selection] of dependent.entries()) {
|
|
@@ -255,14 +258,33 @@ async function runSourceRemoval(argv, operations, dependencies, output, progress
|
|
|
255
258
|
requireConfirmation(options.confirmed, "Removal not confirmed; rerun with --yes after reviewing every Source and Skill preview");
|
|
256
259
|
// Recheck every selected target before deleting the first one. A changed later
|
|
257
260
|
// target must never result in a partially removed batch.
|
|
258
|
-
await progress.run("Removing Skill files", async () => {
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
+
const rechecked = await progress.run("Removing Skill files", async () => {
|
|
262
|
+
const current = await recheckSkillRemoval(plan, removalOptions);
|
|
263
|
+
assertManagedInstallationRecordsUnchanged(installations, await operations.listUserGlobalInstallations());
|
|
264
|
+
await removeSkillFiles(plan, current, removalOptions);
|
|
265
|
+
return current;
|
|
266
|
+
});
|
|
267
|
+
// Source removal persists records rather than reconciling them one by one.
|
|
268
|
+
// Revalidate every record-only target and the complete record snapshot just
|
|
269
|
+
// before that persistence boundary so a stale preview cannot forget a changed record.
|
|
270
|
+
await progress.run("Revalidating Skill records before Source removal", async () => {
|
|
271
|
+
for (const [index, inspection] of rechecked.inspections.entries()) {
|
|
272
|
+
if (!inspection.recordOnly)
|
|
273
|
+
continue;
|
|
274
|
+
const latest = await inspectAbsentManagedSkillRecord(rechecked.selections[index], removalOptions);
|
|
275
|
+
assertRemovalInspectionUnchanged(inspection, latest);
|
|
276
|
+
}
|
|
277
|
+
assertManagedInstallationRecordsUnchanged(installations, await operations.listUserGlobalInstallations());
|
|
261
278
|
});
|
|
262
279
|
await progress.run(`Removing Git Source ${source.id}`, () => operations.removeGitSource(source.id, selected));
|
|
263
280
|
output(`Removed Git Source: ${source.id}`);
|
|
264
281
|
if (selected.length > 0) {
|
|
265
|
-
|
|
282
|
+
const forgotten = plan.inspections.filter((inspection) => inspection.recordOnly).length;
|
|
283
|
+
const removed = selected.length - forgotten;
|
|
284
|
+
if (removed > 0)
|
|
285
|
+
output(`Removed ${removed} dependent user-global Skill${removed === 1 ? "" : "s"}; other dependent Skills remain tracked`);
|
|
286
|
+
if (forgotten > 0)
|
|
287
|
+
output(`Forgot ${forgotten} missing dependent Skill installation record${forgotten === 1 ? "" : "s"}; no files were removed`);
|
|
266
288
|
}
|
|
267
289
|
}
|
|
268
290
|
function parseMigrateSourceOptions(argv) {
|
|
@@ -461,7 +483,10 @@ async function runSkillRemoval(argv, operations, dependencies, output, progress)
|
|
|
461
483
|
}
|
|
462
484
|
const installations = await operations.listUserGlobalInstallations();
|
|
463
485
|
const selected = selectManagedSkills(installations, requested);
|
|
464
|
-
const removalOptions = userGlobalRemovalOptions(path.resolve(dependencies.homeDirectory ?? homedir()),
|
|
486
|
+
const removalOptions = userGlobalRemovalOptions(path.resolve(dependencies.homeDirectory ?? homedir()), {
|
|
487
|
+
...dependencies.installationOptions,
|
|
488
|
+
otherInstallationPaths: installations.filter((record) => !selected.includes(record)).flatMap((record) => record.installation ? [record.installation.path] : []),
|
|
489
|
+
});
|
|
465
490
|
if (hosts.length > 0) {
|
|
466
491
|
await runSkillHostRemoval(selected[0], hosts, installations, confirmed, operations, removalOptions, output, progress);
|
|
467
492
|
return;
|
|
@@ -473,7 +498,12 @@ async function runSkillRemoval(argv, operations, dependencies, output, progress)
|
|
|
473
498
|
output(" Installation records are reconciled after each successful Skill removal");
|
|
474
499
|
requireConfirmation(confirmed, "Removal not confirmed; rerun with --yes after reviewing every Skill preview");
|
|
475
500
|
await progress.run(`Removing ${selected.length} user-global Skill${selected.length === 1 ? "" : "s"}`, () => executeSkillRemoval(plan, operations, removalOptions, installations));
|
|
476
|
-
|
|
501
|
+
const forgotten = plan.inspections.filter((inspection) => inspection.recordOnly).length;
|
|
502
|
+
const removed = selected.length - forgotten;
|
|
503
|
+
if (removed > 0)
|
|
504
|
+
output(`Removed ${removed} user-global Skill${removed === 1 ? "" : "s"}`);
|
|
505
|
+
if (forgotten > 0)
|
|
506
|
+
output(`Forgot ${forgotten} missing user-global Skill installation record${forgotten === 1 ? "" : "s"}; no files were removed`);
|
|
477
507
|
}
|
|
478
508
|
/** Resolves `<id|path>` arguments to managed installations, reporting selection problems as usage errors. */
|
|
479
509
|
function selectManagedSkills(installations, requested) {
|
|
@@ -488,17 +518,24 @@ function selectManagedSkills(installations, requested) {
|
|
|
488
518
|
}
|
|
489
519
|
async function runSkillHostRemoval(selection, hosts, installations, confirmed, operations, options, output, progress) {
|
|
490
520
|
const plan = await planHostRemoval(selection, hosts, options).catch(rethrowSelectionError);
|
|
521
|
+
const recordOnly = plan.kind === "full" && plan.plan.inspections.every((inspection) => inspection.recordOnly);
|
|
491
522
|
output(plan.kind === "full"
|
|
492
|
-
?
|
|
523
|
+
? recordOnly
|
|
524
|
+
? "Preview: forget 1 missing user-global Skill installation record (every recorded Host is removed)"
|
|
525
|
+
: "Preview: remove 1 user-global Skill (every recorded Host is removed)"
|
|
493
526
|
: `Preview: remove ${hosts.length} Host${hosts.length === 1 ? "" : "s"} from 1 user-global Skill`);
|
|
494
527
|
for (const line of plan.kind === "full" ? plan.plan.preview : plan.preview)
|
|
495
528
|
output(line);
|
|
496
|
-
output(
|
|
529
|
+
output(recordOnly
|
|
530
|
+
? " Only the installation record will be forgotten; no files are removed"
|
|
531
|
+
: " The installation record is reconciled after the locations are removed");
|
|
497
532
|
requireConfirmation(confirmed, "Removal not confirmed; rerun with --yes after reviewing the preview");
|
|
498
533
|
await progress.run(plan.kind === "full" ? "Removing 1 user-global Skill" : `Removing Hosts from ${selection.path}`, () => executeHostRemoval(plan, operations, options, installations));
|
|
499
|
-
output(plan.kind === "full"
|
|
500
|
-
?
|
|
501
|
-
:
|
|
534
|
+
output(plan.kind === "full" && plan.plan.inspections.every((inspection) => inspection.recordOnly)
|
|
535
|
+
? `Forgot missing installation record for ${JSON.stringify(selection.path)}; no files were removed`
|
|
536
|
+
: plan.kind === "full"
|
|
537
|
+
? "Removed 1 user-global Skill"
|
|
538
|
+
: `Removed hosts ${plan.removed.join(", ")} from ${JSON.stringify(selection.path)}`);
|
|
502
539
|
}
|
|
503
540
|
async function runSkillHostAdd(argv, operations, dependencies, output, progress) {
|
|
504
541
|
const requested = [];
|
|
@@ -659,10 +696,13 @@ function assertNoManagedUnmanagedOverlap(managed, unmanaged, message) {
|
|
|
659
696
|
}
|
|
660
697
|
}
|
|
661
698
|
}
|
|
662
|
-
function outputUninstallSummary(options, removedManagedCount, output) {
|
|
663
|
-
if (options.skills) {
|
|
699
|
+
function outputUninstallSummary(options, removedManagedCount, forgottenManagedCount, output) {
|
|
700
|
+
if (options.skills && removedManagedCount > 0) {
|
|
664
701
|
output(`Removed ${removedManagedCount} managed user-global Skill${removedManagedCount === 1 ? "" : "s"}`);
|
|
665
702
|
}
|
|
703
|
+
if (options.skills && forgottenManagedCount > 0) {
|
|
704
|
+
output(`Forgot ${forgottenManagedCount} missing managed user-global Skill installation record${forgottenManagedCount === 1 ? "" : "s"}; no files were removed`);
|
|
705
|
+
}
|
|
666
706
|
if (options.unmanagedSkills.length > 0) {
|
|
667
707
|
output(`Permanently deleted ${options.unmanagedSkills.length} unmanaged user-global Skill${options.unmanagedSkills.length === 1 ? "" : "s"}`);
|
|
668
708
|
}
|
|
@@ -723,7 +763,8 @@ async function runUninstall(argv, operations, dependencies, output, progress) {
|
|
|
723
763
|
if (options.data) {
|
|
724
764
|
await progress.run("Removing catalog data", () => operations.removeCatalogData());
|
|
725
765
|
}
|
|
726
|
-
|
|
766
|
+
const forgottenManagedCount = managedRechecked.inspections.filter((inspection) => inspection.recordOnly).length;
|
|
767
|
+
outputUninstallSummary(options, selectedInstallations.length - forgottenManagedCount, forgottenManagedCount, output);
|
|
727
768
|
}
|
|
728
769
|
function selfUpdateOperationsFor(dependencies) {
|
|
729
770
|
if (dependencies.selfUpdateOperations !== undefined)
|
|
@@ -56,8 +56,8 @@ export function parseClaudePluginManifest(raw, root) {
|
|
|
56
56
|
/**
|
|
57
57
|
* Reads one marketplace and returns a descriptor per `plugins` entry. The
|
|
58
58
|
* `readPluginManifest` resolver supplies the raw plugin.json for an in-repo
|
|
59
|
-
* entry
|
|
60
|
-
*
|
|
59
|
+
* entry. The plugin manifest owns the component inventory and, when declared,
|
|
60
|
+
* its version takes precedence over marketplace entry metadata, as in Claude.
|
|
61
61
|
* A `command` source and any source outside the repository yield a descriptor
|
|
62
62
|
* with a warning rather than a throw.
|
|
63
63
|
*/
|
|
@@ -330,7 +330,7 @@ function buildMarketplaceDescriptor(entry, label, marketplaceRoot, metadataPlugi
|
|
|
330
330
|
warnings.push(`marketplace entry name ${JSON.stringify(entryName)} differs from plugin manifest name ${JSON.stringify(plugin.name)}`);
|
|
331
331
|
}
|
|
332
332
|
const entryVersion = manifestVersionEvidence(entry.version, "marketplace-entry");
|
|
333
|
-
const version =
|
|
333
|
+
const version = plugin.version ?? entryVersion;
|
|
334
334
|
return Object.freeze({
|
|
335
335
|
host: "claude",
|
|
336
336
|
coordinates: Object.freeze({ host: "claude", marketplaceRoot, pluginName: entryName }),
|
package/dist/src/package-cli.js
CHANGED
|
@@ -92,7 +92,9 @@ export function packageSelectionMatches(selection, value) {
|
|
|
92
92
|
export function describePackageUpdateCheck(check) {
|
|
93
93
|
const available = check.available === undefined ? "unknown" : describeVersion(check.available);
|
|
94
94
|
const reason = check.reason === undefined ? "" : `; ${check.reason}`;
|
|
95
|
-
|
|
95
|
+
const installed = check.observed === undefined ? "unknown (host evidence unavailable)" : describeEvidence(check.observed);
|
|
96
|
+
const commit = check.available?.kind === "manifest-version" && check.availableCommit !== undefined ? `; commit ${check.availableCommit}` : "";
|
|
97
|
+
return `Package ${formatPackageSelection(check.installed.selection)}: ${check.status}; available ${available}${commit}${reason}; host ${installed}`;
|
|
96
98
|
}
|
|
97
99
|
export function describePackageResult(result) {
|
|
98
100
|
switch (result.status) {
|
|
@@ -318,7 +320,9 @@ function formatPackageRecord(record) {
|
|
|
318
320
|
function describeInspection(selection, inspection, output) {
|
|
319
321
|
output(`Package inspect: ${formatPackageSelection(selection)}`);
|
|
320
322
|
output(` installed: ${describeEvidence(inspection.installed)}`);
|
|
321
|
-
output(` available: ${inspection.available === undefined ? "unknown" : describeVersion(inspection.available)}`);
|
|
323
|
+
output(` available: ${inspection.available === undefined ? "unknown" : describeVersion(inspection.available)}${inspection.availableCommit === undefined || inspection.available?.kind === "git-commit" ? "" : `; commit ${inspection.availableCommit}`}`);
|
|
324
|
+
if (inspection.updateReason !== undefined)
|
|
325
|
+
output(` update: ${inspection.updateReason}`);
|
|
322
326
|
output(` approval: ${inspection.approval}`);
|
|
323
327
|
if (inspection.drift !== undefined) {
|
|
324
328
|
output(` drift: ${inspection.drift}`);
|
|
@@ -389,7 +393,7 @@ function describeEvidence(evidence) {
|
|
|
389
393
|
case "installed": {
|
|
390
394
|
if (evidence.version !== undefined) {
|
|
391
395
|
const qualifier = evidence.reason === undefined ? "" : " (HEAD; local changes)";
|
|
392
|
-
return `installed ${evidence.version}${qualifier}`;
|
|
396
|
+
return `installed ${evidence.version}${evidence.commit === undefined ? "" : ` (commit ${evidence.commit})`}${qualifier}`;
|
|
393
397
|
}
|
|
394
398
|
if (evidence.commit !== undefined) {
|
|
395
399
|
return `installed commit ${evidence.commit}`;
|
package/dist/src/package-flow.js
CHANGED
|
@@ -4,7 +4,7 @@ import { sameAppVersion } from "./app-version.js";
|
|
|
4
4
|
import { createPortableExecutableResolver } from "./executable-resolution.js";
|
|
5
5
|
import { claudeMarketplaceName, discoverBundlesInSnapshot } from "./package-bundles.js";
|
|
6
6
|
import { sourceBundleScope } from "./package-content.js";
|
|
7
|
-
import { affectedBy, classifyUpdate, hostInstallId, packageApprovalDigest, planHostSteps, PackagePlanError, } from "./package-host-plans.js";
|
|
7
|
+
import { affectedBy, classifyUpdate, claudeVersionUpdateNotice, hostInstallId, packageApprovalDigest, planHostSteps, PackagePlanError, } from "./package-host-plans.js";
|
|
8
8
|
import { findInstalledBundle, readHostInstallView } from "./package-host-state.js";
|
|
9
9
|
import { activeBundles as classifyActiveBundles, portableSkillConflicts, } from "./package-owned-skills.js";
|
|
10
10
|
import { bundleRoot, describeCoordinates, selectionKey, } from "./package-model.js";
|
|
@@ -174,7 +174,9 @@ export function createPackageOperations(environment = {}) {
|
|
|
174
174
|
const installedCommitIsAncestor = bundle?.resolvedCommit === undefined || installed.kind !== "installed" || installed.commit === undefined
|
|
175
175
|
? undefined
|
|
176
176
|
: await contentAccess.isCommitAncestor?.(bundle.source, installed.commit, bundle.resolvedCommit).catch(() => undefined);
|
|
177
|
+
const updateReason = claudeVersionUpdateNotice(selection.host, installed, bundle?.available, bundle?.resolvedCommit);
|
|
177
178
|
return Object.freeze({
|
|
179
|
+
...(updateReason === undefined ? {} : { updateReason }),
|
|
178
180
|
...(bundle === undefined ? {} : { descriptor: bundle.descriptor }),
|
|
179
181
|
installed,
|
|
180
182
|
...(bundle?.available === undefined ? {} : { available: bundle.available }),
|
|
@@ -187,13 +189,16 @@ export function createPackageOperations(environment = {}) {
|
|
|
187
189
|
async function checkOne(record) {
|
|
188
190
|
try {
|
|
189
191
|
const inspection = await inspect(record.selection);
|
|
190
|
-
const status = classifyUpdate(inspection.installed, inspection.available, inspection.availableCommit, inspection.installedCommitIsAncestor);
|
|
192
|
+
const status = classifyUpdate(inspection.installed, inspection.available, inspection.availableCommit, inspection.installedCommitIsAncestor, record.selection.host);
|
|
191
193
|
return Object.freeze({
|
|
192
194
|
installed: record,
|
|
195
|
+
observed: inspection.installed,
|
|
193
196
|
status,
|
|
194
197
|
...(inspection.available === undefined ? {} : { available: inspection.available }),
|
|
195
198
|
...(inspection.availableCommit === undefined ? {} : { availableCommit: inspection.availableCommit }),
|
|
196
|
-
...(status === "unknown"
|
|
199
|
+
...(status === "unknown"
|
|
200
|
+
? { reason: inspection.drift ?? describeEvidence(inspection.installed) }
|
|
201
|
+
: inspection.updateReason === undefined ? {} : { reason: inspection.updateReason }),
|
|
197
202
|
});
|
|
198
203
|
}
|
|
199
204
|
catch (error) {
|
|
@@ -234,7 +239,7 @@ export function createPackageOperations(environment = {}) {
|
|
|
234
239
|
if (contradiction !== undefined) {
|
|
235
240
|
return { status: "failed", reason: `${contradiction}; the selection record was left untouched` };
|
|
236
241
|
}
|
|
237
|
-
const change = installedEvidenceChange(context.previous, observed);
|
|
242
|
+
const change = installedEvidenceChange(context.previous, observed, context.selection.host, context.available);
|
|
238
243
|
if (change === "unknown") {
|
|
239
244
|
return {
|
|
240
245
|
status: "failed",
|
|
@@ -242,9 +247,10 @@ export function createPackageOperations(environment = {}) {
|
|
|
242
247
|
};
|
|
243
248
|
}
|
|
244
249
|
if (change === "unchanged") {
|
|
250
|
+
const notice = claudeVersionUpdateNotice(context.selection.host, observed, context.available, context.origin.resolvedCommit);
|
|
245
251
|
return {
|
|
246
252
|
status: "failed",
|
|
247
|
-
reason: `The observed version did not change (${describeEvidence(observed)}); the selection record was left untouched`,
|
|
253
|
+
reason: `The observed version did not change (${describeEvidence(observed)}); the selection record was left untouched${notice === undefined ? "" : `. ${notice}`}`,
|
|
248
254
|
};
|
|
249
255
|
}
|
|
250
256
|
await writeSelection(context, observed);
|
|
@@ -524,15 +530,22 @@ function planDifference(plan, fresh) {
|
|
|
524
530
|
/**
|
|
525
531
|
* Three outcomes, not two. `unknown` means the two observations carry no
|
|
526
532
|
* comparable evidence, and an update must not report that as a success. A host
|
|
527
|
-
* that had nothing installed and now reports an install has changed.
|
|
533
|
+
* that had nothing installed and now reports an install has changed. Versioned
|
|
534
|
+
* Claude plugins use literal cache versions, just as update eligibility does.
|
|
528
535
|
*/
|
|
529
|
-
function installedEvidenceChange(previous, observed) {
|
|
536
|
+
function installedEvidenceChange(previous, observed, host, available) {
|
|
530
537
|
if (observed.kind !== "installed" || previous === undefined || previous.kind === "unknown") {
|
|
531
538
|
return "unknown";
|
|
532
539
|
}
|
|
533
540
|
if (previous.kind === "absent") {
|
|
534
541
|
return "changed";
|
|
535
542
|
}
|
|
543
|
+
if (host === "claude" && available?.kind === "manifest-version") {
|
|
544
|
+
if (previous.version === undefined || observed.version === undefined
|
|
545
|
+
|| previous.reason !== undefined || observed.reason !== undefined)
|
|
546
|
+
return "unknown";
|
|
547
|
+
return previous.version === observed.version ? "unchanged" : "changed";
|
|
548
|
+
}
|
|
536
549
|
const previousCommit = previous.commit === undefined ? undefined : gitCommitId(previous.commit);
|
|
537
550
|
const observedCommit = observed.commit === undefined ? undefined : gitCommitId(observed.commit);
|
|
538
551
|
if (previousCommit !== undefined && observedCommit !== undefined) {
|
|
@@ -124,13 +124,19 @@ export function packageApprovalDigest(declaration) {
|
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
126
|
* Compares installed evidence with available evidence. Manifest versions compare
|
|
127
|
-
* through `sameAppVersion` and `compareAppVersions
|
|
128
|
-
*
|
|
127
|
+
* through `sameAppVersion` and `compareAppVersions` for Pi. Claude's explicit
|
|
128
|
+
* plugin version keys its cache: only a different version installs a new copy.
|
|
129
|
+
* Unversioned bundles compare commits only with proven Source ancestry.
|
|
129
130
|
*/
|
|
130
|
-
export function classifyUpdate(installed, available, availableCommit, installedCommitIsAncestor) {
|
|
131
|
+
export function classifyUpdate(installed, available, availableCommit, installedCommitIsAncestor, host = "pi") {
|
|
131
132
|
if (installed.kind !== "installed" || installed.reason !== undefined || available === undefined) {
|
|
132
133
|
return "unknown";
|
|
133
134
|
}
|
|
135
|
+
if (host === "claude" && available.kind === "manifest-version") {
|
|
136
|
+
if (installed.version === undefined)
|
|
137
|
+
return "unknown";
|
|
138
|
+
return installed.version === available.version ? "current" : "update available";
|
|
139
|
+
}
|
|
134
140
|
if (installed.commit !== undefined) {
|
|
135
141
|
const sourceCommit = availableCommit ?? (available.kind === "git-commit" ? available.commit : undefined);
|
|
136
142
|
if (sourceCommit === undefined)
|
|
@@ -151,6 +157,15 @@ export function classifyUpdate(installed, available, availableCommit, installedC
|
|
|
151
157
|
}
|
|
152
158
|
return "unknown";
|
|
153
159
|
}
|
|
160
|
+
/** A different Source commit is not a deliverable update to Claude's versioned cache. */
|
|
161
|
+
export function claudeVersionUpdateNotice(host, installed, available, availableCommit) {
|
|
162
|
+
if (host !== "claude" || installed.kind !== "installed" || installed.reason !== undefined
|
|
163
|
+
|| available?.kind !== "manifest-version" || installed.version !== available.version
|
|
164
|
+
|| installed.commit === undefined || availableCommit === undefined
|
|
165
|
+
|| normalizeCommit(installed.commit) === normalizeCommit(availableCommit))
|
|
166
|
+
return undefined;
|
|
167
|
+
return `The Source commit differs, but Claude caches plugin version ${available.version}; the publisher must change the declared plugin version to deliver those changes.`;
|
|
168
|
+
}
|
|
154
169
|
/** Validates one host argv with the shared portable-command rules. */
|
|
155
170
|
export function assertValidHostArgv(argv, label) {
|
|
156
171
|
try {
|
|
@@ -56,7 +56,7 @@ export async function inspectProjectSkillRemoval(selection, options) {
|
|
|
56
56
|
actualBaseline.digest !== selection.installation.baseline.digest;
|
|
57
57
|
const paths = [targetPath];
|
|
58
58
|
// A Claude exposure created by Agent Depot is removable only when it is the
|
|
59
|
-
// exact
|
|
59
|
+
// exact managed directory link to this canonical installation. Never follow or
|
|
60
60
|
// remove an unrelated path merely because the Skill selected Claude.
|
|
61
61
|
const canonicalPath = path.join(projectRoot, ".agents", "skills", skillName);
|
|
62
62
|
const claudePath = path.join(projectRoot, ".claude", "skills", skillName);
|
|
@@ -65,11 +65,11 @@ export async function inspectProjectSkillRemoval(selection, options) {
|
|
|
65
65
|
try {
|
|
66
66
|
const claudeInformation = await fileSystem.lstat(claudePath);
|
|
67
67
|
if (!claudeInformation.isSymbolicLink()) {
|
|
68
|
-
throw new ProjectInstallationError("unsafe-target", `Cannot remove the tracked Skill because its Claude location is not the managed
|
|
68
|
+
throw new ProjectInstallationError("unsafe-target", `Cannot remove the tracked Skill because its Claude location is not the managed directory link: ${claudePath}; no path was changed`);
|
|
69
69
|
}
|
|
70
70
|
const target = await fileSystem.readlink(claudePath);
|
|
71
71
|
if (path.resolve(path.dirname(claudePath), target) !== canonicalPath) {
|
|
72
|
-
throw new ProjectInstallationError("unsafe-target", `Cannot remove the tracked Skill because its Claude
|
|
72
|
+
throw new ProjectInstallationError("unsafe-target", `Cannot remove the tracked Skill because its Claude directory link does not point to the canonical installation: ${claudePath}; no path was changed`);
|
|
73
73
|
}
|
|
74
74
|
paths.push(claudePath);
|
|
75
75
|
}
|
|
@@ -87,6 +87,46 @@ export async function inspectProjectSkillRemoval(selection, options) {
|
|
|
87
87
|
...(nonCanonicalPath === undefined ? {} : { nonCanonicalPath }),
|
|
88
88
|
});
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Explicitly inspects a stale user-global record without making it valid for
|
|
92
|
+
* host exposure/update. Every relevant ancestor and both managed host locations
|
|
93
|
+
* are checked; only ENOENT is accepted as absence and no filesystem entry is removed.
|
|
94
|
+
*/
|
|
95
|
+
export async function inspectAbsentManagedSkillRecord(selection, options) {
|
|
96
|
+
if (!selection.installation)
|
|
97
|
+
throw new ProjectInstallationError("removal-unavailable", "Cannot forget a Skill without a recorded installation");
|
|
98
|
+
const fileSystem = options.fileSystem ?? nodeFileSystem;
|
|
99
|
+
const root = path.resolve(options.projectRoot);
|
|
100
|
+
const target = resolveInstallationPath(root, selection.installation.path);
|
|
101
|
+
if (!isSafeRelativePath(selection.installation.path) || selection.installation.path.split("/").some((segment) => segment.toLowerCase() === ".git")) {
|
|
102
|
+
throw new ProjectInstallationError("unsafe-target", `Tracked Skill installation path is unsafe: ${JSON.stringify(selection.installation.path)}; no record was changed`);
|
|
103
|
+
}
|
|
104
|
+
await assertNoSymlinkComponents(root, fileSystem);
|
|
105
|
+
await assertDirectory(root, fileSystem, "user-global root");
|
|
106
|
+
await assertNoSymlinkAncestors(root, relativeSegments(root, target), fileSystem);
|
|
107
|
+
const skillName = skillNameFromSelection(selection);
|
|
108
|
+
const canonical = path.join(root, ".agents", "skills", skillName);
|
|
109
|
+
const claude = path.join(root, ".claude", "skills", skillName);
|
|
110
|
+
for (const otherPath of options.otherInstallationPaths ?? []) {
|
|
111
|
+
const other = resolveInstallationPath(root, otherPath);
|
|
112
|
+
if (pathsOverlap(target, other) || pathsOverlap(canonical, other) || pathsOverlap(claude, other)) {
|
|
113
|
+
throw new ProjectInstallationError("overlapping-targets", `Cannot forget ${JSON.stringify(selection.path)} because its recorded path overlaps another managed Skill record; no record was changed`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
await assertNoSymlinkAncestors(root, [".agents", "skills", skillName], fileSystem);
|
|
117
|
+
await assertNoSymlinkAncestors(root, [".claude", "skills", skillName], fileSystem);
|
|
118
|
+
for (const candidate of new Set([target, canonical, claude])) {
|
|
119
|
+
try {
|
|
120
|
+
await fileSystem.lstat(candidate);
|
|
121
|
+
throw new ProjectInstallationError("unsafe-target", `Cannot forget ${JSON.stringify(selection.path)} because a filesystem object exists at ${candidate}; no record was changed`);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (!isMissing(error))
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return Object.freeze({ skillName, paths: Object.freeze([]), digest: "absent-record-only", adopted: selection.installation.adopted, modified: false, recordOnly: true });
|
|
129
|
+
}
|
|
90
130
|
/**
|
|
91
131
|
* Removes a previously inspected user-global Skill after rechecking ownership.
|
|
92
132
|
* `deletePaths` narrows the deletion to a subset of the inspected paths (used
|
|
@@ -99,6 +139,7 @@ export async function removeProjectSkill(selection, inspection, options, deleteP
|
|
|
99
139
|
current.paths.length !== inspection.paths.length ||
|
|
100
140
|
current.paths.some((candidate, index) => candidate !== inspection.paths[index]) ||
|
|
101
141
|
current.digest !== inspection.digest ||
|
|
142
|
+
current.recordOnly !== inspection.recordOnly ||
|
|
102
143
|
current.adopted !== inspection.adopted ||
|
|
103
144
|
current.modified !== inspection.modified ||
|
|
104
145
|
current.nonCanonicalPath !== inspection.nonCanonicalPath) {
|
|
@@ -120,9 +161,9 @@ export async function removeProjectSkill(selection, inspection, options, deleteP
|
|
|
120
161
|
}
|
|
121
162
|
}
|
|
122
163
|
/**
|
|
123
|
-
* Deletes one managed
|
|
124
|
-
* look at the path immediately before, require a
|
|
125
|
-
* Partial Host removal only ever deletes the Claude
|
|
164
|
+
* Deletes one managed directory link without ever following or recursing into it:
|
|
165
|
+
* look at the path immediately before, require a link, then unlink it.
|
|
166
|
+
* Partial Host removal only ever deletes the Claude link, so it uses this
|
|
126
167
|
* instead of the recursive `rm` that full removal needs for the canonical directory.
|
|
127
168
|
*/
|
|
128
169
|
async function unlinkManagedSymlink(candidate, fileSystem) {
|
|
@@ -135,7 +176,7 @@ async function unlinkManagedSymlink(candidate, fileSystem) {
|
|
|
135
176
|
/**
|
|
136
177
|
* Inspects what exposing the installed Skill to `hosts` would change without
|
|
137
178
|
* writing. Non-Claude Hosts share the canonical `.agents/skills` location, and
|
|
138
|
-
* Claude
|
|
179
|
+
* Claude uses a managed directory link to it; anything else at a target location is a conflict.
|
|
139
180
|
*/
|
|
140
181
|
export async function inspectProjectSkillHostAddition(selection, hosts, options) {
|
|
141
182
|
const removal = await inspectProjectSkillRemoval(selection, options);
|
|
@@ -161,7 +202,7 @@ export async function inspectProjectSkillHostAddition(selection, hosts, options)
|
|
|
161
202
|
else {
|
|
162
203
|
const target = existing.isSymbolicLink() ? await fileSystem.readlink(claudePath) : undefined;
|
|
163
204
|
if (target === undefined || path.resolve(path.dirname(claudePath), target) !== canonicalPath) {
|
|
164
|
-
throw new ProjectInstallationError("target-conflict", `Cannot expose ${JSON.stringify(removal.skillName)} to Claude because ${JSON.stringify(claudePath)} already exists and is not the managed
|
|
205
|
+
throw new ProjectInstallationError("target-conflict", `Cannot expose ${JSON.stringify(removal.skillName)} to Claude because ${JSON.stringify(claudePath)} already exists and is not the managed directory link to the installed Skill; no path was changed.`);
|
|
165
206
|
}
|
|
166
207
|
}
|
|
167
208
|
}
|
|
@@ -446,7 +487,7 @@ async function inspectSkillLocation(candidate, context) {
|
|
|
446
487
|
? Object.freeze({ path: candidate, status: "identical", ...evidence })
|
|
447
488
|
: collisionLocation(candidate, comparison.reason ?? "content-mismatch", evidence);
|
|
448
489
|
}
|
|
449
|
-
/** A Claude
|
|
490
|
+
/** A Claude directory link is an exposure only when it points to the canonical Skill that was itself found identical. */
|
|
450
491
|
async function isClaudeExposureOfCanonical(candidate, context) {
|
|
451
492
|
if (candidate !== context.claudePath || !context.canonicalIdentical)
|
|
452
493
|
return false;
|
|
@@ -523,7 +564,7 @@ export async function installProjectSkillTransaction(request, options) {
|
|
|
523
564
|
// prevents a later host check from following a pre-existing symlink after
|
|
524
565
|
// the other host's directories have already been created.
|
|
525
566
|
await assertNoSymlinkAncestors(projectRoot, [".agents", "skills", skillName], fileSystem);
|
|
526
|
-
// A Claude Skill path may be the managed
|
|
567
|
+
// A Claude Skill path may be the managed directory link to the canonical tree;
|
|
527
568
|
// adoption validates that final component before following it.
|
|
528
569
|
await assertNoSymlinkAncestors(projectRoot, [".claude", "skills"], fileSystem);
|
|
529
570
|
const adoption = await adoptExistingProjectSkill(projectRoot, skillName, request.selection.path, files, hosts, fileSystem, request.confirmAdditionalHostExposure === true, createdPaths, uncertainCreates);
|
|
@@ -609,15 +650,14 @@ async function writeInstalledFile(canonicalPath, file, createdPaths, uncertainCr
|
|
|
609
650
|
await fileSystem.chmod(destination, file.source.executable ? 0o755 : 0o644);
|
|
610
651
|
}
|
|
611
652
|
async function createClaudeSkillSymlink(claudePath, canonicalPath, createdPaths, uncertainCreates, fileSystem) {
|
|
612
|
-
const linkTarget = path.relative(path.dirname(claudePath), canonicalPath);
|
|
613
653
|
uncertainCreates.add(claudePath);
|
|
614
654
|
try {
|
|
615
|
-
await fileSystem
|
|
655
|
+
await createClaudeDirectoryLink(fileSystem, claudePath, canonicalPath);
|
|
616
656
|
}
|
|
617
657
|
catch (error) {
|
|
618
658
|
const reason = error instanceof Error ? error.message : String(error);
|
|
619
|
-
throw new ProjectInstallationError("claude-symlink-failed", `Cannot create the Claude Skill
|
|
620
|
-
"
|
|
659
|
+
throw new ProjectInstallationError("claude-symlink-failed", `Cannot create the Claude Skill directory link at ${JSON.stringify(claudePath)}: ${reason}. ` +
|
|
660
|
+
(process.platform === "win32" ? "Windows junctions do not require symlink privileges; check the target path and filesystem instead. " : "Check that this process can create directory symlinks. ") +
|
|
621
661
|
"No duplicate managed copy was created; the canonical installation was rolled back.");
|
|
622
662
|
}
|
|
623
663
|
await recordCreatedClaudeSymlink(claudePath, createdPaths, uncertainCreates, fileSystem);
|
|
@@ -625,7 +665,7 @@ async function createClaudeSkillSymlink(claudePath, canonicalPath, createdPaths,
|
|
|
625
665
|
async function recordCreatedClaudeSymlink(claudePath, createdPaths, uncertainCreates, fileSystem) {
|
|
626
666
|
const information = await fileSystem.lstat(claudePath);
|
|
627
667
|
if (!information.isSymbolicLink()) {
|
|
628
|
-
throw new ProjectInstallationError("unsafe-target", `Claude installation path is not a
|
|
668
|
+
throw new ProjectInstallationError("unsafe-target", `Claude installation path is not a directory link: ${claudePath}`);
|
|
629
669
|
}
|
|
630
670
|
createdPaths.push({ path: claudePath, kind: "symlink", identity: pathIdentity(information) });
|
|
631
671
|
uncertainCreates.delete(claudePath);
|
|
@@ -651,7 +691,7 @@ function assertOverwriteCollision(inspection) {
|
|
|
651
691
|
// would make Claude continue reading the old bytes after the canonical move.
|
|
652
692
|
const claudeLocation = inspection.locations.find((location) => location.path === inspection.claudePath);
|
|
653
693
|
if (inspection.targetPath === inspection.canonicalPath && claudeLocation?.status === "identical" && claudeLocation.identity !== undefined) {
|
|
654
|
-
throw new ProjectInstallationError("ambiguous-target", `Cannot overwrite ${JSON.stringify(inspection.targetPath)} because the Claude location is a separate real directory rather than the managed
|
|
694
|
+
throw new ProjectInstallationError("ambiguous-target", `Cannot overwrite ${JSON.stringify(inspection.targetPath)} because the Claude location is a separate real directory rather than the managed directory link; no path was changed`);
|
|
655
695
|
}
|
|
656
696
|
}
|
|
657
697
|
function targetLocation(inspection) {
|
|
@@ -847,9 +887,8 @@ async function overwriteProjectSkillTransaction(request, options, files, hosts,
|
|
|
847
887
|
const claudeLocation = inspection.locations.find((location) => location.path === inspection.claudePath);
|
|
848
888
|
if (targetPath === inspection.canonicalPath && hosts.includes("claude") && claudeLocation?.status === "missing") {
|
|
849
889
|
await ensureDirectoryTree(projectRoot, [".claude", "skills"], exposureCreated, exposureUncertain, fileSystem);
|
|
850
|
-
const linkTarget = path.relative(path.dirname(inspection.claudePath), inspection.canonicalPath);
|
|
851
890
|
exposureUncertain.add(inspection.claudePath);
|
|
852
|
-
await fileSystem
|
|
891
|
+
await createClaudeDirectoryLink(fileSystem, inspection.claudePath, inspection.canonicalPath);
|
|
853
892
|
const linkInformation = await fileSystem.lstat(inspection.claudePath);
|
|
854
893
|
if (!linkInformation.isSymbolicLink()) {
|
|
855
894
|
throw new ProjectInstallationError("unsafe-target", `Claude installation path is not a symbolic link: ${inspection.claudePath}`);
|
|
@@ -918,15 +957,19 @@ async function assertExistingSkillMatches(candidate, sourceSkillPath, expectedTr
|
|
|
918
957
|
throw new ProjectInstallationError("adoption-conflict", `Cannot adopt because an existing Skill already exists at ${JSON.stringify(candidate)} with different content (${comparison.reason}); explicit confirmation would be required to overwrite it, but this safe install flow has no overwrite operation. No path was changed.`);
|
|
919
958
|
}
|
|
920
959
|
}
|
|
960
|
+
async function createClaudeDirectoryLink(fileSystem, claudePath, canonicalPath) {
|
|
961
|
+
const windows = process.platform === "win32";
|
|
962
|
+
await fileSystem.symlink(windows ? path.resolve(canonicalPath) : path.relative(path.dirname(claudePath), canonicalPath), claudePath, windows ? "junction" : "dir");
|
|
963
|
+
}
|
|
921
964
|
async function createAdditionalClaudeExposure(projectRoot, canonicalPath, claudePath, fileSystem, createdPaths, uncertainCreates) {
|
|
922
965
|
await ensureDirectoryTree(projectRoot, [".claude", "skills"], createdPaths, uncertainCreates, fileSystem);
|
|
923
966
|
await assertMissing(claudePath, fileSystem, claudePath);
|
|
924
967
|
uncertainCreates.add(claudePath);
|
|
925
968
|
try {
|
|
926
|
-
await fileSystem
|
|
969
|
+
await createClaudeDirectoryLink(fileSystem, claudePath, canonicalPath);
|
|
927
970
|
}
|
|
928
971
|
catch (error) {
|
|
929
|
-
throw new ProjectInstallationError("claude-symlink-failed", `Cannot create the additional Claude Skill
|
|
972
|
+
throw new ProjectInstallationError("claude-symlink-failed", `Cannot create the additional Claude Skill directory link at ${JSON.stringify(claudePath)}: ${errorMessage(error)}. No existing path was changed.`);
|
|
930
973
|
}
|
|
931
974
|
await recordCreatedClaudeSymlink(claudePath, createdPaths, uncertainCreates, fileSystem);
|
|
932
975
|
}
|
|
@@ -1207,6 +1250,9 @@ async function rollbackCreatedPaths(createdPaths, uncertainCreates, fileSystem)
|
|
|
1207
1250
|
}
|
|
1208
1251
|
await fileSystem.rmdir(created.path);
|
|
1209
1252
|
}
|
|
1253
|
+
else if (created.kind === "symlink") {
|
|
1254
|
+
await unlinkManagedSymlink(created.path, fileSystem);
|
|
1255
|
+
}
|
|
1210
1256
|
else {
|
|
1211
1257
|
await fileSystem.rm(created.path);
|
|
1212
1258
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertNoOverlappingProjectSkillRemovalTargets, inspectProjectSkillRemoval, removeProjectSkill, } from "./project-installation.js";
|
|
1
|
+
import { assertNoOverlappingProjectSkillRemovalTargets, inspectAbsentManagedSkillRecord, inspectProjectSkillRemoval, ProjectInstallationError, removeProjectSkill, } from "./project-installation.js";
|
|
2
2
|
/** Result of resolving a user-supplied `<id|path>` against managed installations. */
|
|
3
3
|
export class SkillSelectionError extends Error {
|
|
4
4
|
}
|
|
@@ -32,11 +32,13 @@ export function userGlobalRemovalOptions(homeDirectory, overrides) {
|
|
|
32
32
|
}
|
|
33
33
|
/** Preview lines for one inspected removal, including adopted/modified warnings. */
|
|
34
34
|
export function describeSkillRemoval(selection, inspection, label) {
|
|
35
|
-
const lines =
|
|
36
|
-
|
|
35
|
+
const lines = inspection.recordOnly
|
|
36
|
+
? [` forget missing installation record for ${label}${JSON.stringify(selection.path)} (record only; no files removed)`]
|
|
37
|
+
: [` remove ${label}${JSON.stringify(selection.path)} at ${inspection.paths.join(", ")}`];
|
|
38
|
+
if (!inspection.recordOnly && inspection.adopted) {
|
|
37
39
|
lines.push(" WARNING: this Skill was adopted; removal deletes content that Agent Depot did not create");
|
|
38
40
|
}
|
|
39
|
-
if (inspection.modified) {
|
|
41
|
+
if (!inspection.recordOnly && inspection.modified) {
|
|
40
42
|
lines.push(" WARNING: this Skill is locally modified or has no trusted baseline; removal deletes those changes");
|
|
41
43
|
}
|
|
42
44
|
return lines;
|
|
@@ -48,7 +50,20 @@ export function describeSkillRemoval(selection, inspection, label) {
|
|
|
48
50
|
export async function inspectSkillRemovals(selections, options, onInspected) {
|
|
49
51
|
const inspections = [];
|
|
50
52
|
for (const selection of selections) {
|
|
51
|
-
const
|
|
53
|
+
const otherInstallationPaths = [
|
|
54
|
+
...(options.otherInstallationPaths ?? []),
|
|
55
|
+
...selections.filter((candidate) => candidate !== selection)
|
|
56
|
+
.flatMap((candidate) => candidate.installation ? [candidate.installation.path] : []),
|
|
57
|
+
];
|
|
58
|
+
let inspection;
|
|
59
|
+
try {
|
|
60
|
+
inspection = await inspectProjectSkillRemoval(selection, { ...options, otherInstallationPaths });
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (!(error instanceof ProjectInstallationError) || error.code !== "missing-installation" || options.installationTrust !== "user-global-state")
|
|
64
|
+
throw error;
|
|
65
|
+
inspection = await inspectAbsentManagedSkillRecord(selection, { ...options, otherInstallationPaths });
|
|
66
|
+
}
|
|
52
67
|
inspections.push(inspection);
|
|
53
68
|
onInspected?.(selection, inspection);
|
|
54
69
|
}
|
|
@@ -70,9 +85,9 @@ export async function recheckSkillRemoval(plan, options) {
|
|
|
70
85
|
return rechecked;
|
|
71
86
|
}
|
|
72
87
|
export function assertRemovalInspectionUnchanged(before, after) {
|
|
73
|
-
const unchanged = before.digest === after.digest && before.adopted === after.adopted && before.modified === after.modified &&
|
|
74
|
-
before.skillName === after.skillName && before.
|
|
75
|
-
before.paths.every((candidate, pathIndex) => candidate === after.paths[pathIndex]);
|
|
88
|
+
const unchanged = before.digest === after.digest && before.recordOnly === after.recordOnly && before.adopted === after.adopted && before.modified === after.modified &&
|
|
89
|
+
before.skillName === after.skillName && before.nonCanonicalPath === after.nonCanonicalPath &&
|
|
90
|
+
before.paths.length === after.paths.length && before.paths.every((candidate, pathIndex) => candidate === after.paths[pathIndex]);
|
|
76
91
|
if (!unchanged) {
|
|
77
92
|
throw new Error("The inspected Skill removal target changed before deletion; no path was removed");
|
|
78
93
|
}
|
|
@@ -86,8 +101,11 @@ export function assertManagedInstallationRecordsUnchanged(expected, actual) {
|
|
|
86
101
|
}
|
|
87
102
|
/** Deletes the files of every rechecked selection; records are left to the caller. */
|
|
88
103
|
export async function removeSkillFiles(plan, rechecked, options) {
|
|
104
|
+
assertRemovalBatchUnchanged(plan, rechecked);
|
|
89
105
|
for (const [index, selection] of plan.selections.entries()) {
|
|
90
106
|
assertRemovalInspectionUnchanged(plan.inspections[index], rechecked.inspections[index]);
|
|
107
|
+
if (rechecked.inspections[index].recordOnly)
|
|
108
|
+
continue;
|
|
91
109
|
await removeProjectSkill(selection, rechecked.inspections[index], options);
|
|
92
110
|
}
|
|
93
111
|
}
|
|
@@ -102,16 +120,35 @@ export async function removeSkillsAndRecords(plan, rechecked, operations, option
|
|
|
102
120
|
if (!list || !reconcile) {
|
|
103
121
|
throw new Error("Configured Source operations cannot reconcile removed user-global Skills");
|
|
104
122
|
}
|
|
123
|
+
assertRemovalBatchUnchanged(plan, rechecked);
|
|
105
124
|
let expected = initialManaged;
|
|
106
125
|
for (const [index, selection] of plan.selections.entries()) {
|
|
107
126
|
assertRemovalInspectionUnchanged(plan.inspections[index], rechecked.inspections[index]);
|
|
108
127
|
assertManagedInstallationRecordsUnchanged(expected, await list.call(operations));
|
|
109
|
-
|
|
128
|
+
if (rechecked.inspections[index].recordOnly) {
|
|
129
|
+
const latest = await inspectAbsentManagedSkillRecord(selection, options);
|
|
130
|
+
assertRemovalInspectionUnchanged(rechecked.inspections[index], latest);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
await removeProjectSkill(selection, rechecked.inspections[index], options);
|
|
134
|
+
}
|
|
110
135
|
await reconcile.call(operations, [selection]);
|
|
111
136
|
expected = await list.call(operations);
|
|
112
137
|
}
|
|
113
138
|
return expected;
|
|
114
139
|
}
|
|
140
|
+
/** Compare the complete confirmed batch before any entry can mutate files or records. */
|
|
141
|
+
function assertRemovalBatchUnchanged(plan, rechecked) {
|
|
142
|
+
if (plan.selections.length !== rechecked.selections.length || plan.inspections.length !== rechecked.inspections.length) {
|
|
143
|
+
throw new Error("The inspected Skill removal batch changed before deletion; no path was removed");
|
|
144
|
+
}
|
|
145
|
+
for (const [index, inspection] of plan.inspections.entries()) {
|
|
146
|
+
const current = rechecked.inspections[index];
|
|
147
|
+
if (!current)
|
|
148
|
+
throw new Error("The inspected Skill removal batch changed before deletion; no path was removed");
|
|
149
|
+
assertRemovalInspectionUnchanged(inspection, current);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
115
152
|
/** Execute step: recheck, then remove files and records. */
|
|
116
153
|
export async function executeSkillRemoval(plan, operations, options, initialManaged) {
|
|
117
154
|
const rechecked = await recheckSkillRemoval(plan, options);
|
|
@@ -92,7 +92,9 @@ export async function runBulkUninstall(plan, operations, env) {
|
|
|
92
92
|
await runUninstall({ ...item.prepared, installations: expected }, operations, env);
|
|
93
93
|
// Only the record this item removed moves the baseline the next item is checked against.
|
|
94
94
|
expected = expected.filter((record) => !item.prepared.plan.selections.some((removed) => sameRecord(removed, record)));
|
|
95
|
-
return
|
|
95
|
+
return item.prepared.plan.inspections.every((inspection) => inspection.recordOnly)
|
|
96
|
+
? `Forgot missing installation record for ${item.label}; no files were removed`
|
|
97
|
+
: `Removed ${item.label}`;
|
|
96
98
|
}, (item, error) => (item.kind === "unmanaged" ? errorText(error) : `Failed ${item.label}: ${errorText(error)}`));
|
|
97
99
|
return resultOf(outcomes, plan.skipped);
|
|
98
100
|
}
|
|
@@ -128,15 +128,24 @@ export async function prepareUninstall(operations, environment, skill, installed
|
|
|
128
128
|
if (matches.length === 0) {
|
|
129
129
|
return { kind: recordsFor(installed.project, skill, installed.sources).length > 0 ? "project-only" : "not-installed" };
|
|
130
130
|
}
|
|
131
|
-
const options = userGlobalRemovalOptions(homeOf(environment), environment.installationOptions);
|
|
132
131
|
const record = matches[0];
|
|
132
|
+
const options = userGlobalRemovalOptions(homeOf(environment), {
|
|
133
|
+
...environment.installationOptions,
|
|
134
|
+
otherInstallationPaths: installations.filter((candidate) => candidate !== record).flatMap((candidate) => candidate.installation ? [candidate.installation.path] : []),
|
|
135
|
+
});
|
|
133
136
|
const plan = await planSkillRemoval([record], options);
|
|
134
|
-
const scopeNote =
|
|
137
|
+
const scopeNote = plan.inspections[0]?.recordOnly
|
|
138
|
+
? ` forgets the missing installation record, from hosts: ${record.hosts.join(", ")}; no files are removed`
|
|
139
|
+
: ` removes the whole user-global installation, from hosts: ${record.hosts.join(", ")}`;
|
|
135
140
|
return { kind: "ready", plan: { ...plan, preview: [...plan.preview, scopeNote] }, installations };
|
|
136
141
|
}
|
|
137
142
|
/** Execute step: rechecks the plan, then removes files and records. */
|
|
138
143
|
export async function runUninstall(prepared, operations, environment) {
|
|
139
|
-
const
|
|
144
|
+
const selected = new Set(prepared.plan.selections.map((selection) => JSON.stringify(selection)));
|
|
145
|
+
const options = userGlobalRemovalOptions(homeOf(environment), {
|
|
146
|
+
...environment.installationOptions,
|
|
147
|
+
otherInstallationPaths: prepared.installations.filter((candidate) => !selected.has(JSON.stringify(candidate))).flatMap((candidate) => candidate.installation ? [candidate.installation.path] : []),
|
|
148
|
+
});
|
|
140
149
|
await executeSkillRemoval(prepared.plan, operations, options, prepared.installations);
|
|
141
150
|
}
|
|
142
151
|
/** The user-global record of the Skill, when it is installed there. */
|
|
@@ -178,16 +187,26 @@ export async function runHostAddition(prepared, operations, environment) {
|
|
|
178
187
|
/** Plan step for removing some hosts of a user-global Skill; removing all of them is a full removal. */
|
|
179
188
|
export async function prepareHostRemoval(operations, environment, skill, installed, hosts) {
|
|
180
189
|
const { record, installations } = await currentGlobalRecord(operations, skill, installed);
|
|
181
|
-
const options = userGlobalRemovalOptions(homeOf(environment),
|
|
190
|
+
const options = userGlobalRemovalOptions(homeOf(environment), {
|
|
191
|
+
...environment.installationOptions,
|
|
192
|
+
otherInstallationPaths: installations.filter((candidate) => JSON.stringify(candidate) !== JSON.stringify(record)).flatMap((candidate) => candidate.installation ? [candidate.installation.path] : []),
|
|
193
|
+
});
|
|
182
194
|
const plan = await planHostRemoval(record, hosts, options).catch(asMessage);
|
|
183
195
|
return { plan, installations, preview: plan.kind === "full" ? plan.plan.preview : plan.preview };
|
|
184
196
|
}
|
|
185
197
|
/** Execute step: rechecks, removes only the chosen hosts' locations, then reconciles the record. */
|
|
186
198
|
export async function runHostRemoval(prepared, operations, environment, skill) {
|
|
187
|
-
const
|
|
199
|
+
const selection = prepared.plan.kind === "full" ? prepared.plan.plan.selections[0] : prepared.plan.selection;
|
|
200
|
+
const options = userGlobalRemovalOptions(homeOf(environment), {
|
|
201
|
+
...environment.installationOptions,
|
|
202
|
+
otherInstallationPaths: prepared.installations.filter((candidate) => selection !== undefined && JSON.stringify(candidate) !== JSON.stringify(selection))
|
|
203
|
+
.flatMap((candidate) => candidate.installation ? [candidate.installation.path] : []),
|
|
204
|
+
});
|
|
188
205
|
await executeHostRemoval(prepared.plan, operations, options, prepared.installations);
|
|
189
206
|
const { plan } = prepared;
|
|
190
|
-
return plan.kind === "full"
|
|
191
|
-
? `
|
|
192
|
-
:
|
|
207
|
+
return plan.kind === "full" && plan.plan.inspections.every((inspection) => inspection.recordOnly)
|
|
208
|
+
? `Forgot missing installation record for ${plan.plan.selections[0]?.path ?? skill.path}; no files were removed`
|
|
209
|
+
: plan.kind === "full"
|
|
210
|
+
? `Removed ${skill.name} (${skill.path})`
|
|
211
|
+
: `Removed hosts ${plan.removed.join(", ")} from ${plan.selection.path}`;
|
|
193
212
|
}
|
|
@@ -15,7 +15,7 @@ function PackageDetails({ row }) {
|
|
|
15
15
|
const status = row.status.kind === "unknown" && row.status.reason === "local changes"
|
|
16
16
|
? "update not checked · local changes"
|
|
17
17
|
: row.status.kind === "unknown" ? "cannot assess" : row.status.kind === "current" ? "up to date" : "update available";
|
|
18
|
-
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.legend, children: " " }), _jsxs(Text, { color: theme.value, children: [row.selection.host, " "] }), version === undefined ? _jsx(Text, { color: theme.warning, children: row.installed }) : _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: installedPrefix }), _jsx(Text, { color: theme.version, children: version }), _jsx(Text, { color: theme.value, children: installedSuffix })] }), _jsx(Text, { color: theme.value, children: " available " }), _jsx(Text, { color: row.available === "unknown" ? theme.warning : theme.version, children: row.available }), _jsxs(Text, { color: theme.warning, children: [" ", status] }), row.drift === undefined ? null : _jsxs(Text, { color: theme.warning, children: [" drift: ", row.drift] })] }));
|
|
18
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.legend, children: " " }), _jsxs(Text, { color: theme.value, children: [row.selection.host, " "] }), version === undefined ? _jsx(Text, { color: theme.warning, children: row.installed }) : _jsxs(_Fragment, { children: [_jsx(Text, { color: theme.value, children: installedPrefix }), _jsx(Text, { color: theme.version, children: version }), _jsx(Text, { color: theme.value, children: installedSuffix })] }), _jsx(Text, { color: theme.value, children: " available " }), _jsx(Text, { color: row.available === "unknown" ? theme.warning : theme.version, children: row.available }), _jsxs(Text, { color: theme.warning, children: [" ", status] }), row.updateReason === undefined ? null : _jsxs(Text, { color: theme.warning, children: [" ", row.updateReason] }), row.drift === undefined ? null : _jsxs(Text, { color: theme.warning, children: [" drift: ", row.drift] })] }));
|
|
19
19
|
}
|
|
20
20
|
/** One line of the Installations tree: cursor, indent, mark box, label and the leaf's details. */
|
|
21
21
|
export function TreeRowLine({ row, selected, marked }) {
|
|
@@ -139,7 +139,9 @@ function handleConfirmKey(context, mode, input, key) {
|
|
|
139
139
|
answerYesNo(input, key, () => {
|
|
140
140
|
void execute(context, `Removing ${mode.skill.name}…`, async () => {
|
|
141
141
|
await runUninstall(mode.prepared, operations, env);
|
|
142
|
-
return
|
|
142
|
+
return mode.prepared.plan.inspections.every((inspection) => inspection.recordOnly)
|
|
143
|
+
? `Forgot missing installation record for ${mode.skill.path}; no files were removed`
|
|
144
|
+
: `Removed ${mode.skill.name} (${mode.skill.path})`;
|
|
143
145
|
});
|
|
144
146
|
}, cancelWith("Uninstall cancelled"));
|
|
145
147
|
return;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { classifyUpdate } from "../package-host-plans.js";
|
|
2
2
|
import { describeCoordinates, } from "../package-model.js";
|
|
3
3
|
/** Version evidence as a short label; `unknown` when the host or the bundle states none. */
|
|
4
|
-
export function packageVersionLabel(evidence) {
|
|
4
|
+
export function packageVersionLabel(evidence, commit) {
|
|
5
5
|
if (evidence === undefined)
|
|
6
6
|
return "unknown";
|
|
7
|
-
return evidence.kind === "manifest-version" ? evidence.version : evidence.commit.slice(0, 7);
|
|
7
|
+
return evidence.kind === "manifest-version" ? versionWithCommit(evidence.version, commit) : evidence.commit.slice(0, 7);
|
|
8
|
+
}
|
|
9
|
+
function versionWithCommit(version, commit) {
|
|
10
|
+
return `${version}${commit === undefined ? "" : ` (${commit.slice(0, 7)})`}`;
|
|
8
11
|
}
|
|
9
12
|
/** What the host reports about an install, as one phrase. Never a guess. */
|
|
10
13
|
export function installedEvidenceLabel(evidence) {
|
|
@@ -12,7 +15,7 @@ export function installedEvidenceLabel(evidence) {
|
|
|
12
15
|
case "installed": {
|
|
13
16
|
if (evidence.version !== undefined) {
|
|
14
17
|
const qualifier = evidence.reason === undefined ? "" : " (HEAD; local changes)";
|
|
15
|
-
return `installed ${evidence.version}${qualifier}`;
|
|
18
|
+
return `installed ${versionWithCommit(evidence.version, evidence.commit)}${qualifier}`;
|
|
16
19
|
}
|
|
17
20
|
if (evidence.reason !== undefined) {
|
|
18
21
|
const reason = evidence.reason.includes("local modifications") ? "local changes" : evidence.reason;
|
|
@@ -52,17 +55,18 @@ export function packageRow(record, inspection) {
|
|
|
52
55
|
name: inspection.descriptor?.name ?? describeCoordinates(record.selection),
|
|
53
56
|
installed: installedEvidenceLabel(inspection.installed),
|
|
54
57
|
...(inspection.installed.kind === "installed" && inspection.installed.version !== undefined
|
|
55
|
-
? { installedVersion: inspection.installed.version }
|
|
58
|
+
? { installedVersion: versionWithCommit(inspection.installed.version, inspection.installed.commit) }
|
|
56
59
|
: inspection.installed.kind === "installed" && inspection.installed.commit !== undefined
|
|
57
60
|
? { installedVersion: inspection.installed.commit.slice(0, 7) }
|
|
58
61
|
: {}),
|
|
59
|
-
available: packageVersionLabel(inspection.available),
|
|
60
|
-
status: packageRowStatus(inspection),
|
|
62
|
+
available: packageVersionLabel(inspection.available, inspection.availableCommit),
|
|
63
|
+
status: packageRowStatus(record.selection, inspection),
|
|
64
|
+
...(inspection.updateReason === undefined ? {} : { updateReason: inspection.updateReason }),
|
|
61
65
|
...(inspection.drift === undefined ? {} : { drift: inspection.drift }),
|
|
62
66
|
};
|
|
63
67
|
}
|
|
64
|
-
function packageRowStatus(inspection) {
|
|
65
|
-
const status = classifyUpdate(inspection.installed, inspection.available, inspection.availableCommit, inspection.installedCommitIsAncestor);
|
|
68
|
+
function packageRowStatus(selection, inspection) {
|
|
69
|
+
const status = classifyUpdate(inspection.installed, inspection.available, inspection.availableCommit, inspection.installedCommitIsAncestor, selection.host);
|
|
66
70
|
if (status === "unknown") {
|
|
67
71
|
return {
|
|
68
72
|
kind: "unknown",
|
|
@@ -24,7 +24,7 @@ function updateResultField(line, defaultOk) {
|
|
|
24
24
|
/** What the last check found out: its age, how the sources were refreshed, and what failed. */
|
|
25
25
|
export function CheckStatus({ data, now }) {
|
|
26
26
|
const errors = data.results.filter((result) => result.error !== undefined);
|
|
27
|
-
return (_jsxs(BorderPanel, { title: "Update check", children: [_jsxs(Text, { color: theme.field, children: ["checked ", _jsx(Text, { color: theme.value, children: formatAgo(now - data.checkedAt) }), _jsx(Text, { color: theme.legend, children: " \u00B7 " }), _jsx(Text, { color: theme.value, children: describeRefresh(data) })] }), data.refresh.failed.map((failure) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.warning, children: "[WARN] " }), _jsx(FieldLine, { label: "Refresh failed for", value: `${failure.source}: ${failure.reason}` })] }, failure.source))), data.appError ? _jsxs(Text, { children: [_jsx(Text, { color: theme.error, children: "[FAIL] " }), _jsx(FieldLine, { label: "Error (Apps):", value: data.appError })] }) : null, data.packageError ? _jsxs(Text, { children: [_jsx(Text, { color: theme.error, children: "[FAIL] " }), _jsx(FieldLine, { label: "Error (Packages):", value: data.packageError })] }) : null, errors.map((result) => _jsxs(Text, { children: [_jsx(Text, { color: theme.error, children: "[FAIL] " }), _jsx(FieldLine, { label: `Error (${result.scope}):`, value: result.error ?? "" })] }, result.scope))] }));
|
|
27
|
+
return (_jsxs(BorderPanel, { title: "Update check", children: [_jsxs(Text, { color: theme.field, children: ["checked ", _jsx(Text, { color: theme.value, children: formatAgo(now - data.checkedAt) }), _jsx(Text, { color: theme.legend, children: " \u00B7 " }), _jsx(Text, { color: theme.value, children: describeRefresh(data) })] }), data.refresh.failed.map((failure) => (_jsxs(Text, { children: [_jsx(Text, { color: theme.warning, children: "[WARN] " }), _jsx(FieldLine, { label: "Refresh failed for", value: `${failure.source}: ${failure.reason}` })] }, failure.source))), data.appError ? _jsxs(Text, { children: [_jsx(Text, { color: theme.error, children: "[FAIL] " }), _jsx(FieldLine, { label: "Error (Apps):", value: data.appError })] }) : null, data.rows.filter(row => row.kind === "package" && row.check.status === "current" && row.check.reason !== undefined).map(row => (_jsxs(Text, { color: theme.warning, children: [row.path, ": ", row.reason] }, row.key))), data.packageError ? _jsxs(Text, { children: [_jsx(Text, { color: theme.error, children: "[FAIL] " }), _jsx(FieldLine, { label: "Error (Packages):", value: data.packageError })] }) : null, errors.map((result) => _jsxs(Text, { children: [_jsx(Text, { color: theme.error, children: "[FAIL] " }), _jsx(FieldLine, { label: `Error (${result.scope}):`, value: result.error ?? "" })] }, result.scope))] }));
|
|
28
28
|
}
|
|
29
29
|
/** One line of the list: an update, the fold of unassessable items, or one of those when unfolded. */
|
|
30
30
|
export function EntryLine({ entry, selected, checked, view, showUnknown }) {
|
package/dist/src/tui/updates.js
CHANGED
|
@@ -141,8 +141,10 @@ export async function loadUpdateRows(operations, environment, options) {
|
|
|
141
141
|
check,
|
|
142
142
|
path: `Package: ${describeCoordinates(check.installed.selection)}`,
|
|
143
143
|
policy: formatVersionPolicy(check.installed.selection.version),
|
|
144
|
-
installed:
|
|
145
|
-
|
|
144
|
+
installed: check.observed === undefined
|
|
145
|
+
? `${recordedVersionLabel(check.installed)} (recorded)`
|
|
146
|
+
: installedEvidenceLabel(check.observed).replace(/^installed (?:at )?/, ""),
|
|
147
|
+
available: check.available === undefined ? "?" : packageVersionLabel(check.available, check.availableCommit),
|
|
146
148
|
status: packageStatusLabel(check.status),
|
|
147
149
|
reason: check.reason ?? "the host reports no usable version evidence",
|
|
148
150
|
});
|