@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 +26 -0
- package/README.md +1 -1
- package/dist/src/cli.js +2 -2
- package/dist/src/package-cli.js +1 -18
- package/dist/src/package-flow.js +8 -8
- package/dist/src/package-model.js +51 -0
- package/dist/src/profile-export.js +3 -3
- package/dist/src/profile-import.js +2 -2
- package/dist/src/tui/app.js +1 -1
- package/dist/src/tui/installations.js +1 -1
- package/dist/src/tui/package-actions.js +2 -2
- package/dist/src/tui/package-rows.js +4 -4
- package/dist/src/tui/updates-keys.js +30 -11
- package/dist/src/tui/updates-model.js +15 -3
- package/dist/src/tui/updates-panel.js +7 -1
- package/dist/src/tui/updates-tree.js +51 -0
- package/dist/src/tui/updates-view.js +8 -4
- package/dist/src/tui/updates.js +3 -3
- package/dist/src/update-groups.js +29 -0
- package/package.json +2 -2
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. `
|
|
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,
|
|
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";
|
package/dist/src/package-cli.js
CHANGED
|
@@ -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("--")) {
|
package/dist/src/package-flow.js
CHANGED
|
@@ -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,
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 ${
|
|
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 {
|
|
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 ${
|
|
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
|
}
|
package/dist/src/tui/app.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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: ${
|
|
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 {
|
|
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 ??
|
|
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: ${
|
|
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(
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
/**
|
|
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 === "
|
|
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 === "
|
|
45
|
-
context.
|
|
46
|
-
|
|
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 === "
|
|
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
|
|
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((
|
|
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 ? "
|
|
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:
|
|
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
|
}
|
package/dist/src/tui/updates.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { checkAppUpdates, createAppOperations } from "../app-flow.js";
|
|
2
|
-
import {
|
|
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: ${
|
|
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.
|
|
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
|
|
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",
|