@iseoane/agent-depot 0.5.0 → 0.5.1
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 +16 -0
- package/dist/src/git-source.js +21 -0
- package/dist/src/package-flow.js +13 -4
- package/dist/src/package-host-plans.js +12 -9
- package/dist/src/package-host-state.js +129 -5
- package/dist/src/package-owned-skills.js +8 -6
- package/dist/src/process-runner.js +9 -1
- package/dist/src/skill-discovery.js +5 -0
- package/dist/src/tui/app.js +7 -7
- package/dist/src/tui/catalog-browse.js +36 -10
- package/dist/src/tui/catalog-ownership.js +12 -0
- package/dist/src/tui/catalog-panel.js +10 -2
- package/dist/src/tui/catalog-state.js +8 -3
- package/dist/src/tui/catalog-tree.js +6 -2
- package/dist/src/tui/catalog-view.js +27 -4
- package/dist/src/tui/package-rows.js +5 -1
- package/dist/src/tui/render.js +1 -1
- package/dist/src/tui/window.js +3 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.1] - 2026-10-07
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The TUI fills the terminal from startup, responds to resize, and restores the primary screen on exit.
|
|
15
|
+
- Catalog keeps a stable list height while you move between Skills with different Package owners.
|
|
16
|
+
- Catalog `a` marks or clears only visible loose Skills in the repository under the cursor. Marks in other repositories and filtered-out Skills stay unchanged.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Catalog rows use readable contrast, one-line truncation, and compact Package badges. A separate panel explains the highlighted Skill's ownership.
|
|
21
|
+
- Host-confirmed installed Packages disappear from Catalog. Selected-only Packages and Packages with unknown installation state remain visible.
|
|
22
|
+
- Pi Git Package inspections read the installed checkout's origin, commit, and version without changing the checkout. Local modifications keep the Package installed but make version assessment unavailable.
|
|
23
|
+
- Git Package updates require cached Source history to prove that the installed commit precedes the available commit. Different or locally divergent commits alone do not imply an update.
|
|
24
|
+
- Tests isolate `PI_CODING_AGENT_DIR` so an inherited shell override cannot redirect fixtures to user state.
|
|
25
|
+
|
|
10
26
|
## [0.5.0] - 2026-10-06
|
|
11
27
|
|
|
12
28
|
### Added
|
package/dist/src/git-source.js
CHANGED
|
@@ -238,6 +238,27 @@ export class GitSourceSnapshotAccess {
|
|
|
238
238
|
files: Object.freeze(files),
|
|
239
239
|
});
|
|
240
240
|
}
|
|
241
|
+
/** True only when the cached Source history proves `olderCommit` precedes `newerCommit`. */
|
|
242
|
+
async isAncestor(source, olderCommit, newerCommit) {
|
|
243
|
+
if (![olderCommit, newerCommit].every((commit) => /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(commit))) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const destination = await this.requireMirror(source);
|
|
248
|
+
await this.runner.run("git", [
|
|
249
|
+
"--git-dir",
|
|
250
|
+
destination,
|
|
251
|
+
"merge-base",
|
|
252
|
+
"--is-ancestor",
|
|
253
|
+
olderCommit,
|
|
254
|
+
newerCommit,
|
|
255
|
+
]);
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
241
262
|
/** Resolves the selected ref to an immutable commit without refreshing the mirror. */
|
|
242
263
|
async readResolvedCommit(source) {
|
|
243
264
|
const destination = await this.requireMirror(source);
|
package/dist/src/package-flow.js
CHANGED
|
@@ -38,6 +38,7 @@ export function createPackageOperations(environment = {}) {
|
|
|
38
38
|
const readHostView = () => readHostInstallView({
|
|
39
39
|
...(environment.homeDirectory === undefined ? {} : { homeDirectory: environment.homeDirectory }),
|
|
40
40
|
...(environment.readHostStateFile === undefined ? {} : { readHostStateFile: environment.readHostStateFile }),
|
|
41
|
+
...(environment.readPiCheckout === undefined ? {} : { readPiCheckout: environment.readPiCheckout }),
|
|
41
42
|
});
|
|
42
43
|
async function resolveSourceFor(projectSource) {
|
|
43
44
|
return sourceOperations.resolveProjectSource
|
|
@@ -170,10 +171,15 @@ export function createPackageOperations(environment = {}) {
|
|
|
170
171
|
const installed = bundle === undefined
|
|
171
172
|
? Object.freeze({ kind: "unknown", reason: drift ?? "the bundle could not be resolved" })
|
|
172
173
|
: findInstalledBundle(view, bundle.descriptor, selection);
|
|
174
|
+
const installedCommitIsAncestor = bundle?.resolvedCommit === undefined || installed.kind !== "installed" || installed.commit === undefined
|
|
175
|
+
? undefined
|
|
176
|
+
: await contentAccess.isCommitAncestor?.(bundle.source, installed.commit, bundle.resolvedCommit).catch(() => undefined);
|
|
173
177
|
return Object.freeze({
|
|
174
178
|
...(bundle === undefined ? {} : { descriptor: bundle.descriptor }),
|
|
175
179
|
installed,
|
|
176
180
|
...(bundle?.available === undefined ? {} : { available: bundle.available }),
|
|
181
|
+
...(bundle?.resolvedCommit === undefined ? {} : { availableCommit: bundle.resolvedCommit }),
|
|
182
|
+
...(installedCommitIsAncestor === true ? { installedCommitIsAncestor: true } : {}),
|
|
177
183
|
approval,
|
|
178
184
|
...((drift ?? identityDrift) === undefined ? {} : { drift: drift ?? identityDrift }),
|
|
179
185
|
});
|
|
@@ -181,11 +187,12 @@ export function createPackageOperations(environment = {}) {
|
|
|
181
187
|
async function checkOne(record) {
|
|
182
188
|
try {
|
|
183
189
|
const inspection = await inspect(record.selection);
|
|
184
|
-
const status = classifyUpdate(inspection.installed, inspection.available);
|
|
190
|
+
const status = classifyUpdate(inspection.installed, inspection.available, inspection.availableCommit, inspection.installedCommitIsAncestor);
|
|
185
191
|
return Object.freeze({
|
|
186
192
|
installed: record,
|
|
187
193
|
status,
|
|
188
194
|
...(inspection.available === undefined ? {} : { available: inspection.available }),
|
|
195
|
+
...(inspection.availableCommit === undefined ? {} : { availableCommit: inspection.availableCommit }),
|
|
189
196
|
...(status === "unknown" ? { reason: inspection.drift ?? describeEvidence(inspection.installed) } : {}),
|
|
190
197
|
});
|
|
191
198
|
}
|
|
@@ -525,14 +532,14 @@ function installedEvidenceChange(previous, observed) {
|
|
|
525
532
|
if (previous.kind === "absent") {
|
|
526
533
|
return "changed";
|
|
527
534
|
}
|
|
528
|
-
if (previous.version !== undefined && observed.version !== undefined) {
|
|
529
|
-
return sameAppVersion(previous.version, observed.version) ? "unchanged" : "changed";
|
|
530
|
-
}
|
|
531
535
|
const previousCommit = previous.commit === undefined ? undefined : gitCommitId(previous.commit);
|
|
532
536
|
const observedCommit = observed.commit === undefined ? undefined : gitCommitId(observed.commit);
|
|
533
537
|
if (previousCommit !== undefined && observedCommit !== undefined) {
|
|
534
538
|
return previousCommit === observedCommit ? "unchanged" : "changed";
|
|
535
539
|
}
|
|
540
|
+
if (previous.version !== undefined && observed.version !== undefined) {
|
|
541
|
+
return sameAppVersion(previous.version, observed.version) ? "unchanged" : "changed";
|
|
542
|
+
}
|
|
536
543
|
return "unknown";
|
|
537
544
|
}
|
|
538
545
|
/**
|
|
@@ -609,6 +616,8 @@ function hasControlCharacter(value) {
|
|
|
609
616
|
function describeEvidence(evidence) {
|
|
610
617
|
switch (evidence.kind) {
|
|
611
618
|
case "installed": {
|
|
619
|
+
if (evidence.reason !== undefined)
|
|
620
|
+
return evidence.reason;
|
|
612
621
|
if (evidence.version !== undefined) {
|
|
613
622
|
return `installed ${JSON.stringify(evidence.version)}`;
|
|
614
623
|
}
|
|
@@ -124,13 +124,21 @@ export function packageApprovalDigest(declaration) {
|
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
126
|
* Compares installed evidence with available evidence. Manifest versions compare
|
|
127
|
-
* through `sameAppVersion` and `compareAppVersions`; commits compare
|
|
128
|
-
*
|
|
127
|
+
* through `sameAppVersion` and `compareAppVersions`; differing commits compare only
|
|
128
|
+
* when cached Source history proves the installed commit is an ancestor.
|
|
129
129
|
*/
|
|
130
|
-
export function classifyUpdate(installed, available) {
|
|
131
|
-
if (installed.kind !== "installed" || available === undefined) {
|
|
130
|
+
export function classifyUpdate(installed, available, availableCommit, installedCommitIsAncestor) {
|
|
131
|
+
if (installed.kind !== "installed" || installed.reason !== undefined || available === undefined) {
|
|
132
132
|
return "unknown";
|
|
133
133
|
}
|
|
134
|
+
if (installed.commit !== undefined) {
|
|
135
|
+
const sourceCommit = availableCommit ?? (available.kind === "git-commit" ? available.commit : undefined);
|
|
136
|
+
if (sourceCommit === undefined)
|
|
137
|
+
return "unknown";
|
|
138
|
+
if (normalizeCommit(installed.commit) === normalizeCommit(sourceCommit))
|
|
139
|
+
return "current";
|
|
140
|
+
return installedCommitIsAncestor === true ? "update available" : "unknown";
|
|
141
|
+
}
|
|
134
142
|
if (installed.version !== undefined && available.kind === "manifest-version") {
|
|
135
143
|
if (sameAppVersion(installed.version, available.version)) {
|
|
136
144
|
return "current";
|
|
@@ -141,11 +149,6 @@ export function classifyUpdate(installed, available) {
|
|
|
141
149
|
}
|
|
142
150
|
return order < 0 ? "update available" : "current";
|
|
143
151
|
}
|
|
144
|
-
if (installed.commit !== undefined && available.kind === "git-commit") {
|
|
145
|
-
return normalizeCommit(installed.commit) === normalizeCommit(available.commit)
|
|
146
|
-
? "current"
|
|
147
|
-
: "update available";
|
|
148
|
-
}
|
|
149
152
|
return "unknown";
|
|
150
153
|
}
|
|
151
154
|
/** Validates one host argv with the shared portable-command rules. */
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
1
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { canonicalizeGitSourceUrl } from "./git-source.js";
|
|
5
|
+
import { assertNoSymlinkPath } from "./path-safety.js";
|
|
6
|
+
import { runProcess } from "./process-runner.js";
|
|
4
7
|
/**
|
|
5
8
|
* Reads the `packages` setting, or a bare `packages` array, into records.
|
|
6
9
|
* Nothing here throws: an unknown entry shape becomes a drifted record so the
|
|
@@ -44,13 +47,15 @@ export function parseClaudeMarketplaceState(value) {
|
|
|
44
47
|
*/
|
|
45
48
|
export async function readHostInstallView(env = {}) {
|
|
46
49
|
const home = env.homeDirectory ?? homedir();
|
|
50
|
+
const piAgentDirectory = path.resolve(env.piAgentDirectory ?? process.env.PI_CODING_AGENT_DIR ?? path.join(home, ".pi", "agent"));
|
|
47
51
|
const reader = env.readHostStateFile ?? ((filePath) => readFile(filePath, "utf8"));
|
|
48
52
|
const [pi, claudePlugins, claudeMarketplaces] = await Promise.all([
|
|
49
|
-
readSection("pi-settings", path.join(
|
|
53
|
+
readSection("pi-settings", path.join(piAgentDirectory, "settings.json"), reader, parsePiSettingsFile),
|
|
50
54
|
readSection("claude-installed-plugins", path.join(home, ".claude", "plugins", "installed_plugins.json"), reader, parseClaudePluginsFile),
|
|
51
55
|
readSection("claude-known-marketplaces", path.join(home, ".claude", "plugins", "known_marketplaces.json"), reader, parseClaudeMarketplacesFile),
|
|
52
56
|
]);
|
|
53
|
-
|
|
57
|
+
const verifiedPi = await attachPiCheckoutEvidence(pi, piAgentDirectory, env.readPiCheckout ?? readPiCheckoutEvidence);
|
|
58
|
+
return Object.freeze({ pi: verifiedPi, claudePlugins, claudeMarketplaces });
|
|
54
59
|
}
|
|
55
60
|
/**
|
|
56
61
|
* What the host says about one bundle. Pi identity is the repository URL without
|
|
@@ -62,6 +67,9 @@ export function findInstalledBundle(view, descriptor, selection) {
|
|
|
62
67
|
if (descriptor.host !== selection.host) {
|
|
63
68
|
return unknownEvidence("the bundle descriptor and the selection disagree on the host");
|
|
64
69
|
}
|
|
70
|
+
if (selection.host === "pi" && (descriptor.coordinates.host !== "pi" || descriptor.coordinates.root !== selection.root)) {
|
|
71
|
+
return unknownEvidence("the Pi bundle root and the selection disagree");
|
|
72
|
+
}
|
|
65
73
|
return selection.host === "pi"
|
|
66
74
|
? findPiInstalled(view.pi, selection)
|
|
67
75
|
: findClaudeInstalled(view, descriptor, selection);
|
|
@@ -126,7 +134,16 @@ function findPiInstalled(section, selection) {
|
|
|
126
134
|
const wanted = normalizeRepository(identity);
|
|
127
135
|
const match = section.records.find((record) => !record.drifted && normalizeRepository(record.identity) === wanted);
|
|
128
136
|
if (match !== undefined) {
|
|
129
|
-
|
|
137
|
+
if (selection.root !== "") {
|
|
138
|
+
return unknownEvidence("Pi project-scoped and subdirectory package checkouts are not supported for version evidence");
|
|
139
|
+
}
|
|
140
|
+
if (match.checkoutIssue !== undefined) {
|
|
141
|
+
return installedEvidence(undefined, undefined, match.checkoutIssue);
|
|
142
|
+
}
|
|
143
|
+
if (match.checkoutCommit === undefined) {
|
|
144
|
+
return installedEvidence(undefined, undefined, `Pi reports this Package installed, but its checkout could not be verified. Version and update assessment are unavailable; inspect the configured checkout before updating.`);
|
|
145
|
+
}
|
|
146
|
+
return installedEvidence(match.checkoutVersion, match.checkoutCommit);
|
|
130
147
|
}
|
|
131
148
|
if (section.records.some((record) => record.drifted)) {
|
|
132
149
|
return unknownEvidence(`${section.path} holds an entry whose shape could not be read, so the install cannot be verified`);
|
|
@@ -158,12 +175,119 @@ function findClaudeInstalled(view, descriptor, selection) {
|
|
|
158
175
|
}
|
|
159
176
|
return Object.freeze({ kind: "absent" });
|
|
160
177
|
}
|
|
161
|
-
function installedEvidence(version, commit) {
|
|
178
|
+
function installedEvidence(version, commit, reason) {
|
|
162
179
|
return Object.freeze({
|
|
163
180
|
kind: "installed",
|
|
164
181
|
...(version === undefined || version === "" ? {} : { version }),
|
|
165
182
|
...(commit === undefined || commit === "" ? {} : { commit }),
|
|
183
|
+
...(reason === undefined ? {} : { reason }),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
async function attachPiCheckoutEvidence(section, agentDirectory, reader) {
|
|
187
|
+
const records = await Promise.all(section.records.map(async (record) => {
|
|
188
|
+
if (record.drifted || !record.identity.startsWith("git:"))
|
|
189
|
+
return record;
|
|
190
|
+
try {
|
|
191
|
+
const checkoutPath = piGitInstallPath(agentDirectory, record.identity);
|
|
192
|
+
const evidence = await reader(checkoutPath, record.identity);
|
|
193
|
+
return Object.freeze({
|
|
194
|
+
...record,
|
|
195
|
+
...(evidence.commit === undefined ? {} : { checkoutCommit: evidence.commit }),
|
|
196
|
+
...(evidence.version === undefined ? {} : { checkoutVersion: evidence.version }),
|
|
197
|
+
...(evidence.issue === undefined ? {} : { checkoutIssue: evidence.issue }),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
return Object.freeze({
|
|
202
|
+
...record,
|
|
203
|
+
checkoutIssue: error instanceof Error ? error.message : "Pi Git checkout could not be verified",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}));
|
|
207
|
+
return Object.freeze({ ...section, records: Object.freeze(records) });
|
|
208
|
+
}
|
|
209
|
+
function piGitInstallPath(agentDirectory, identity) {
|
|
210
|
+
const source = identity.slice("git:".length);
|
|
211
|
+
let parsed;
|
|
212
|
+
try {
|
|
213
|
+
parsed = new URL(`https://${source}`);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
throw new Error("the configured Pi Git source has no safe checkout path");
|
|
217
|
+
}
|
|
218
|
+
const segments = parsed.pathname.split("/").filter(Boolean).map((segment) => decodeURIComponent(segment));
|
|
219
|
+
const lastSegment = segments.at(-1);
|
|
220
|
+
if (lastSegment?.endsWith(".git"))
|
|
221
|
+
segments[segments.length - 1] = lastSegment.slice(0, -4);
|
|
222
|
+
if (segments.length < 2 || segments.some((segment) => segment === "." || segment === ".." || segment.includes("/") || segment.includes("\\"))) {
|
|
223
|
+
throw new Error("the configured Pi Git source has no safe checkout path");
|
|
224
|
+
}
|
|
225
|
+
return path.resolve(agentDirectory, "git", parsed.hostname, ...segments);
|
|
226
|
+
}
|
|
227
|
+
async function readPiCheckoutEvidence(checkoutPath, sourceIdentity) {
|
|
228
|
+
const unavailable = (reason) => ({
|
|
229
|
+
issue: `Pi reports this Package installed, but ${reason}. Version and update assessment are unavailable; inspect the checkout before updating.`,
|
|
230
|
+
});
|
|
231
|
+
try {
|
|
232
|
+
await assertNoSymlinkPath(checkoutPath, "Pi package checkout path");
|
|
233
|
+
const root = await lstat(checkoutPath).catch((error) => {
|
|
234
|
+
if (isNotFound(error))
|
|
235
|
+
return undefined;
|
|
236
|
+
throw error;
|
|
237
|
+
});
|
|
238
|
+
if (root === undefined)
|
|
239
|
+
return unavailable("its configured Git checkout is missing");
|
|
240
|
+
if (!root.isDirectory())
|
|
241
|
+
return unavailable("its configured Git checkout is not a directory");
|
|
242
|
+
const [topLevel, origin, commit, status, manifest] = await Promise.all([
|
|
243
|
+
runPiGit(checkoutPath, ["rev-parse", "--show-toplevel"]),
|
|
244
|
+
runPiGit(checkoutPath, ["config", "--get", "remote.origin.url"]),
|
|
245
|
+
runPiGit(checkoutPath, ["rev-parse", "--verify", "HEAD^{commit}"]),
|
|
246
|
+
runPiGit(checkoutPath, ["status", "--porcelain", "--untracked-files=all"]),
|
|
247
|
+
runPiGit(checkoutPath, ["show", "HEAD:package.json"]),
|
|
248
|
+
]);
|
|
249
|
+
const expectedRoot = path.resolve(checkoutPath);
|
|
250
|
+
if (path.resolve(topLevel) !== expectedRoot)
|
|
251
|
+
return unavailable("the package path is not the root of its Git checkout");
|
|
252
|
+
if (normalizeRepository(piSourceIdentityFromUrl(origin)) !== normalizeRepository(sourceIdentity)) {
|
|
253
|
+
return unavailable("the Git checkout origin does not match the selected Pi package Source");
|
|
254
|
+
}
|
|
255
|
+
if (!/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(commit))
|
|
256
|
+
return unavailable("the Pi checkout HEAD is not a full Git commit");
|
|
257
|
+
if (status !== "")
|
|
258
|
+
return unavailable("the Pi Git checkout has local modifications");
|
|
259
|
+
const packageJson = JSON.parse(manifest);
|
|
260
|
+
if (!isRecord(packageJson) || typeof packageJson.version !== "string" || packageJson.version.trim() === "") {
|
|
261
|
+
return { commit: commit.toLowerCase() };
|
|
262
|
+
}
|
|
263
|
+
return { commit: commit.toLowerCase(), version: packageJson.version };
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
const message = error instanceof Error ? error.message : "";
|
|
267
|
+
const reason = message.includes("timed out")
|
|
268
|
+
? "Git checkout verification timed out"
|
|
269
|
+
: message.includes("symbolic link")
|
|
270
|
+
? "the configured checkout path contains a symbolic link"
|
|
271
|
+
: "the Pi Git checkout could not be read safely";
|
|
272
|
+
return unavailable(reason);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function runPiGit(checkoutPath, args) {
|
|
276
|
+
const result = await runProcess("git", ["-C", checkoutPath, ...args], {
|
|
277
|
+
captureStdout: true,
|
|
278
|
+
maxOutputBytes: 1024 * 1024,
|
|
279
|
+
maxStderrBytes: 8192,
|
|
280
|
+
timeoutMs: 3000,
|
|
166
281
|
});
|
|
282
|
+
if (result.timedOut)
|
|
283
|
+
throw new Error("Pi checkout verification timed out");
|
|
284
|
+
if (result.outputTooLarge || result.code !== 0 || result.signal !== null)
|
|
285
|
+
throw new Error("Pi checkout verification command failed");
|
|
286
|
+
return result.stdout.toString("utf8").trim();
|
|
287
|
+
}
|
|
288
|
+
function piSourceIdentityFromUrl(origin) {
|
|
289
|
+
const canonical = canonicalizeGitSourceUrl(origin);
|
|
290
|
+
return `git:${canonical.replace(/^[a-z][a-z0-9+.-]*:\/\//iu, "")}`;
|
|
167
291
|
}
|
|
168
292
|
function unknownEvidence(reason) {
|
|
169
293
|
return Object.freeze({ kind: "unknown", reason });
|
|
@@ -21,12 +21,14 @@ export function activeBundles(bundles, selections, hostView) {
|
|
|
21
21
|
continue;
|
|
22
22
|
}
|
|
23
23
|
const selection = packageSelectionFor(descriptor, source);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
const selected = selectedKeys.has(selectionKey(selection));
|
|
25
|
+
const installed = findInstalledBundle(hostView, descriptor, selection).kind === "installed";
|
|
26
|
+
if (selected || installed) {
|
|
27
|
+
active.push({
|
|
28
|
+
descriptor,
|
|
29
|
+
selection,
|
|
30
|
+
reason: selected ? (installed ? "selected-and-installed" : "selected") : "installed",
|
|
31
|
+
});
|
|
30
32
|
}
|
|
31
33
|
}
|
|
32
34
|
return Object.freeze(active);
|
|
@@ -12,6 +12,12 @@ export function runProcess(command, args, options = {}) {
|
|
|
12
12
|
let stderr = "";
|
|
13
13
|
let stderrBytes = 0;
|
|
14
14
|
let outputTooLarge = false;
|
|
15
|
+
let timedOut = false;
|
|
16
|
+
const timeout = options.timeoutMs === undefined ? undefined : setTimeout(() => {
|
|
17
|
+
timedOut = true;
|
|
18
|
+
child.kill();
|
|
19
|
+
}, options.timeoutMs);
|
|
20
|
+
timeout?.unref();
|
|
15
21
|
child.stdout?.on("data", (chunk) => {
|
|
16
22
|
stdoutBytes += chunk.length;
|
|
17
23
|
if (options.maxOutputBytes !== undefined && stdoutBytes > options.maxOutputBytes) {
|
|
@@ -35,7 +41,9 @@ export function runProcess(command, args, options = {}) {
|
|
|
35
41
|
});
|
|
36
42
|
child.on("error", reject);
|
|
37
43
|
child.on("close", (code, signal) => {
|
|
38
|
-
|
|
44
|
+
if (timeout !== undefined)
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
resolve({ code, signal, stdout: Buffer.concat(stdout), stderr, outputTooLarge, ...(timedOut ? { timedOut: true } : {}) });
|
|
39
47
|
});
|
|
40
48
|
});
|
|
41
49
|
}
|
|
@@ -73,6 +73,11 @@ export class NodeSourceContentAccess {
|
|
|
73
73
|
resolvedVersion: Object.freeze({ kind: "git-commit", commit: snapshot.resolvedVersion }),
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
|
+
async isCommitAncestor(source, olderCommit, newerCommit) {
|
|
77
|
+
if (source.kind === "builtin")
|
|
78
|
+
return undefined;
|
|
79
|
+
return this.gitSnapshots.isAncestor(source, olderCommit, newerCommit);
|
|
80
|
+
}
|
|
76
81
|
async readResolvedVersion(source) {
|
|
77
82
|
if (source.kind === "builtin") {
|
|
78
83
|
return AGENT_DEPOT_PACKAGE_VERSION === undefined
|
package/dist/src/tui/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text, useApp,
|
|
2
|
+
import { Box, Text, useApp, useWindowSize } from "ink";
|
|
3
3
|
import { useCallback, useRef, useState } from "react";
|
|
4
4
|
import { AGENT_DEPOT_PACKAGE_VERSION } from "../project-manifest.js";
|
|
5
5
|
import { CatalogView } from "./catalog-view.js";
|
|
@@ -20,7 +20,7 @@ const TABS = [
|
|
|
20
20
|
function hintsOf(view, catalogFocusedOnSource) {
|
|
21
21
|
switch (view) {
|
|
22
22
|
case "catalog":
|
|
23
|
-
return `j/k move · Enter expand · space mark · a
|
|
23
|
+
return `j/k move · Enter expand · space mark · a repo · i install · / filter${catalogFocusedOnSource ? " · s all sources" : ""} · q quit`;
|
|
24
24
|
case "installations":
|
|
25
25
|
return "j/k move · Enter expand · space mark · a all · u uninstall · h add hosts · A adopt · q quit";
|
|
26
26
|
case "profile":
|
|
@@ -31,7 +31,7 @@ function hintsOf(view, catalogFocusedOnSource) {
|
|
|
31
31
|
}
|
|
32
32
|
export function App({ operations, environment, onExit }) {
|
|
33
33
|
const { exit } = useApp();
|
|
34
|
-
const {
|
|
34
|
+
const { columns, rows } = useWindowSize();
|
|
35
35
|
const [capturing, setCapturing] = useState(false);
|
|
36
36
|
// Updated synchronously by the views so keys in the same burst never see a stale capturing state.
|
|
37
37
|
const capturingRef = useRef(false);
|
|
@@ -61,8 +61,8 @@ export function App({ operations, environment, onExit }) {
|
|
|
61
61
|
else if (input === "5")
|
|
62
62
|
showView("profile");
|
|
63
63
|
});
|
|
64
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: theme.accent, children: ["Agent Depot", AGENT_DEPOT_PACKAGE_VERSION === undefined ? "" : ` v${AGENT_DEPOT_PACKAGE_VERSION}`] }), _jsx(Text, { children: TABS.map(([name, label], position) => (_jsxs(Text, { color: view === name ? theme.accent : theme.inactive, bold: view === name, children: [position > 0 ? " " : "", view === name ? `[${label}]` : ` ${label} `] }, name))) }), _jsx(Text, { color: theme.muted, children: separatorLine(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, children: [_jsxs(Text, { bold: true, color: theme.accent, children: ["Agent Depot", AGENT_DEPOT_PACKAGE_VERSION === undefined ? "" : ` v${AGENT_DEPOT_PACKAGE_VERSION}`] }), _jsx(Text, { children: TABS.map(([name, label], position) => (_jsxs(Text, { color: view === name ? theme.accent : theme.inactive, bold: view === name, children: [position > 0 ? " " : "", view === name ? `[${label}]` : ` ${label} `] }, name))) }), _jsx(Text, { color: theme.muted, children: separatorLine(columns) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: view === "sources" ? (_jsx(SourcesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange, onHintsChange: setSourcesHints, onOpenCatalog: (source) => {
|
|
65
|
+
setCatalogSourceId(source.id);
|
|
66
|
+
setView("catalog");
|
|
67
|
+
} })) : view === "installations" ? (_jsx(InstallationsView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "profile" ? (_jsx(ProfileView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : view === "updates" ? (_jsx(UpdatesView, { operations: operations, environment: environment, onCapturingChange: onCapturingChange })) : (_jsx(CatalogView, { operations: operations, sourceId: catalogSourceId, environment: environment, onCapturingChange: onCapturingChange })) }), _jsx(Text, { color: theme.muted, children: capturing ? "Enter submit · Esc cancel · y/n confirm · space toggle" : view === "sources" ? sourcesHints : hintsOf(view, catalogSourceId !== undefined) })] }));
|
|
68
68
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { packageOwnedSkillAction } from "./catalog-ownership.js";
|
|
2
|
+
import { looseLeavesOf, skillKey } from "./catalog-tree.js";
|
|
2
3
|
import { navigateTree } from "./tree-navigation.js";
|
|
3
4
|
import { toggleNode } from "./tree.js";
|
|
4
5
|
import { toggledMark } from "./view-state.js";
|
|
@@ -8,19 +9,40 @@ function markRow(context, row) {
|
|
|
8
9
|
context.setMessage({ kind: "error", text: "Mark skills, not source rows" });
|
|
9
10
|
return;
|
|
10
11
|
}
|
|
12
|
+
if (row.node.data.ownedBy !== undefined) {
|
|
13
|
+
context.setMessage({ kind: "error", text: `Package-owned Skills cannot be marked. ${packageOwnedSkillAction(row.node.data.ownedBy)}` });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
11
16
|
context.setMessage(undefined);
|
|
12
17
|
context.marks.setMarked(toggledMark(context.marks.markedRef.current, row.node.id));
|
|
13
18
|
}
|
|
14
|
-
/** `a`: marks
|
|
15
|
-
function
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
/** `a`: marks the visible loose Skills in the highlighted Source, or clears those marks. */
|
|
20
|
+
function markRepository(context, row, visible) {
|
|
21
|
+
const byId = new Map(visible.map((candidate) => [candidate.node.id, candidate]));
|
|
22
|
+
let ancestor = row;
|
|
23
|
+
while (ancestor !== undefined && ancestor.node.data.kind !== "source") {
|
|
24
|
+
ancestor = ancestor.parentId === undefined ? undefined : byId.get(ancestor.parentId);
|
|
25
|
+
}
|
|
26
|
+
if (ancestor?.node.data.kind !== "source") {
|
|
27
|
+
context.setMessage({ kind: "error", text: "Move to a Source, group, Package, or Skill row to select repository Skills" });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const sourceId = ancestor.node.data.sourceId;
|
|
31
|
+
const keys = looseLeavesOf(context.navigation.latestRoots())
|
|
32
|
+
.filter((skill) => skill.sourceId === sourceId)
|
|
33
|
+
.map(skillKey);
|
|
34
|
+
if (keys.length === 0) {
|
|
35
|
+
context.setMessage({ kind: "error", text: "There are no visible loose skills in this repository" });
|
|
20
36
|
return;
|
|
21
37
|
}
|
|
22
38
|
context.setMessage(undefined);
|
|
23
|
-
|
|
39
|
+
const { markedRef, setMarked } = context.marks;
|
|
40
|
+
const next = new Set(markedRef.current);
|
|
41
|
+
if (keys.every((key) => next.has(key)))
|
|
42
|
+
keys.forEach((key) => next.delete(key));
|
|
43
|
+
else
|
|
44
|
+
keys.forEach((key) => next.add(key));
|
|
45
|
+
setMarked(next);
|
|
24
46
|
}
|
|
25
47
|
/** `i`: installs the highlighted bundle, or the marked Skills that are listed, or the highlighted one. */
|
|
26
48
|
function chooseHosts(context, row) {
|
|
@@ -32,8 +54,12 @@ function chooseHosts(context, row) {
|
|
|
32
54
|
return;
|
|
33
55
|
}
|
|
34
56
|
// Only what is listed installs: marks on Skills hidden by the filter are ignored.
|
|
35
|
-
const marked =
|
|
57
|
+
const marked = looseLeavesOf(context.navigation.latestRoots())
|
|
36
58
|
.filter((skill) => context.marks.markedRef.current.has(skillKey(skill)));
|
|
59
|
+
if (marked.length === 0 && data.kind === "skill" && data.ownedBy !== undefined) {
|
|
60
|
+
context.setMessage({ kind: "error", text: packageOwnedSkillAction(data.ownedBy) });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
37
63
|
const highlighted = data.kind === "skill" ? [data.skill] : [];
|
|
38
64
|
const chosen = marked.length > 0 ? marked : highlighted;
|
|
39
65
|
if (chosen.length === 0) {
|
|
@@ -60,7 +86,7 @@ export function handleCatalogBrowseKey(context, input, key) {
|
|
|
60
86
|
else if (input === " " && row)
|
|
61
87
|
markRow(context, row);
|
|
62
88
|
else if (input === "a")
|
|
63
|
-
|
|
89
|
+
markRepository(context, row, snapshot.visible);
|
|
64
90
|
else if (input === "i" && context.ready && row)
|
|
65
91
|
chooseHosts(context, row);
|
|
66
92
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Compact badge for a Skill reserved by an active Package. */
|
|
2
|
+
export function packageOwnerBadge(owner) {
|
|
3
|
+
return `${owner.host}:${owner.name}`;
|
|
4
|
+
}
|
|
5
|
+
/** Full reason shown outside the row so the row can stay one physical line. */
|
|
6
|
+
export function packageOwnedSkillNotice(owner) {
|
|
7
|
+
return `Package-owned Skill: provided by the ${owner.host} Package ${owner.name}. Install or update the Package from its Package row.`;
|
|
8
|
+
}
|
|
9
|
+
/** Actionable refusal for attempts to install a Package-owned Skill as loose. */
|
|
10
|
+
export function packageOwnedSkillAction(owner) {
|
|
11
|
+
return `Install or update the ${owner.host} Package ${owner.name} from its Package row.`;
|
|
12
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { describeSkills, HOST_CHOICES, SCOPE_CHOICES } from "./catalog-actions.js";
|
|
4
|
+
import { packageOwnedSkillNotice, packageOwnerBadge } from "./catalog-ownership.js";
|
|
4
5
|
import { skillKey } from "./catalog-tree.js";
|
|
5
6
|
import { hasUnmanagedCopy } from "./catalog-installs.js";
|
|
6
|
-
import { describeRow, UNMANAGED_MARKER } from "./catalog-tree.js";
|
|
7
|
+
import { describeRow, truncateDescription, UNMANAGED_MARKER } from "./catalog-tree.js";
|
|
7
8
|
import { isPackageMode, PackagePanel } from "./package-actions.js";
|
|
8
9
|
import { HostChecklist, PreviewLines } from "./panel-parts.js";
|
|
9
10
|
import { rowStyle } from "./theme.js";
|
|
@@ -17,10 +18,17 @@ export function CatalogRowLine({ row, selected, marked, installed }) {
|
|
|
17
18
|
const { data } = row.node;
|
|
18
19
|
const isGroup = data.kind === "source" || data.kind === "group";
|
|
19
20
|
const ownedBy = data.kind === "skill" ? data.ownedBy : undefined;
|
|
20
|
-
return (_jsxs(Text, { ...rowStyle(selected), color: isGroup ? theme.group :
|
|
21
|
+
return (_jsxs(Text, { ...rowStyle(selected), color: isGroup ? theme.group : undefined, wrap: "truncate-end", children: [selected ? "> " : " ", " ".repeat(row.depth), data.kind === "skill" ? marked ? "[x] " : "[ ] " : "", data.kind === "skill" && ownedBy !== undefined
|
|
22
|
+
? _jsxs(_Fragment, { children: [data.skill.name, _jsxs(Text, { color: theme.marker, children: [" ", "[", packageOwnerBadge(ownedBy), "]"] }), " ", truncateDescription(data.skill.description)] })
|
|
23
|
+
: describeRow(row), data.kind === "skill" && hasUnmanagedCopy(data.skill, installed)
|
|
21
24
|
? _jsxs(Text, { color: theme.marker, children: [" ", UNMANAGED_MARKER] })
|
|
22
25
|
: null] }));
|
|
23
26
|
}
|
|
27
|
+
/** Full Package ownership reason for the highlighted Skill, outside the one-line row. */
|
|
28
|
+
export function CatalogSelectionHelp({ row, height }) {
|
|
29
|
+
const owner = row?.node.data.kind === "skill" ? row.node.data.ownedBy : undefined;
|
|
30
|
+
return (_jsx(Box, { flexDirection: "column", minHeight: height, children: owner === undefined ? null : _jsx(Text, { color: theme.marker, children: packageOwnedSkillNotice(owner) }) }));
|
|
31
|
+
}
|
|
24
32
|
/** The prompt of the install flow; browsing shows nothing. */
|
|
25
33
|
export function ActionPanel({ mode }) {
|
|
26
34
|
if (isPackageMode(mode))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
2
|
-
import { packageSelectionFor } from "../package-model.js";
|
|
2
|
+
import { packageSelectionFor, selectionKey } from "../package-model.js";
|
|
3
3
|
import { projectSourceOf } from "../sources.js";
|
|
4
4
|
import { filterReducer, initialFilter } from "./catalog-filter.js";
|
|
5
5
|
import { loadInstalledSkills, NO_INSTALLED } from "./catalog-installs.js";
|
|
@@ -23,7 +23,7 @@ export function useCatalogSkills(operations, sourceId, all, env, reloadKey = 0)
|
|
|
23
23
|
: [sourceId];
|
|
24
24
|
const labels = new Map(sources.map((source) => [source.id, sourceLabel(source)]));
|
|
25
25
|
const skills = [];
|
|
26
|
-
|
|
26
|
+
let bundles = [];
|
|
27
27
|
const discovered = [];
|
|
28
28
|
const warnings = [];
|
|
29
29
|
try {
|
|
@@ -62,7 +62,12 @@ export function useCatalogSkills(operations, sourceId, all, env, reloadKey = 0)
|
|
|
62
62
|
// Ownership is selection- or host-gated, so a loose Skill stays normal until its Package is active.
|
|
63
63
|
let activeBundles = [];
|
|
64
64
|
try {
|
|
65
|
-
|
|
65
|
+
const active = await packages.activeBundles(discovered);
|
|
66
|
+
const installedKeys = new Set(active
|
|
67
|
+
.filter(bundle => bundle.reason === "installed" || bundle.reason === "selected-and-installed")
|
|
68
|
+
.map(bundle => selectionKey(bundle.selection)));
|
|
69
|
+
bundles = bundles.filter(entry => !installedKeys.has(selectionKey(entry.selection)));
|
|
70
|
+
activeBundles = active.map(bundle => bundle.descriptor);
|
|
66
71
|
}
|
|
67
72
|
catch (error) {
|
|
68
73
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -7,7 +7,7 @@ const DESCRIPTION_LIMIT = 60;
|
|
|
7
7
|
export const UNMANAGED_MARKER = "on disk, unmanaged · adopt it from Installations";
|
|
8
8
|
export const skillKey = (skill) => `skill:${skill.sourceId}:${skill.path}`;
|
|
9
9
|
export const bundleKey = (entry) => `bundle:${entry.sourceId}:${entry.descriptor.host}:${bundleRoot(entry.descriptor.coordinates)}`;
|
|
10
|
-
function
|
|
10
|
+
export function truncateDescription(text) {
|
|
11
11
|
return text.length > DESCRIPTION_LIMIT ? `${text.slice(0, DESCRIPTION_LIMIT - 3)}...` : text;
|
|
12
12
|
}
|
|
13
13
|
/** A repository name, with directory and ref for scoped GitHub Sources. */
|
|
@@ -69,9 +69,13 @@ export function describeRow(row) {
|
|
|
69
69
|
if (data.kind === "bundle") {
|
|
70
70
|
return `Package ${data.entry.descriptor.name} (${data.entry.descriptor.host}) ${packageVersionLabel(data.entry.descriptor.version)}`;
|
|
71
71
|
}
|
|
72
|
-
return `${data.skill.name} ${
|
|
72
|
+
return `${data.skill.name} ${truncateDescription(data.skill.description)}`;
|
|
73
73
|
}
|
|
74
74
|
/** The Skills under the tree's Sources, in display order. */
|
|
75
75
|
export function leavesOf(roots) {
|
|
76
76
|
return roots.flatMap((root) => (root.children ?? []).flatMap((child) => child.data.kind === "skill" ? [child.data.skill] : []));
|
|
77
77
|
}
|
|
78
|
+
/** Skills that can still be installed as loose Skills, excluding active Package-owned rows. */
|
|
79
|
+
export function looseLeavesOf(roots) {
|
|
80
|
+
return roots.flatMap((root) => (root.children ?? []).flatMap((child) => child.data.kind === "skill" && child.data.ownedBy === undefined ? [child.data.skill] : []));
|
|
81
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from "ink";
|
|
2
|
+
import { Box, Text, useWindowSize } from "ink";
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { BROWSE } from "./catalog-actions.js";
|
|
5
5
|
import { handleCatalogBrowseKey } from "./catalog-browse.js";
|
|
6
6
|
import { filterSkills } from "./catalog-filter.js";
|
|
7
7
|
import { handleCatalogActionKey } from "./catalog-flow.js";
|
|
8
8
|
import { isInstalled } from "./catalog-installs.js";
|
|
9
|
-
import { ActionPanel, CatalogHeader, CatalogRowLine } from "./catalog-panel.js";
|
|
9
|
+
import { ActionPanel, CatalogHeader, CatalogRowLine, CatalogSelectionHelp } from "./catalog-panel.js";
|
|
10
|
+
import { packageOwnedSkillNotice } from "./catalog-ownership.js";
|
|
10
11
|
import { packageOperationsOf } from "./environment.js";
|
|
11
12
|
import { handlePackageKey, isPackageMode, startPackageAction } from "./package-actions.js";
|
|
12
13
|
import { ListFooter } from "./panel-parts.js";
|
|
@@ -18,8 +19,26 @@ import { useMarks, useMounted } from "./view-state.js";
|
|
|
18
19
|
import { computeWindow, useListHeight } from "./window.js";
|
|
19
20
|
import { useKeys } from "./keys.js";
|
|
20
21
|
const NO_ENVIRONMENT = {};
|
|
22
|
+
function wrappedLineCount(text, columns) {
|
|
23
|
+
let lines = 1;
|
|
24
|
+
let used = 0;
|
|
25
|
+
for (const word of text.split(" ")) {
|
|
26
|
+
const width = [...word].reduce((total, character) => total + ((character.codePointAt(0) ?? 0) > 0xff ? 2 : 1), 0);
|
|
27
|
+
if (used > 0 && used + 1 + width <= columns) {
|
|
28
|
+
used += 1 + width;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (used > 0) {
|
|
32
|
+
lines += 1;
|
|
33
|
+
used = 0;
|
|
34
|
+
}
|
|
35
|
+
lines += Math.floor((width - 1) / columns);
|
|
36
|
+
used = ((width - 1) % columns) + 1;
|
|
37
|
+
}
|
|
38
|
+
return lines;
|
|
39
|
+
}
|
|
21
40
|
export function CatalogView({ operations, sourceId, onCapturingChange, environment, listHeight }) {
|
|
22
|
-
const
|
|
41
|
+
const { columns } = useWindowSize();
|
|
23
42
|
const env = environment ?? NO_ENVIRONMENT;
|
|
24
43
|
const [all, setAll] = useState(sourceId === undefined);
|
|
25
44
|
const mounted = useMounted();
|
|
@@ -48,6 +67,10 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
48
67
|
const { restart } = navigation;
|
|
49
68
|
useEffect(restart, [operations, sourceId, all, restart]);
|
|
50
69
|
const { rows, index } = navigation;
|
|
70
|
+
const helpLines = state.status === "ready"
|
|
71
|
+
? Math.max(1, ...state.activeBundles.map((owner) => wrappedLineCount(packageOwnedSkillNotice(owner), Math.max(columns, 1))))
|
|
72
|
+
: 1;
|
|
73
|
+
const height = useListHeight(listHeight, 6 + helpLines);
|
|
51
74
|
const flow = {
|
|
52
75
|
operations,
|
|
53
76
|
env,
|
|
@@ -108,5 +131,5 @@ export function CatalogView({ operations, sourceId, onCapturingChange, environme
|
|
|
108
131
|
return _jsxs(Text, { color: theme.error, children: ["Error: ", state.message] });
|
|
109
132
|
const window = computeWindow(rows.length, index, height);
|
|
110
133
|
const selectedCount = leavesOf(roots).filter((skill) => marks.marked.has(skillKey(skill))).length;
|
|
111
|
-
return (_jsxs(Box, { flexDirection: "column", children: [state.warnings.map((warning) => _jsx(Text, { color: theme.warning, children: warning }, warning)), _jsx(CatalogHeader, { scope: scope, listed: visible.length, installable: installable.length, total: skills.length, filter: filter.filter }), rows.slice(window.start, window.end).map((row, offset) => (_jsx(CatalogRowLine, { row: row, selected: window.start + offset === index, marked: marks.marked.has(row.node.id), installed: installed }, row.node.id))), _jsx(ListFooter, { indicator: window.indicator, selectedCount: selectedCount, message: message }), _jsx(ActionPanel, { mode: action })] }));
|
|
134
|
+
return (_jsxs(Box, { flexDirection: "column", children: [state.warnings.map((warning) => _jsx(Text, { color: theme.warning, children: warning }, warning)), _jsx(CatalogHeader, { scope: scope, listed: visible.length, installable: installable.length, total: skills.length, filter: filter.filter }), rows.slice(window.start, window.end).map((row, offset) => (_jsx(CatalogRowLine, { row: row, selected: window.start + offset === index, marked: marks.marked.has(row.node.id), installed: installed }, row.node.id))), _jsx(CatalogSelectionHelp, { row: rows[index], height: helpLines }), _jsx(ListFooter, { indicator: window.indicator, selectedCount: selectedCount, message: message }), _jsx(ActionPanel, { mode: action })] }));
|
|
112
135
|
}
|
|
@@ -10,6 +10,10 @@ export function packageVersionLabel(evidence) {
|
|
|
10
10
|
export function installedEvidenceLabel(evidence) {
|
|
11
11
|
switch (evidence.kind) {
|
|
12
12
|
case "installed": {
|
|
13
|
+
if (evidence.reason !== undefined) {
|
|
14
|
+
const reason = evidence.reason.includes("local modifications") ? "local changes" : evidence.reason;
|
|
15
|
+
return `installed (version unavailable: ${reason})`;
|
|
16
|
+
}
|
|
13
17
|
if (evidence.version !== undefined)
|
|
14
18
|
return `installed ${evidence.version}`;
|
|
15
19
|
if (evidence.commit !== undefined)
|
|
@@ -46,7 +50,7 @@ export function packageRow(record, inspection) {
|
|
|
46
50
|
name: inspection.descriptor?.name ?? describeCoordinates(record.selection),
|
|
47
51
|
installed: installedEvidenceLabel(inspection.installed),
|
|
48
52
|
available: packageVersionLabel(inspection.available),
|
|
49
|
-
status: classifyUpdate(inspection.installed, inspection.available),
|
|
53
|
+
status: classifyUpdate(inspection.installed, inspection.available, inspection.availableCommit, inspection.installedCommitIsAncestor),
|
|
50
54
|
...(inspection.drift === undefined ? {} : { drift: inspection.drift }),
|
|
51
55
|
};
|
|
52
56
|
}
|
package/dist/src/tui/render.js
CHANGED
|
@@ -3,6 +3,6 @@ import { render } from "ink";
|
|
|
3
3
|
import { App } from "./app.js";
|
|
4
4
|
/** Renders the TUI and resolves once the user exits it. */
|
|
5
5
|
export async function renderTui(operations, environment) {
|
|
6
|
-
const instance = render(_jsx(App, { operations: operations, environment: environment }));
|
|
6
|
+
const instance = render(_jsx(App, { operations: operations, environment: environment }), { alternateScreen: true });
|
|
7
7
|
await instance.waitUntilExit();
|
|
8
8
|
}
|
package/dist/src/tui/window.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useWindowSize } from "ink";
|
|
2
2
|
/** Rows assumed when the terminal size is unknown (for example under test renderers). */
|
|
3
3
|
export const FALLBACK_TERMINAL_ROWS = 24;
|
|
4
4
|
/** Lines the shell itself uses: title, tab bar, separator and footer hint. */
|
|
@@ -23,9 +23,8 @@ export function pageStep(height) {
|
|
|
23
23
|
* view's own `reserved` lines. `override` fixes the height for tests.
|
|
24
24
|
*/
|
|
25
25
|
export function useListHeight(override, reserved) {
|
|
26
|
-
const {
|
|
26
|
+
const { rows } = useWindowSize();
|
|
27
27
|
if (override !== undefined)
|
|
28
28
|
return Math.max(override, 1);
|
|
29
|
-
|
|
30
|
-
return Math.max(rows - APP_CHROME_ROWS - reserved, MINIMUM_ROWS);
|
|
29
|
+
return Math.max((rows || FALLBACK_TERMINAL_ROWS) - APP_CHROME_ROWS - reserved, MINIMUM_ROWS);
|
|
31
30
|
}
|