@iseoane/agent-depot 0.6.10 → 0.6.12

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,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.6.12] - 2026-11-09
11
+
12
+ ### Changed
13
+
14
+ - Development only; the published CLI is unchanged. `pnpm test:single` maps test
15
+ paths on every shell, including Git Bash on Windows (#91); TUI tests no longer time
16
+ out on Windows (#90); local `pnpm test:e2e` on Windows skips its PTY journeys with
17
+ an explicit reason, as local end-to-end tests are Linux-only (#92).
18
+
19
+ ## [0.6.11] - 2026-11-08
20
+
21
+ ### Changed
22
+
23
+ - The TUI Updates view is grouped by Source, with Packages and Skills below each
24
+ Source and Apps last. Groups start collapsed; `Enter` or `→`/`←` expand and
25
+ collapse them, and `space` on a group marks or clears all its rows (`[x]`, `[-]`,
26
+ `[ ]`). `u`, or `Enter` on a row, previews exactly the marked rows. `Enter` also
27
+ folds the "cannot be checked" line.
28
+ - TUI Package labels always name their Source and never show a Source-root Package
29
+ as `.` or `name@.` (for example `…demo.git::claude#demo`); update results name the
30
+ Package's Source. CLI selection tokens are unchanged.
31
+ - Profile export and import previews name a Source-root Package as `Package pi from …`
32
+ instead of `Package pi . from …`, and Package flow failure and approval reasons say
33
+ `pi from <source>` instead of `pi .`. Profile `--package` filters still accept the
34
+ previous spelling.
35
+
10
36
  ## [0.6.10] - 2026-11-06
11
37
 
12
38
  ### Changed
package/README.md CHANGED
@@ -520,7 +520,7 @@ Keys per view (prompts also accept `Esc` to cancel and `y`/`n` to confirm):
520
520
  | Sources | `j`/`k` move through Git Sources and App recipes. `Enter` opens the Catalog for a Git Source or previews/approves an App recipe. Approval does not install the App. `n` adds a Git Source or guides recipe authoring; `r` refreshes Sources or reloads recipe status; `d` removes. `space`/`a` mark Git Sources only. Refresh previews show every selected Source; review every physical row before `y`. `n` cancels. |
521
521
  | Catalog | `Enter`/arrows expand and collapse, `space` marks the highlighted loose Skill, `a` marks or clears the visible loose Skills in its Source, and `i` starts installation for marked Skills or the highlighted Skill, Package, or verified-absent approved App. A Package opens the same paged panel as in Installations; confirm an App install separately with `y`. `/` filters and `s` widens a Source view to all Sources. Marks in other Sources remain unchanged; Skills hidden by the filter are not installed. |
522
522
  | Installations | `Enter`/arrows expand and collapse. On user-global rows, `u` starts an App or managed Skill uninstall, or removes an unmanaged Skill; project Skills cannot be removed. `h` or `i` adds Hosts to a user-global Skill installation, and `A` or `Enter` adopts an unmanaged Skill. `space` marks a leaf and `a` marks all visible leaves; bulk actions skip project installations with a reason. On an App row, `u` previews uninstall and `h` selects setup/teardown; changed recipes must be approved again before commands run. On a Package row, `i` previews the frozen install plan and runs it, approving the receipt first when it is missing; `Enter` approves the install declaration only, `U` updates, `u` uninstalls, and `f` forgets the selection record. Each of these opens its own panel, which pages when it is taller than the terminal: arrows or PageUp/PageDown move by a visible page, `j`/`k` by one row, and `Review N of M` with the row window shows where you are. `y` and `n` answer from any page. Package rows are not markable for bulk actions. |
523
- | Updates | `space` marks an update, `a` marks all available updates, and `t`/`p`/`g` selects all, project, or user-global scope. `Enter` previews the marked rows; `u` previews the selected batch; neither applies updates. `r` checks again, and left/right arrows fold or unfold the "cannot be checked" group. In a preview, arrows or PageUp/PageDown move by a visible page and `j`/`k` move one row. Review every row before `y`; `n` cancels. Unknown rows are informational. Packages and Apps are user-global only; Packages need action approval before update execution. |
523
+ | Updates | Updates are grouped by Source, with Packages and Skills below each Source and Apps last; every group starts collapsed. `→`/`←` and `Enter` expand and collapse a group, and `←` on a row selects its group. `space` marks an update, or every available update in the highlighted group, even when it is collapsed. `a` marks all available updates, and `t`/`p`/`g` selects all, project, or user-global scope. `u` previews the marked rows from anywhere, and `Enter` on an update row does too; neither applies updates. `r` checks again, and `Enter` or `→`/`←` on the "cannot be checked" line fold or unfold it. In a preview, arrows or PageUp/PageDown move by a visible page and `j`/`k` move one row. Review every row before `y`; `n` cancels. Unknown rows are informational. Packages and Apps are user-global only; Packages need action approval before update execution. |
524
524
  | Profile | Press `e` to export or `i` to import. See [Profile workflow](#profile-workflow) for navigation, selection, previews, and cancellation. |
525
525
 
526
526
  ### Profile workflow
package/dist/src/cli.js CHANGED
@@ -6,8 +6,8 @@ import path from "node:path";
6
6
  import { checkAppUpdates, createAppOperations } from "./app-flow.js";
7
7
  import { describeAppResult, describeManualCompletion, runAppCommand } from "./app-cli.js";
8
8
  import { createPackageOperations } from "./package-flow.js";
9
- import { describeLifecyclePlan, describePackageResult, describePackageUpdateCheck, formatPackageSelection, packageSelectionMatches, planWithRefresh, runPackageCommand } from "./package-cli.js";
10
- import { selectionKey } from "./package-model.js";
9
+ import { describeLifecyclePlan, describePackageResult, describePackageUpdateCheck, packageSelectionMatches, planWithRefresh, runPackageCommand } from "./package-cli.js";
10
+ import { formatPackageSelection, selectionKey } from "./package-model.js";
11
11
  import { EXPORT_USAGE, IMPORT_USAGE, runProfileExport, runProfileImport } from "./profile-cli.js";
12
12
  import { pathsOverlap } from "./path-safety.js";
13
13
  import { withChildOutput } from "./process-runner.js";
@@ -1,5 +1,5 @@
1
1
  import { sourceIdForUrl } from "./git-source.js";
2
- import { PACKAGE_HOSTS, bundleRoot, describeCoordinates, selectionKey } from "./package-model.js";
2
+ import { PACKAGE_HOSTS, bundleRoot, describeCoordinates, describeSelectionCoordinate, displayRoot, formatPackageSelection, selectionKey, sourceToken } from "./package-model.js";
3
3
  import { NO_PROGRESS } from "./progress.js";
4
4
  import { projectSourceOf } from "./sources.js";
5
5
  import { CliUsageError } from "./usage-error.js";
@@ -76,9 +76,6 @@ export async function runPackageCommand(values, packages, sources, output, progr
76
76
  export function planWithRefresh(packages, selection, action, progress) {
77
77
  return progress.run(`Refreshing Source for Package ${formatPackageSelection(selection)}`, () => packages.planLifecycle(selection, action), { mayPrompt: true });
78
78
  }
79
- export function formatPackageSelection(selection) {
80
- return `${sourceToken(selection.source)}::${describeSelectionCoordinate(selection)}`;
81
- }
82
79
  /**
83
80
  * The two spellings the CLI prints name the same selection: `discover` prints
84
81
  * the registered Source id, and `list` prints the Source's own token. Both are
@@ -295,12 +292,6 @@ function sourceMatchesToken(source, token) {
295
292
  }
296
293
  return source.kind === "git" && (source.url === token || sourceToken(projectSourceOf(source)) === token);
297
294
  }
298
- function sourceToken(source) {
299
- if (source.kind === "builtin") {
300
- return source.id;
301
- }
302
- return "url" in source ? source.url : source.path;
303
- }
304
295
  /** The registered Source id: derived for a Git Source, and its own token otherwise. */
305
296
  function sourceIdToken(source) {
306
297
  return "url" in source ? sourceIdForUrl(source.url) : sourceToken(source);
@@ -308,11 +299,6 @@ function sourceIdToken(source) {
308
299
  function formatDescriptorSelection(sourceId, descriptor) {
309
300
  return `${sourceId}::${describeSelectionCoordinate(descriptor.coordinates)}`;
310
301
  }
311
- function describeSelectionCoordinate(coordinates) {
312
- return coordinates.host === "pi"
313
- ? `pi:${displayRoot(coordinates.root)}`
314
- : `claude:${displayRoot(coordinates.marketplaceRoot)}#${coordinates.pluginName}`;
315
- }
316
302
  function formatPackageRecord(record) {
317
303
  const verified = record.verified === undefined ? "unknown" : describeVersion(record.verified);
318
304
  return `Package ${formatPackageSelection(record.selection)}\tinstallId=${record.installId}\tverified=${verified}\tverifiedAt=${record.verifiedAt}`;
@@ -410,9 +396,6 @@ function packageResultSucceeded(result) {
410
396
  return result.status === "installed" || result.status === "updated" || result.status === "removed" ||
411
397
  result.status === "selected" || result.status === "forgotten";
412
398
  }
413
- function displayRoot(root) {
414
- return root === "" ? "." : root;
415
- }
416
399
  function requireOptionValue(args, index, option) {
417
400
  const value = args[index];
418
401
  if (!value || value.startsWith("--")) {
@@ -7,7 +7,7 @@ import { sourceBundleScope } from "./package-content.js";
7
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
- import { bundleRoot, describeCoordinates, selectionKey, } from "./package-model.js";
10
+ import { bundleRoot, describePackageInSource, selectionKey, } from "./package-model.js";
11
11
  import { defaultPackageStateDirectory, PackageStateStore, } from "./package-state.js";
12
12
  import { runProcess } from "./process-runner.js";
13
13
  import { SourceStateStore } from "./source-state.js";
@@ -52,7 +52,7 @@ export function createPackageOperations(environment = {}) {
52
52
  return source;
53
53
  }
54
54
  if (sourceOperations.refreshProjectSource === undefined) {
55
- throw new PackagePlanError(`Planning a Package lifecycle for ${describeCoordinates(selection)} requires a Source refresh, but this environment cannot refresh project Sources`);
55
+ throw new PackagePlanError(`Planning a Package lifecycle for ${describePackageInSource(selection)} requires a Source refresh, but this environment cannot refresh project Sources`);
56
56
  }
57
57
  return await sourceOperations.refreshProjectSource(selection.source);
58
58
  }
@@ -69,7 +69,7 @@ export function createPackageOperations(environment = {}) {
69
69
  const descriptor = discoverBundlesInSnapshot(files, sourceBundleScope(source))
70
70
  .find((candidate) => sameCoordinates(candidate.coordinates, coordinates));
71
71
  if (descriptor === undefined) {
72
- throw new PackagePlanError(`Source ${JSON.stringify(source.id)} no longer provides a ${selection.host} Package at ${JSON.stringify(bundleRoot(coordinates))}`);
72
+ throw new PackagePlanError(`Source ${JSON.stringify(source.id)} no longer provides a ${selection.host} Package ${bundleRoot(coordinates) === "" ? "at its root" : `at ${JSON.stringify(bundleRoot(coordinates))}`}`);
73
73
  }
74
74
  const resolvedCommit = await readResolvedCommit(source);
75
75
  const available = availableEvidence(descriptor, resolvedCommit);
@@ -222,7 +222,7 @@ export function createPackageOperations(environment = {}) {
222
222
  switch (context.action) {
223
223
  case "install": {
224
224
  if (observed.kind !== "installed") {
225
- return { status: "failed", reason: `The host does not report ${describeCoordinates(context.selection)} installed (${describeEvidence(observed)})` };
225
+ return { status: "failed", reason: `The host does not report ${describePackageInSource(context.selection)} installed (${describeEvidence(observed)})` };
226
226
  }
227
227
  const contradiction = pinContradiction(context.selection, observed);
228
228
  if (contradiction !== undefined) {
@@ -233,7 +233,7 @@ export function createPackageOperations(environment = {}) {
233
233
  }
234
234
  case "update": {
235
235
  if (observed.kind !== "installed") {
236
- return { status: "failed", reason: `The update left no installed ${describeCoordinates(context.selection)} (${describeEvidence(observed)})` };
236
+ return { status: "failed", reason: `The update left no installed ${describePackageInSource(context.selection)} (${describeEvidence(observed)})` };
237
237
  }
238
238
  const contradiction = pinContradiction(context.selection, observed);
239
239
  if (contradiction !== undefined) {
@@ -262,7 +262,7 @@ export function createPackageOperations(environment = {}) {
262
262
  }
263
263
  case "uninstall": {
264
264
  if (observed.kind !== "absent") {
265
- return { status: "failed", reason: `The host still reports ${describeCoordinates(context.selection)} (${describeEvidence(observed)})` };
265
+ return { status: "failed", reason: `The host still reports ${describePackageInSource(context.selection)} (${describeEvidence(observed)})` };
266
266
  }
267
267
  await state.drop(context.selection);
268
268
  return Object.freeze({ status: "removed" });
@@ -343,7 +343,7 @@ export function createPackageOperations(environment = {}) {
343
343
  return Object.freeze({ status: "stale plan", reason: difference });
344
344
  }
345
345
  if (await state.approvalStatus(fresh.digest) !== "approved") {
346
- throw new PackagePlanError(`The ${plan.action} plan for ${describeCoordinates(plan.selection)} is not approved; review it and approve the current declaration first`);
346
+ throw new PackagePlanError(`The ${plan.action} plan for ${describePackageInSource(plan.selection)} is not approved; review it and approve the current declaration first`);
347
347
  }
348
348
  const previous = findInstalledBundle(await readHostView(), fresh.plan.descriptor, plan.selection);
349
349
  const context = contextFromDerived(fresh, plan.action, previous);
@@ -586,7 +586,7 @@ function pinContradiction(selection, observed) {
586
586
  if (verdict.kind !== "contradicted") {
587
587
  return undefined;
588
588
  }
589
- return `The host reports ${describeCoordinates(selection)} ${describeEvidence(observed)}, which contradicts the fixed pin ${JSON.stringify(verdict.pin)}`;
589
+ return `The host reports ${describePackageInSource(selection)} ${describeEvidence(observed)}, which contradicts the fixed pin ${JSON.stringify(verdict.pin)}`;
590
590
  }
591
591
  /**
592
592
  * The record's `verified` field is a claim about the host, so it derives from the
@@ -20,6 +20,57 @@ export function describeCoordinates(coordinates) {
20
20
  ? `pi ${at(coordinates.root)}`
21
21
  : `claude ${coordinates.pluginName}@${at(coordinates.marketplaceRoot)}`;
22
22
  }
23
+ /**
24
+ * Like `describeCoordinates` for text that already names the Source: a bundle at the Source root
25
+ * omits its root instead of showing `.`. Display only; `--package` filters keep `describeCoordinates`.
26
+ */
27
+ export function describeBundleInSource(coordinates) {
28
+ return coordinates.host === "pi"
29
+ ? `pi${coordinates.root === "" ? "" : ` ${coordinates.root}`}`
30
+ : `claude ${coordinates.pluginName}${coordinates.marketplaceRoot === "" ? "" : `@${coordinates.marketplaceRoot}`}`;
31
+ }
32
+ /** Host, root and Source in prose. Unlike `formatPackageSelection` it cannot be mistaken for a CLI token. */
33
+ export function describePackageInSource(selection) {
34
+ return `${describeBundleInSource(selection)} from ${sourceToken(selection.source)}`;
35
+ }
36
+ /** The token the CLI prints for a Source: the built-in id, the URL or the local path. */
37
+ export function sourceToken(source) {
38
+ if (source.kind === "builtin") {
39
+ return source.id;
40
+ }
41
+ return "url" in source ? source.url : source.path;
42
+ }
43
+ /** A root-level bundle shows its root as `.`. */
44
+ export function displayRoot(root) {
45
+ return root === "" ? "." : root;
46
+ }
47
+ /** `pi:<root>` or `claude:<root>#<plugin>`. */
48
+ export function describeSelectionCoordinate(coordinates) {
49
+ return coordinates.host === "pi"
50
+ ? `pi:${displayRoot(coordinates.root)}`
51
+ : `claude:${displayRoot(coordinates.marketplaceRoot)}#${coordinates.pluginName}`;
52
+ }
53
+ /** Coordinates qualified by the Source they live in, so equal roots of different Sources stay apart. */
54
+ export function formatPackageCoordinates(source, coordinates) {
55
+ return `${sourceToken(source)}::${describeSelectionCoordinate(coordinates)}`;
56
+ }
57
+ /**
58
+ * The TUI spelling of coordinates: like the CLI's, but a bundle at the Source root omits its root
59
+ * segment instead of showing `.`. Display only; the CLI keeps `formatPackageCoordinates`.
60
+ */
61
+ export function displayPackageCoordinates(source, coordinates) {
62
+ const root = bundleRoot(coordinates);
63
+ const at = root === "" ? "" : `:${root}`;
64
+ return `${sourceToken(source)}::${coordinates.host === "pi" ? `pi${at}` : `claude${at}#${coordinates.pluginName}`}`;
65
+ }
66
+ /** The TUI spelling of a selection. */
67
+ export function displayPackageSelection(selection) {
68
+ return displayPackageCoordinates(selection.source, selection);
69
+ }
70
+ /** The CLI spelling of a selection. */
71
+ export function formatPackageSelection(selection) {
72
+ return formatPackageCoordinates(selection.source, selection);
73
+ }
23
74
  /**
24
75
  * The stable selection identity. Derived from the Source and the coordinates
25
76
  * only, so a bundle rename (its manifest name or version) does not move the
@@ -2,7 +2,7 @@ import { writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { loadAppOwnedSkillFilter } from "./app-owned-skills.js";
4
4
  import { sourceIdForUrl } from "./git-source.js";
5
- import { bundleRoot, describeCoordinates } from "./package-model.js";
5
+ import { bundleRoot, describeBundleInSource, describeCoordinates } from "./package-model.js";
6
6
  import { AGENT_DEPOT_PACKAGE_VERSION } from "./project-manifest.js";
7
7
  import { parseProfile, PROFILE_FORMAT, PROFILE_FORMAT_V2, serializeProfile } from "./profile.js";
8
8
  import { formatVersionPolicy } from "./skill-install.js";
@@ -33,7 +33,7 @@ export function packageFilterNames(selection) {
33
33
  }
34
34
  /** One Package line, shared by the export preview and the import plan label. */
35
35
  export function describeProfilePackage(selection) {
36
- return `Package ${describeCoordinates(selection)} from ${profileSourceIdentity(selection.source)} (${formatVersionPolicy(selection.version)}; saved selection)`;
36
+ return `Package ${describeBundleInSource(selection)} from ${profileSourceIdentity(selection.source)} (${formatVersionPolicy(selection.version)}; saved selection)`;
37
37
  }
38
38
  export function describeProfileSkill(skill) {
39
39
  return `Skill ${skill.path} from ${profileSourceIdentity(skill.source)} (${skill.version.policy})`;
@@ -42,7 +42,7 @@ export function describeProfileReplica(replica) {
42
42
  switch (replica.kind) {
43
43
  case "skill": return `Installed Skill ${replica.path} from ${profileSourceIdentity(replica.source)}`;
44
44
  case "app": return `Installed App ${replica.name} (${replica.platform ?? "all platforms"})`;
45
- case "package": return `Installed Package ${describeCoordinates(replica.selection)} from ${profileSourceIdentity(replica.selection.source)}`;
45
+ case "package": return `Installed Package ${describeBundleInSource(replica.selection)} from ${profileSourceIdentity(replica.selection.source)}`;
46
46
  }
47
47
  }
48
48
  export function profileSourceIdentity(source) {
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
  import { compareAppVersions } from "./app-version.js";
6
6
  import { canonicalizeGitSourceUrl, sourceIdForUrl } from "./git-source.js";
7
- import { describeCoordinates, selectionKey } from "./package-model.js";
7
+ import { describeBundleInSource, selectionKey } from "./package-model.js";
8
8
  import { describeProfilePackage, describeProfileReplica, packageFilterNames, profileSourceIdentity, selectProfileItems } from "./profile-export.js";
9
9
  import { parseProfile, PROFILE_FORMAT } from "./profile.js";
10
10
  import { AGENT_DEPOT_PACKAGE_VERSION } from "./project-manifest.js";
@@ -128,7 +128,7 @@ function classifySkill(value, skills, { showPath = false, showSource = false } =
128
128
  function classifyPackage(value, recorded) {
129
129
  const existing = recorded.find(record => selectionKey(record.selection) === selectionKey(value));
130
130
  const result = comparison(existing === undefined ? undefined : { version: existing.selection.version }, { version: value.version });
131
- const thing = `Package ${describeCoordinates(value)} from ${profileSourceIdentity(value.source)} (${formatVersionPolicy(value.version)})`;
131
+ const thing = `Package ${describeBundleInSource(value)} from ${profileSourceIdentity(value.source)} (${formatVersionPolicy(value.version)})`;
132
132
  return { block: "packages", value, label: describeProfilePackage(value), group: groupOf(value.source), ...result,
133
133
  summary: summarize(result.status, thing, `Select ${thing}`, "reason" in result ? result.reason : undefined) };
134
134
  }
@@ -21,7 +21,7 @@ function hintsOf(view, catalogFocusedOnSource) {
21
21
  case "profile":
22
22
  return "e export · i import · q quit";
23
23
  case "updates":
24
- return "j/k move · space mark · a all · Enter preview · u update · r check again · q quit";
24
+ return "j/k move · space mark · a all · Enter expand · u update · r check again · q quit";
25
25
  }
26
26
  }
27
27
  export function Header({ view, columns }) {
@@ -46,7 +46,7 @@ export function homeOf(environment) {
46
46
  function projectRootOf(environment) {
47
47
  return path.resolve(environment.projectRoot ?? process.cwd());
48
48
  }
49
- function sourceLabel(source) {
49
+ export function sourceLabel(source) {
50
50
  if (source.kind === "builtin")
51
51
  return source.id;
52
52
  return "url" in source ? `${source.url}${source.ref === undefined ? "" : `@${source.ref}`}` : source.path;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { describeCoordinates } from "../package-model.js";
2
+ import { displayPackageSelection } from "../package-model.js";
3
3
  import { errorText } from "./batch.js";
4
4
  import { movePreviewCursor } from "./mode-keys.js";
5
5
  import { installedEvidenceLabel, packagePlanLines } from "./package-rows.js";
@@ -41,7 +41,7 @@ export async function startPackageAction(context, target, input) {
41
41
  target,
42
42
  lines: [
43
43
  `Package: ${target.name} (${target.selection.host})`,
44
- `Coordinates: ${describeCoordinates(target.selection)}`,
44
+ `Coordinates: ${displayPackageSelection(target.selection)}`,
45
45
  `Installed: ${installedEvidenceLabel(inspection.installed)}`,
46
46
  "Forgetting drops the Agent Depot selection record only; the host install is left as it is and no host command runs.",
47
47
  ],
@@ -1,5 +1,5 @@
1
1
  import { classifyUpdate } from "../package-host-plans.js";
2
- import { describeCoordinates, } from "../package-model.js";
2
+ import { displayPackageCoordinates, displayPackageSelection, } from "../package-model.js";
3
3
  /** Version evidence as a short label; `unknown` when the host or the bundle states none. */
4
4
  export function packageVersionLabel(evidence, commit) {
5
5
  if (evidence === undefined)
@@ -52,7 +52,7 @@ export function recordedVersionLabel(record) {
52
52
  export function packageRow(record, inspection) {
53
53
  return {
54
54
  selection: record.selection,
55
- name: inspection.descriptor?.name ?? describeCoordinates(record.selection),
55
+ name: inspection.descriptor?.name ?? displayPackageSelection(record.selection),
56
56
  installed: installedEvidenceLabel(inspection.installed),
57
57
  ...(inspection.installed.kind === "installed" && inspection.installed.version !== undefined
58
58
  ? { installedVersion: versionWithCommit(inspection.installed.version, inspection.installed.commit) }
@@ -90,7 +90,7 @@ export function packagePlanLines(plan, approval) {
90
90
  return [
91
91
  `Environment: ${plan.environment}`,
92
92
  `Package: ${descriptor.name} (${selection.host}) ${plan.action}`,
93
- `Coordinates: ${describeCoordinates(selection)}`,
93
+ `Coordinates: ${displayPackageSelection(selection)}`,
94
94
  `Origin: manifest ${plan.origin.manifestDigest.slice(0, 12)}${plan.origin.resolvedCommit === undefined ? "" : ` commit ${plan.origin.resolvedCommit.slice(0, 7)}`}`,
95
95
  `Available: ${packageVersionLabel(descriptor.version)}`,
96
96
  `Components (${descriptor.components.length}):`,
@@ -101,7 +101,7 @@ export function packagePlanLines(plan, approval) {
101
101
  `Step ${position + 1} (${command.purpose}): ${JSON.stringify([command.executable, ...command.args])}`,
102
102
  `Executable: ${command.resolvedPath ?? command.executable}`,
103
103
  ])),
104
- `Blast radius: ${plan.affects.map(describeCoordinates).join(", ")}`,
104
+ `Blast radius: ${plan.affects.map((coordinates) => displayPackageCoordinates(selection.source, coordinates)).join(", ")}`,
105
105
  `Working directory: ${plan.cwd}`,
106
106
  `Approval: ${approval === "approved" ? "approved" : "needs approval"}`,
107
107
  ...plan.warnings.map((warning) => `Warning: ${warning}`),
@@ -1,3 +1,5 @@
1
+ import { collapseOrParent, expandNode, toggleNode } from "./tree.js";
2
+ import { toggleGroup } from "./updates-tree.js";
1
3
  import { answerYesNo, movePreviewCursor } from "./mode-keys.js";
2
4
  import { busy } from "./working-model.js";
3
5
  import { pageStep } from "./window.js";
@@ -18,14 +20,14 @@ function moveCursorKey(context, snapshot, input, key) {
18
20
  return false;
19
21
  return true;
20
22
  }
21
- /** Space toggles the highlighted update, `a` selects them all. */
23
+ /** Space toggles the highlighted update or every update of the highlighted group, `a` selects them all. */
22
24
  function selectKey(context, snapshot, input) {
23
25
  if (input === " ") {
24
26
  const entry = snapshot.entries[snapshot.at];
25
- if (entry?.kind !== "update")
26
- return true;
27
- const checked = context.checked();
28
- context.setChecked(checked.includes(entry.row.key) ? checked.filter((id) => id !== entry.row.key) : [...checked, entry.row.key]);
27
+ if (entry?.kind === "group")
28
+ context.setChecked(toggleGroup(context.checked(), entry.leafKeys));
29
+ else if (entry?.kind === "update")
30
+ context.setChecked(toggleGroup(context.checked(), [entry.row.key]));
29
31
  }
30
32
  else if (input === "a")
31
33
  context.setChecked(snapshot.updatable.map((row) => row.key));
@@ -33,21 +35,38 @@ function selectKey(context, snapshot, input) {
33
35
  return false;
34
36
  return true;
35
37
  }
36
- /** Right/left unfold and fold the "cannot be checked" rows; Enter toggles the fold or previews the selection. */
38
+ /** Moves to the group above the highlighted line, or stays when there is none. */
39
+ function selectParent(context, snapshot, parentId) {
40
+ const parent = snapshot.entries.findIndex((entry) => entry.kind !== "summary" && entry.kind !== "unknown" && entry.line.node.id === parentId);
41
+ if (parent >= 0)
42
+ context.setCursor(parent);
43
+ }
44
+ /** Right/left expand and collapse groups and the "cannot be checked" rows; Enter does the same on a line that is not an update. */
37
45
  function foldKey(context, snapshot, key) {
38
46
  const entry = snapshot.entries[snapshot.at];
39
47
  if (key.rightArrow) {
40
- if (entry?.kind === "summary")
48
+ if (entry?.kind === "group")
49
+ context.setExpanded(expandNode(context.expanded(), entry.line));
50
+ else if (entry?.kind === "summary")
41
51
  context.setShowUnknown(true);
42
52
  }
43
53
  else if (key.leftArrow) {
44
- if (entry?.kind === "unknown")
45
- context.setCursor(snapshot.updatable.length);
46
- if (entry?.kind === "summary" || entry?.kind === "unknown")
54
+ if (entry?.kind === "group" || entry?.kind === "update") {
55
+ const next = collapseOrParent(context.expanded(), [entry.line], 0);
56
+ context.setExpanded(next.expanded);
57
+ if (next.selectedId !== entry.line.node.id)
58
+ selectParent(context, snapshot, entry.line.parentId);
59
+ }
60
+ else if (entry?.kind === "summary" || entry?.kind === "unknown") {
61
+ if (entry.kind === "unknown")
62
+ context.setCursor(snapshot.entries.findIndex((candidate) => candidate.kind === "summary"));
47
63
  context.setShowUnknown(false);
64
+ }
48
65
  }
49
66
  else if (key.return) {
50
- if (entry?.kind === "summary")
67
+ if (entry?.kind === "group")
68
+ context.setExpanded(toggleNode(context.expanded(), entry.line));
69
+ else if (entry?.kind === "summary")
51
70
  context.setShowUnknown(!context.unknownShown());
52
71
  else if (entry?.kind !== "unknown" && snapshot.data)
53
72
  context.startPreview();
@@ -1,6 +1,16 @@
1
+ import { buildUpdateTree } from "./updates-tree.js";
2
+ import { flattenVisible } from "./tree.js";
1
3
  export const REFRESHING = "Refreshing sources…";
2
4
  export const CHECKING = "Checking for updates…";
3
5
  export const PHASE_LABEL = { refreshing: REFRESHING, checking: CHECKING };
6
+ /** A stable React key: tree lines use their node id, the others their own identity. */
7
+ export function entryId(entry) {
8
+ switch (entry.kind) {
9
+ case "summary": return "summary";
10
+ case "unknown": return `unknown:${entry.row.key}`;
11
+ default: return entry.line.node.id;
12
+ }
13
+ }
4
14
  /** Names the reasons worth knowing before expanding the collapsed "cannot be checked" row. */
5
15
  export function describeUnknownReasons(rows) {
6
16
  const newer = rows.filter((row) => /^installed by a newer agent depot/i.test(row.reason)).length;
@@ -14,14 +24,16 @@ export function describeUnknownReasons(rows) {
14
24
  return parts.length === 0 ? "" : ` (${parts.join(", ")})`;
15
25
  }
16
26
  const matchesScope = (row, filter) => filter === "all" || row.scope === filter;
17
- /** The rows the list shows for a scope filter and fold state, and the cursor positions they make up. */
18
- export function viewOf(data, scope, showUnknown) {
27
+ /** The rows the list shows for a scope filter and fold states, and the cursor positions they make up. */
28
+ export function viewOf(data, scope, showUnknown, expanded) {
19
29
  const shown = (data?.rows ?? []).filter((row) => matchesScope(row, scope));
20
30
  const updatable = shown.filter((row) => row.status === "update available");
21
31
  const unknown = shown.filter((row) => row.status === "cannot assess");
22
32
  const upToDate = shown.filter((row) => row.status === "up to date").length;
23
33
  const entries = [
24
- ...updatable.map((row) => ({ kind: "update", row })),
34
+ ...flattenVisible(buildUpdateTree(updatable), expanded).map((line) => line.node.data.kind === "group"
35
+ ? { kind: "group", line, label: line.node.data.label, leafKeys: line.node.data.leafKeys }
36
+ : { kind: "update", line, row: line.node.data.row }),
25
37
  ...(unknown.length > 0 ? [{ kind: "summary" }] : []),
26
38
  ...(unknown.length > 0 && showUnknown ? unknown.map((row) => ({ kind: "unknown", row })) : []),
27
39
  ];
@@ -5,6 +5,7 @@ import { BorderPanel, FieldLine, PreviewConfirm } from "./panel-parts.js";
5
5
  import { previewViewport } from "./preview-window.js";
6
6
  import { describeRefresh, describeUnknownReasons } from "./updates-model.js";
7
7
  import { formatAgo, previewLineText } from "./updates.js";
8
+ import { groupCheckState, packageLeafLabel } from "./updates-tree.js";
8
9
  import { updateModePreviewChrome, updateModePreviewLines, updateModeQuestion } from "./updates-preview.js";
9
10
  function updateResultField(line, defaultOk) {
10
11
  if (line.startsWith("Update summary: ")) {
@@ -26,19 +27,24 @@ export function CheckStatus({ data, now }) {
26
27
  const errors = data.results.filter((result) => result.error !== undefined);
27
28
  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
29
  }
30
+ const BOX = { all: "[x]", some: "[-]", none: "[ ]" };
29
31
  /** One line of the list: an update, the fold of unassessable items, or one of those when unfolded. */
30
32
  export function EntryLine({ entry, selected, checked, view, showUnknown }) {
31
33
  const cursor = selected ? "> " : " ";
32
34
  if (entry.kind === "summary") {
33
35
  return (_jsxs(Text, { ...rowStyle(selected), color: theme.inactive, children: [cursor, view.unknown.length, " cannot be checked ", showUnknown ? "▾" : "▸", describeUnknownReasons(view.unknown)] }));
34
36
  }
37
+ if (entry.kind === "group") {
38
+ const state = groupCheckState(entry.leafKeys, checked);
39
+ return (_jsxs(Text, { ...rowStyle(selected), children: [cursor, " ".repeat(entry.line.depth), BOX[state], " ", _jsxs(Text, { color: theme.field, children: [entry.line.expanded ? "▾" : "▸", " ", entry.label] })] }));
40
+ }
35
41
  const { row } = entry;
36
42
  if (entry.kind === "unknown") {
37
43
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { ...rowStyle(selected), children: [cursor, " ", _jsx(Text, { color: theme.field, children: row.path }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.scope })] }), _jsxs(Text, { color: theme.muted, children: [" ", row.kind === "package" && /local modifications|local changes/i.test(row.reason)
38
44
  ? `Update comparison unavailable: ${row.reason}`
39
45
  : row.reason] })] }));
40
46
  }
41
- return (_jsxs(Text, { ...rowStyle(selected), children: [cursor, checked ? "[x]" : "[ ]", " ", _jsx(Text, { color: theme.field, children: row.path }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.scope }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.policy }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.value, children: row.installed }), _jsx(Text, { color: theme.legend, children: " -> " }), _jsx(Text, { color: theme.value, children: row.available })] }));
47
+ return (_jsxs(Text, { ...rowStyle(selected), children: [cursor, " ".repeat(entry.line.depth), BOX[checked.includes(row.key) ? "all" : "none"], " ", _jsx(Text, { color: theme.field, children: packageLeafLabel(row) }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.scope }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.field, children: row.policy }), _jsx(Text, { color: theme.legend, children: " " }), _jsx(Text, { color: theme.value, children: row.installed }), _jsx(Text, { color: theme.legend, children: " -> " }), _jsx(Text, { color: theme.value, children: row.available })] }));
42
48
  }
43
49
  /** Under the list: the window indicator, what is up to date or selected, and the last result. */
44
50
  export function UpdatesFooter({ indicator, upToDate, updatableCount, checkedCount, message }) {
@@ -0,0 +1,51 @@
1
+ import { bundleRoot } from "../package-model.js";
2
+ import { groupUpdates } from "../update-groups.js";
3
+ import { sourceLabel } from "./installations.js";
4
+ function grouped(row) {
5
+ if (row.kind === "app")
6
+ return { kind: "app", row };
7
+ const source = row.kind === "skill" ? row.item.selection.source : row.check.installed.selection.source;
8
+ const label = sourceLabel(source);
9
+ return { kind: row.kind, source: { id: label, label }, row };
10
+ }
11
+ // Node ids stay stable across reloads, and keys and Source labels hold paths and URLs, so they are encoded.
12
+ const leaf = (row) => ({ id: `update:${encodeURIComponent(row.key)}`, data: { kind: "update", row } });
13
+ function group(id, name, leaves, children = leaves) {
14
+ const leafKeys = leaves.flatMap((node) => (node.data.kind === "update" ? [node.data.row.key] : node.data.leafKeys));
15
+ return { id, data: { kind: "group", label: `${name} (${leafKeys.length})`, leafKeys }, children };
16
+ }
17
+ /** Source groups with Packages and Skills below them, then Apps; built from the updatable rows only. */
18
+ export function buildUpdateTree(rows) {
19
+ const { sources, apps } = groupUpdates(rows.map(grouped));
20
+ const nodes = sources.map(({ source, subgroups }) => {
21
+ const id = `source:${encodeURIComponent(source.id)}`;
22
+ const children = subgroups.map(({ kind, items }) => group(`${id}:${kind}`, kind === "packages" ? "Packages" : "Skills", items.map((item) => leaf(item.row))));
23
+ return group(id, source.label, children);
24
+ });
25
+ if (apps.length > 0)
26
+ nodes.push(group("apps", "Apps", apps.map((item) => leaf(item.row))));
27
+ return nodes;
28
+ }
29
+ /** Whether all, some or none of a group's rows are selected. */
30
+ export function groupCheckState(leafKeys, checked) {
31
+ const count = leafKeys.filter((key) => checked.includes(key)).length;
32
+ if (count === 0)
33
+ return "none";
34
+ return count === leafKeys.length ? "all" : "some";
35
+ }
36
+ /** Space on a group: selects all its rows unless they all are, then unselects them; other rows are untouched. */
37
+ export function toggleGroup(checked, leafKeys) {
38
+ if (groupCheckState(leafKeys, checked) === "all")
39
+ return checked.filter((key) => !leafKeys.includes(key));
40
+ return [...checked, ...leafKeys.filter((key) => !checked.includes(key))];
41
+ }
42
+ /** A Package row inside its Source group: the bundle name, plus its path only when it is not the Source root. */
43
+ export function packageLeafLabel(row) {
44
+ if (row.kind !== "package")
45
+ return row.path;
46
+ const selection = row.check.installed.selection;
47
+ const root = bundleRoot(selection);
48
+ // A Pi bundle has no plugin name, so its directory names it; at the Source root only the host is left.
49
+ const name = selection.host === "claude" ? selection.pluginName : root.split("/").pop() || "pi package";
50
+ return `Package: ${name}${root === "" ? "" : ` ${root}`}`;
51
+ }
@@ -5,10 +5,11 @@ import { theme } from "./theme.js";
5
5
  import { previewViewport } from "./preview-window.js";
6
6
  import { computeWindow, useListHeight } from "./window.js";
7
7
  import { BROWSE } from "./updates-mode.js";
8
+ import { defaultExpanded } from "./tree.js";
8
9
  import { useMirrored } from "./view-state.js";
9
10
  import { useUpdatesFlow } from "./updates-flow.js";
10
11
  import { handleUpdatesKey } from "./updates-keys.js";
11
- import { viewOf } from "./updates-model.js";
12
+ import { entryId, viewOf } from "./updates-model.js";
12
13
  import { CheckStatus, EntryLine, ModePanel, UpdatesFooter } from "./updates-panel.js";
13
14
  import { previewLineText } from "./updates.js";
14
15
  import { updateModePreviewChrome, updateModePreviewLines } from "./updates-preview.js";
@@ -26,6 +27,7 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
26
27
  const [scope, scopeRef, setScope] = useMirrored("all");
27
28
  const [cursor, cursorRef, setCursor] = useMirrored(0);
28
29
  const [showUnknown, showUnknownRef, setShowUnknown] = useMirrored(false);
30
+ const [expanded, expandedRef, setExpanded] = useMirrored(defaultExpanded());
29
31
  const [checked, checkedRef, setChecked] = useMirrored([]);
30
32
  const [mode, setModeState] = useState(BROWSE);
31
33
  // Mirrors the mode synchronously so later keystrokes and the shell never see stale state.
@@ -52,7 +54,7 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
52
54
  const dataRef = useRef(undefined);
53
55
  /** The list, the cursor and the fold as of the latest keystroke. */
54
56
  const latest = () => {
55
- const view = viewOf(dataRef.current, scopeRef.current, showUnknownRef.current);
57
+ const view = viewOf(dataRef.current, scopeRef.current, showUnknownRef.current, expandedRef.current);
56
58
  return { ...view, data: dataRef.current, at: Math.min(cursorRef.current, Math.max(view.entries.length - 1, 0)) };
57
59
  };
58
60
  const changeScope = (next) => {
@@ -77,7 +79,7 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
77
79
  }, [load]);
78
80
  const data = state.status === "ready" ? state.data : undefined;
79
81
  dataRef.current = data;
80
- const { shown, updatable, unknown, upToDate, entries } = viewOf(data, scope, showUnknown);
82
+ const { shown, updatable, unknown, upToDate, entries } = viewOf(data, scope, showUnknown, expanded);
81
83
  const index = Math.min(cursor, Math.max(entries.length - 1, 0));
82
84
  const previewGeometry = (current) => {
83
85
  const cursor = "cursor" in current ? current.cursor : 0;
@@ -95,6 +97,8 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
95
97
  checked: () => checkedRef.current,
96
98
  setChecked,
97
99
  setCursor,
100
+ expanded: () => expandedRef.current,
101
+ setExpanded,
98
102
  unknownShown: () => showUnknownRef.current,
99
103
  setShowUnknown,
100
104
  changeScope,
@@ -115,5 +119,5 @@ export function UpdatesView({ operations, environment, onCapturingChange, listHe
115
119
  const showingBrowse = mode.kind === "browse";
116
120
  // One indicator for the whole operation: a check's own phase label, else the busy mode's.
117
121
  const working = state.status === "loading" ? state : mode.kind === "busy" ? mode : undefined;
118
- return (_jsxs(Box, { flexDirection: "column", children: [showingBrowse ? _jsxs(Text, { color: theme.title, bold: true, children: ["Updates (scope: ", scope, ") t all \u00B7 p project \u00B7 g user-global \u00B7 u update"] }) : null, working ? _jsx(BorderPanel, { title: state.status === "loading" ? "Update check" : "Updates", children: _jsx(Working, { label: working.label, since: working.startedAt, now: clock }) }) : null, showingBrowse && data ? _jsx(CheckStatus, { data: data, now: clock() }) : null, showingBrowse && data && !hasErrors && shown.length === 0 ? _jsx(Text, { children: "No installations" }) : null, showingBrowse ? entries.slice(window.start, window.end).map((entry, offset) => (_jsx(EntryLine, { entry: entry, selected: window.start + offset === index, checked: entry.kind === "update" && checked.includes(entry.row.key), view: view, showUnknown: showUnknown }, entry.kind === "summary" ? "summary" : entry.row.key))) : null, showingBrowse ? _jsx(UpdatesFooter, { indicator: window.indicator, upToDate: upToDate, updatableCount: updatable.length, checkedCount: checked.length, message: message }) : null, _jsx(ModePanel, { mode: mode })] }));
122
+ return (_jsxs(Box, { flexDirection: "column", children: [showingBrowse ? _jsxs(Text, { color: theme.title, bold: true, children: ["Updates (scope: ", scope, ") t all \u00B7 p project \u00B7 g user-global \u00B7 u update"] }) : null, working ? _jsx(BorderPanel, { title: state.status === "loading" ? "Update check" : "Updates", children: _jsx(Working, { label: working.label, since: working.startedAt, now: clock }) }) : null, showingBrowse && data ? _jsx(CheckStatus, { data: data, now: clock() }) : null, showingBrowse && data && !hasErrors && shown.length === 0 ? _jsx(Text, { children: "No installations" }) : null, showingBrowse ? entries.slice(window.start, window.end).map((entry, offset) => (_jsx(EntryLine, { entry: entry, selected: window.start + offset === index, checked: checked, view: view, showUnknown: showUnknown }, entryId(entry)))) : null, showingBrowse ? _jsx(UpdatesFooter, { indicator: window.indicator, upToDate: upToDate, updatableCount: updatable.length, checkedCount: checked.length, message: message }) : null, _jsx(ModePanel, { mode: mode })] }));
119
123
  }
@@ -1,5 +1,5 @@
1
1
  import { checkAppUpdates, createAppOperations } from "../app-flow.js";
2
- import { describeCoordinates, selectionKey } from "../package-model.js";
2
+ import { displayPackageSelection, selectionKey } from "../package-model.js";
3
3
  import { formatVersionPolicy } from "../skill-install.js";
4
4
  import { applyConfirmedUpdates, describeUpdatePreview, loadUpdates, previewUpdateCandidates, readScopeInstallations, } from "../update-flow.js";
5
5
  import { packageOperationsOf } from "./environment.js";
@@ -139,7 +139,7 @@ export async function loadUpdateRows(operations, environment, options) {
139
139
  key: `package:${selectionKey(check.installed.selection)}`,
140
140
  scope: "user-global",
141
141
  check,
142
- path: `Package: ${describeCoordinates(check.installed.selection)}`,
142
+ path: `Package: ${displayPackageSelection(check.installed.selection)}`,
143
143
  policy: formatVersionPolicy(check.installed.selection.version),
144
144
  installed: check.observed === undefined
145
145
  ? `${recordedVersionLabel(check.installed)} (recorded)`
@@ -277,7 +277,7 @@ export async function runUpdates(prepared, confirmedPaths, confirmManual) {
277
277
  }
278
278
  }
279
279
  for (const { plan, approval } of prepared.packagePlans ?? []) {
280
- const name = `Package: ${plan.descriptor.name} (${plan.selection.host})`;
280
+ const name = `Package: ${plan.descriptor.name} (${plan.selection.host}) from ${displayPackageSelection(plan.selection)}`;
281
281
  try {
282
282
  const packages = prepared.packages;
283
283
  if (!packages)
@@ -0,0 +1,29 @@
1
+ // Code-unit order, so Linux and Windows agree whatever the locale.
2
+ const compare = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
3
+ /** Groups updates by Source, then by Packages and Skills, with Apps apart. */
4
+ export function groupUpdates(items) {
5
+ const bySource = new Map();
6
+ const apps = [];
7
+ for (const item of items) {
8
+ if (item.kind === "app") {
9
+ apps.push(item);
10
+ continue;
11
+ }
12
+ let group = bySource.get(item.source.id);
13
+ if (group === undefined) {
14
+ group = { source: item.source, packages: [], skills: [] };
15
+ bySource.set(item.source.id, group);
16
+ }
17
+ (item.kind === "package" ? group.packages : group.skills).push(item);
18
+ }
19
+ const sources = [...bySource.values()]
20
+ .sort((left, right) => compare(left.source.label, right.source.label) || compare(left.source.id, right.source.id))
21
+ .map(({ source, packages, skills }) => ({
22
+ source,
23
+ subgroups: [
24
+ ...(packages.length > 0 ? [{ kind: "packages", items: packages }] : []),
25
+ ...(skills.length > 0 ? [{ kind: "skills", items: skills }] : []),
26
+ ],
27
+ }));
28
+ return { sources, apps };
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iseoane/agent-depot",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
4
4
  "description": "A cross-platform catalog for reusable coding-agent skills.",
5
5
  "license": "MIT",
6
6
  "author": "iseoane",
@@ -74,7 +74,7 @@
74
74
  "typecheck": "tsc --noEmit",
75
75
  "lint": "eslint \"src/**/*.{ts,tsx}\" \"resources/pi/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\"",
76
76
  "test": "pnpm build && node --test --import ./dist/tests/isolated-env.js \"dist/tests/*.test.js\"",
77
- "test:single": "pnpm build && node -e \"const { spawnSync } = require('node:child_process'); const input = process.argv[1] || 'tests/version.test.ts'; const file = input.replace(/^tests[\\\\/]/, 'dist/tests/').replace(/\\\\.tsx?$/i, '.js'); process.exit(spawnSync(process.execPath, ['--import', './dist/tests/isolated-env.js', '--test', file], { stdio: 'inherit' }).status ?? 1);\"",
77
+ "test:single": "pnpm build && node scripts/test-single.mjs",
78
78
  "test:coverage": "node -e \"require('node:fs').rmSync('coverage',{recursive:true,force:true})\" && tsc -p tsconfig.coverage.json && node -e \"const { spawnSync } = require('node:child_process'); process.exit(spawnSync(process.execPath, ['--enable-source-maps','--import','./dist/tests/isolated-env.js','--test','--test-reporter=dot','dist/tests/*.test.js'], { stdio: 'inherit', env: { ...process.env, NODE_V8_COVERAGE: 'coverage/v8' } }).status ?? 1);\"",
79
79
  "audit:dead-code": "npx --yes fallow@3.30.0 dead-code --fail-on-issues",
80
80
  "audit:dupes": "npx --yes fallow@3.30.0 dupes",