@iseoane/agent-depot 0.6.7 → 0.6.9

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 CHANGED
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.9] - 2026-11-04
11
+
12
+ ### Fixed
13
+
14
+ - Catalog and Installations now read cached Git Source manifests in a bounded
15
+ batch instead of starting a Git process per file. Readers share small immutable
16
+ snapshots for the same repository, directory scope and commit, while resolving
17
+ the current ref again on every read. Host state and operation safety checks
18
+ remain fresh; opening a view does not refresh its Sources.
19
+
20
+ ## [0.6.8] - 2026-11-04
21
+
22
+ ### Fixed
23
+
24
+ - Versioned Claude Packages now follow the Host's literal plugin cache version,
25
+ with the plugin manifest taking precedence over marketplace metadata. A newer
26
+ Source commit at the same version is explained instead of offered as an update
27
+ Claude cannot apply. Successful literal-version changes are recognized even
28
+ when the commit is unchanged or the version strings normalize identically.
29
+ - Package comparisons and results in the CLI and TUI now show the observed Host
30
+ version and commit together. Remembered evidence is labelled as recorded;
31
+ Pi updates between different commits at the same manifest version remain
32
+ available when verified Source ancestry proves progress.
33
+
10
34
  ## [0.6.7] - 2026-11-04
11
35
 
12
36
  ### Fixed
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { isLockContention } from "./atomic-file.js";
6
6
  import { assertNoSymlinkPath } from "./path-safety.js";
7
7
  import { runProcess } from "./process-runner.js";
8
+ import { decodeSnapshotBatch, reuseSourceSnapshot, SOURCE_SNAPSHOT_LIMITS } from "./source-snapshot.js";
8
9
  export const SKILL_TREE_LIMITS = Object.freeze({
9
10
  maxFiles: 256,
10
11
  maxFileBytes: 2 * 1024 * 1024,
@@ -62,11 +63,14 @@ class NodeGitSnapshotCommandRunner {
62
63
  async runBinary(command, args, options = {}) {
63
64
  const result = await runProcess(command, args, {
64
65
  captureStdout: true,
66
+ ...(options.stdin === undefined ? {} : { stdin: options.stdin, timeoutMs: 30_000, maxStderrBytes: 64 * 1024 }),
65
67
  ...(options.maxOutputBytes === undefined ? {} : { maxOutputBytes: options.maxOutputBytes }),
66
68
  });
67
69
  if (result.outputTooLarge) {
68
70
  throw new Error("git command output exceeds the safe size limit");
69
71
  }
72
+ if (result.timedOut)
73
+ throw new Error("git command timed out");
70
74
  if (result.code !== 0) {
71
75
  throw new Error(`git command failed: ${gitFailureDetail(result)}`);
72
76
  }
@@ -168,12 +172,13 @@ function parseSkillTreeRecord(record, relativeSkillPath) {
168
172
  }
169
173
  return { relativePath, objectId, executable: mode === "100755" };
170
174
  }
175
+ const defaultSnapshotRunner = new NodeGitSnapshotCommandRunner();
171
176
  export class GitSourceSnapshotAccess {
172
177
  cachePath;
173
178
  runner;
174
179
  constructor(options = {}) {
175
180
  this.cachePath = options.cachePath ?? defaultGitSourceCachePath();
176
- this.runner = options.runner ?? new NodeGitSnapshotCommandRunner();
181
+ this.runner = options.runner ?? defaultSnapshotRunner;
177
182
  }
178
183
  async requireMirror(source) {
179
184
  const destination = cachePathForSource(this.cachePath, source);
@@ -278,37 +283,46 @@ export class GitSourceSnapshotAccess {
278
283
  }
279
284
  async readSnapshot(source) {
280
285
  const destination = await this.requireMirror(source);
286
+ // Resolve on every read, even on a cache hit: refs can change outside Agent Depot.
281
287
  const commit = await this.readResolvedCommit(source);
282
288
  const directory = gitSourceLocation(source.url).directory;
283
- const tree = await this.runner.run("git", [
284
- "--git-dir", destination, "ls-tree", "-r", "-z", commit,
285
- ...(directory === undefined ? [] : ["--", directory]),
286
- ]);
287
- const files = [];
288
- for (const record of tree.split("\0")) {
289
- if (!record) {
290
- continue;
291
- }
292
- const separator = record.indexOf("\t");
293
- if (separator < 0) {
294
- continue;
289
+ const key = JSON.stringify([path.resolve(destination), commit, directory]);
290
+ return reuseSourceSnapshot(this.runner, key, async () => {
291
+ const tree = await this.runner.run("git", [
292
+ "--no-replace-objects", "--git-dir", destination, "ls-tree", "-r", "-z", commit,
293
+ ...(directory === undefined ? [] : ["--", directory]),
294
+ ], { maxOutputBytes: SKILL_TREE_LIMITS.maxListingBytes });
295
+ if (tree !== "" && !tree.endsWith("\0"))
296
+ throw new Error("Truncated Git snapshot listing");
297
+ const entries = [];
298
+ for (const record of tree.split("\0")) {
299
+ if (!record)
300
+ continue;
301
+ const separator = record.indexOf("\t");
302
+ if (separator < 0)
303
+ throw new Error("Malformed Git snapshot listing");
304
+ const [mode, type, objectId] = record.slice(0, separator).split(" ");
305
+ const relativePath = record.slice(separator + 1);
306
+ if (mode === "120000" || type !== "blob" || !relativePath
307
+ || (directory !== undefined && !relativePath.startsWith(`${directory}/`))
308
+ || !isSourceSnapshotFile(relativePath, directory)
309
+ || relativePath.split("/").slice(0, -1).some(segment => EXCLUDED_LIFECYCLE_SEGMENTS.has(segment)))
310
+ continue;
311
+ if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(objectId)
312
+ || !["100644", "100755"].includes(mode))
313
+ throw new Error("Unsafe Git snapshot entry");
314
+ entries.push({ path: relativePath, objectId });
315
+ if (entries.length > SOURCE_SNAPSHOT_LIMITS.maxFiles)
316
+ throw new Error("Git snapshot exceeds file count limit");
295
317
  }
296
- const metadata = record.slice(0, separator).split(" ");
297
- const relativePath = record.slice(separator + 1);
298
- if (metadata[0] === "120000"
299
- || metadata[1] !== "blob"
300
- || !relativePath
301
- || (directory !== undefined && !relativePath.startsWith(`${directory}/`))
302
- || !isSourceSnapshotFile(relativePath, directory)
303
- || relativePath.split("/").slice(0, -1).some((segment) => EXCLUDED_LIFECYCLE_SEGMENTS.has(segment))) {
304
- continue;
305
- }
306
- files.push({
307
- path: relativePath,
308
- content: await this.runner.run("git", ["--git-dir", destination, "show", `${commit}:${relativePath}`]),
318
+ if (entries.length === 0)
319
+ return Object.freeze([]);
320
+ const output = await this.runner.runBinary("git", ["--no-replace-objects", "--git-dir", destination, "cat-file", "--batch"], {
321
+ stdin: Buffer.from(entries.map(entry => `${entry.objectId}\n`).join("")),
322
+ maxOutputBytes: SOURCE_SNAPSHOT_LIMITS.maxTotalBytes + entries.length * 130,
309
323
  });
310
- }
311
- return Object.freeze(files);
324
+ return decodeSnapshotBatch(output, entries);
325
+ });
312
326
  }
313
327
  /** File paths at or under `root`, from the same immutable commit as the other reads. */
314
328
  async listFiles(source, root) {
@@ -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, because the entry version overrides the plugin manifest version and
60
- * the plugin manifest owns the component inventory (agents, hooks, and so on).
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 = entryVersion ?? plugin.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 }),
@@ -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
- return `Package ${formatPackageSelection(check.installed.selection)}: ${check.status}; available ${available}${reason}`;
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}`;
@@ -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" ? { reason: inspection.drift ?? describeEvidence(inspection.installed) } : {}),
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`; differing commits compare only
128
- * when cached Source history proves the installed commit is an ancestor.
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 {
@@ -151,6 +151,10 @@ function signalProcesses(pids, signal) {
151
151
  */
152
152
  export function runProcess(command, args, options = {}) {
153
153
  return new Promise((resolve, reject) => {
154
+ if (options.stdin !== undefined && options.stdin.byteLength > 1024 * 1024) {
155
+ reject(new Error("Process stdin exceeds the safe size limit"));
156
+ return;
157
+ }
154
158
  const invocation = spawnInvocation(command, args);
155
159
  const sink = childOutputSinks.getStore();
156
160
  const child = spawn(invocation.command, [...invocation.args], {
@@ -158,7 +162,7 @@ export function runProcess(command, args, options = {}) {
158
162
  shell: false,
159
163
  windowsVerbatimArguments: invocation.windowsVerbatimArguments,
160
164
  // Without a capture, stdout is piped only for a sink, and then discarded after it is shown.
161
- stdio: ["ignore", options.captureStdout || sink !== undefined ? "pipe" : "ignore", "pipe"],
165
+ stdio: [options.stdin === undefined ? "ignore" : "pipe", options.captureStdout || sink !== undefined ? "pipe" : "ignore", "pipe"],
162
166
  });
163
167
  const stdout = [];
164
168
  let stdoutBytes = 0;
@@ -189,6 +193,7 @@ export function runProcess(command, args, options = {}) {
189
193
  };
190
194
  // A surviving descendant keeps the pipes open, so `close` may never come: give up on them.
191
195
  const giveUp = () => {
196
+ child.stdin?.destroy();
192
197
  child.stdout?.destroy();
193
198
  child.stderr?.destroy();
194
199
  settle(child.exitCode, child.signalCode);
@@ -263,5 +268,11 @@ export function runProcess(command, args, options = {}) {
263
268
  });
264
269
  child.on("error", reject);
265
270
  child.on("close", settle);
271
+ child.stdin?.on("error", (error) => {
272
+ // Windows reports EOF when a child exits with input still pending.
273
+ if (error.code !== "EPIPE" && error.code !== "EOF")
274
+ reject(error);
275
+ });
276
+ child.stdin?.end(options.stdin);
266
277
  });
267
278
  }
@@ -0,0 +1,59 @@
1
+ export const SOURCE_SNAPSHOT_LIMITS = Object.freeze({
2
+ maxFiles: 4096, maxFileBytes: 2 * 1024 * 1024, maxTotalBytes: 32 * 1024 * 1024,
3
+ });
4
+ /** Git batch payloads are byte framed: contents may themselves contain newlines or NULs. */
5
+ export function decodeSnapshotBatch(output, entries) {
6
+ const bytes = Buffer.from(output);
7
+ let offset = 0;
8
+ let total = 0;
9
+ const files = entries.map(entry => {
10
+ const end = bytes.indexOf(10, offset);
11
+ if (end < offset || end - offset > 128)
12
+ throw new Error("Malformed Git batch header");
13
+ const header = bytes.subarray(offset, end).toString("utf8").split(" ");
14
+ const size = Number(header[2]);
15
+ if (header.length !== 3 || header[0] !== entry.objectId || header[1] !== "blob"
16
+ || !/^\d+$/u.test(header[2]) || !Number.isSafeInteger(size) || size > SOURCE_SNAPSHOT_LIMITS.maxFileBytes) {
17
+ throw new Error("Invalid Git batch object or size limit");
18
+ }
19
+ total += size;
20
+ if (total > SOURCE_SNAPSHOT_LIMITS.maxTotalBytes)
21
+ throw new Error("Git snapshot exceeds total size limit");
22
+ offset = end + 1;
23
+ if (offset + size >= bytes.length || bytes[offset + size] !== 10)
24
+ throw new Error("Truncated Git batch object");
25
+ const content = bytes.subarray(offset, offset + size).toString("utf8");
26
+ offset += size + 1;
27
+ return Object.freeze({ path: entry.path, content });
28
+ });
29
+ if (offset !== bytes.length)
30
+ throw new Error("Unexpected trailing Git batch output");
31
+ return Object.freeze(files);
32
+ }
33
+ // Default readers share immutable work; injected runners have separate cache domains.
34
+ // Only small results are retained, with at most eight entries per runner (32 MiB of UTF-8 data).
35
+ const caches = new WeakMap();
36
+ export function reuseSourceSnapshot(owner, key, load) {
37
+ let cache = caches.get(owner);
38
+ if (!cache) {
39
+ cache = new Map();
40
+ caches.set(owner, cache);
41
+ }
42
+ const hit = cache.get(key);
43
+ if (hit) {
44
+ cache.delete(key);
45
+ cache.set(key, hit);
46
+ return hit;
47
+ }
48
+ const pending = load();
49
+ const remove = () => { if (cache.get(key) === pending)
50
+ cache.delete(key); };
51
+ cache.set(key, pending);
52
+ if (cache.size > 8)
53
+ cache.delete(cache.keys().next().value);
54
+ void pending.then(files => {
55
+ if (files.reduce((size, file) => size + Buffer.byteLength(file.content, "utf8"), 0) > 4 * 1024 * 1024)
56
+ remove();
57
+ }, remove);
58
+ return pending;
59
+ }
@@ -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 }) {
@@ -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 }) {
@@ -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: recordedVersionLabel(check.installed),
145
- available: check.available === undefined ? "?" : packageVersionLabel(check.available),
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iseoane/agent-depot",
3
- "version": "0.6.7",
3
+ "version": "0.6.9",
4
4
  "description": "A cross-platform catalog for reusable coding-agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "iseoane",