@iseoane/agent-depot 0.5.0 → 0.5.2
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 +34 -0
- package/README.es.md +78 -33
- package/README.md +66 -25
- package/dist/src/cli.js +55 -0
- package/dist/src/git-source.js +21 -0
- package/dist/src/package-cli.js +2 -1
- package/dist/src/package-flow.js +14 -5
- package/dist/src/package-host-plans.js +12 -9
- package/dist/src/package-host-state.js +133 -5
- package/dist/src/package-owned-skills.js +8 -6
- package/dist/src/process-runner.js +9 -1
- package/dist/src/self-update.js +141 -0
- 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 +9 -3
- package/dist/src/tui/render.js +1 -1
- package/dist/src/tui/window.js +3 -4
- package/package.json +32 -12
- package/resources/pi/agents-panel/contract.ts +62 -0
- package/resources/pi/agents-panel/index.ts +471 -0
- package/skills/doctor-md-agents/SKILL.md +2 -1
- package/skills/doctor-md-agents/scripts/scan_corrections.py +141 -13
- package/skills/doctor-md-skill/references/supported-harnesses.md +2 -0
- package/skills/doctor-md-skill/scripts/collect_sessions.py +26 -2
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
|
/**
|
|
@@ -574,7 +581,7 @@ function pinContradiction(selection, observed) {
|
|
|
574
581
|
* cannot be attested leaves the field absent.
|
|
575
582
|
*/
|
|
576
583
|
function verifiedEvidence(observed, available) {
|
|
577
|
-
if (observed.kind !== "installed") {
|
|
584
|
+
if (observed.kind !== "installed" || observed.reason !== undefined) {
|
|
578
585
|
return undefined;
|
|
579
586
|
}
|
|
580
587
|
if (observed.commit !== undefined) {
|
|
@@ -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(match.checkoutVersion, 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,13 +175,124 @@ 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 }),
|
|
166
184
|
});
|
|
167
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
|
+
const packageJson = JSON.parse(manifest);
|
|
258
|
+
const version = isRecord(packageJson) && typeof packageJson.version === "string" && packageJson.version.trim() !== ""
|
|
259
|
+
? packageJson.version
|
|
260
|
+
: undefined;
|
|
261
|
+
if (status !== "") {
|
|
262
|
+
return {
|
|
263
|
+
...(version === undefined ? {} : { version }),
|
|
264
|
+
issue: `Pi reports this Package installed, but the Pi Git checkout has local modifications. Update assessment is unavailable; inspect the checkout before updating.`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return { commit: commit.toLowerCase(), ...(version === undefined ? {} : { version }) };
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
const message = error instanceof Error ? error.message : "";
|
|
271
|
+
const reason = message.includes("timed out")
|
|
272
|
+
? "Git checkout verification timed out"
|
|
273
|
+
: message.includes("symbolic link")
|
|
274
|
+
? "the configured checkout path contains a symbolic link"
|
|
275
|
+
: "the Pi Git checkout could not be read safely";
|
|
276
|
+
return unavailable(reason);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async function runPiGit(checkoutPath, args) {
|
|
280
|
+
const result = await runProcess("git", ["-C", checkoutPath, ...args], {
|
|
281
|
+
captureStdout: true,
|
|
282
|
+
maxOutputBytes: 1024 * 1024,
|
|
283
|
+
maxStderrBytes: 8192,
|
|
284
|
+
timeoutMs: 3000,
|
|
285
|
+
});
|
|
286
|
+
if (result.timedOut)
|
|
287
|
+
throw new Error("Pi checkout verification timed out");
|
|
288
|
+
if (result.outputTooLarge || result.code !== 0 || result.signal !== null)
|
|
289
|
+
throw new Error("Pi checkout verification command failed");
|
|
290
|
+
return result.stdout.toString("utf8").trim();
|
|
291
|
+
}
|
|
292
|
+
function piSourceIdentityFromUrl(origin) {
|
|
293
|
+
const canonical = canonicalizeGitSourceUrl(origin);
|
|
294
|
+
return `git:${canonical.replace(/^[a-z][a-z0-9+.-]*:\/\//iu, "")}`;
|
|
295
|
+
}
|
|
168
296
|
function unknownEvidence(reason) {
|
|
169
297
|
return Object.freeze({ kind: "unknown", reason });
|
|
170
298
|
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { compareAppVersions } from "./app-version.js";
|
|
4
|
+
import { createPortableExecutableResolver } from "./executable-resolution.js";
|
|
5
|
+
import { runProcess } from "./process-runner.js";
|
|
6
|
+
const REGISTRY_URL = "https://registry.npmjs.org";
|
|
7
|
+
const MAX_REGISTRY_RESPONSE_BYTES = 8 * 1024;
|
|
8
|
+
const MAX_NPM_STDERR_BYTES = 16 * 1024;
|
|
9
|
+
const COMMAND_TIMEOUT_MS = 30_000;
|
|
10
|
+
function compareVersions(left, right) {
|
|
11
|
+
const comparison = compareAppVersions(left, right);
|
|
12
|
+
if (comparison === undefined)
|
|
13
|
+
throw new Error(`Invalid semantic version comparison ${JSON.stringify(left)} and ${JSON.stringify(right)}`);
|
|
14
|
+
return comparison;
|
|
15
|
+
}
|
|
16
|
+
async function latestVersion(fetcher, packageName) {
|
|
17
|
+
const encodedName = encodeURIComponent(packageName).replace(/^%40/u, "@");
|
|
18
|
+
const response = await fetcher(`${REGISTRY_URL}/${encodedName}/latest`, {
|
|
19
|
+
signal: AbortSignal.timeout(10_000),
|
|
20
|
+
credentials: "omit",
|
|
21
|
+
redirect: "error",
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok)
|
|
24
|
+
throw new Error(`npm registry returned HTTP ${response.status}`);
|
|
25
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
26
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_REGISTRY_RESPONSE_BYTES) {
|
|
27
|
+
throw new Error("npm registry response exceeded the size limit");
|
|
28
|
+
}
|
|
29
|
+
if (!response.body)
|
|
30
|
+
throw new Error("npm registry response did not include a body");
|
|
31
|
+
const reader = response.body.getReader();
|
|
32
|
+
const chunks = [];
|
|
33
|
+
let responseBytes = 0;
|
|
34
|
+
try {
|
|
35
|
+
for (;;) {
|
|
36
|
+
const chunk = await reader.read();
|
|
37
|
+
if (chunk.done)
|
|
38
|
+
break;
|
|
39
|
+
responseBytes += chunk.value.byteLength;
|
|
40
|
+
if (responseBytes > MAX_REGISTRY_RESPONSE_BYTES) {
|
|
41
|
+
await reader.cancel();
|
|
42
|
+
throw new Error("npm registry response exceeded the size limit");
|
|
43
|
+
}
|
|
44
|
+
chunks.push(chunk.value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
reader.releaseLock();
|
|
49
|
+
}
|
|
50
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
51
|
+
let payload;
|
|
52
|
+
try {
|
|
53
|
+
payload = JSON.parse(body);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new Error("npm registry returned invalid JSON");
|
|
57
|
+
}
|
|
58
|
+
if (typeof payload !== "object" || payload === null || !("version" in payload) || typeof payload.version !== "string") {
|
|
59
|
+
throw new Error("npm registry response did not contain a version");
|
|
60
|
+
}
|
|
61
|
+
compareVersions(payload.version, payload.version);
|
|
62
|
+
return payload.version;
|
|
63
|
+
}
|
|
64
|
+
function successfulStdout(result) {
|
|
65
|
+
if (result.code !== 0 || result.signal !== null || result.timedOut || result.outputTooLarge)
|
|
66
|
+
return undefined;
|
|
67
|
+
return result.stdout.toString("utf8").trim();
|
|
68
|
+
}
|
|
69
|
+
export function createSelfUpdateOperations(options) {
|
|
70
|
+
const resolveExecutable = options.resolveExecutable ?? createPortableExecutableResolver();
|
|
71
|
+
const run = options.run ?? runProcess;
|
|
72
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
73
|
+
async function npmGlobalExecutable() {
|
|
74
|
+
const npm = await resolveExecutable("npm");
|
|
75
|
+
if (npm.blocked || npm.executable === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
const target = await realpath(npm.executable).catch(() => undefined);
|
|
78
|
+
if (target === undefined || path.basename(target) !== "npm-cli.js" || path.basename(path.dirname(target)) !== "bin" || path.basename(path.dirname(path.dirname(target))) !== "npm")
|
|
79
|
+
return undefined;
|
|
80
|
+
let rootResult;
|
|
81
|
+
try {
|
|
82
|
+
rootResult = await run(npm.executable, ["root", "--global", "--silent", "--logs-max=0", "--no-update-notifier"], {
|
|
83
|
+
captureStdout: true,
|
|
84
|
+
maxOutputBytes: 4096,
|
|
85
|
+
maxStderrBytes: MAX_NPM_STDERR_BYTES,
|
|
86
|
+
timeoutMs: COMMAND_TIMEOUT_MS,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
const globalRoot = successfulStdout(rootResult);
|
|
93
|
+
if (globalRoot === undefined || !path.isAbsolute(globalRoot))
|
|
94
|
+
return undefined;
|
|
95
|
+
const installedPackagePath = path.join(globalRoot, ...options.packageName.split("/"));
|
|
96
|
+
const installedPackage = await lstat(installedPackagePath).catch(() => undefined);
|
|
97
|
+
if (installedPackage === undefined || !installedPackage.isDirectory())
|
|
98
|
+
return undefined;
|
|
99
|
+
const actualRoot = await realpath(options.packageRoot).catch(() => undefined);
|
|
100
|
+
const reportedRoot = await realpath(installedPackagePath).catch(() => undefined);
|
|
101
|
+
return actualRoot !== undefined && actualRoot === reportedRoot ? npm.executable : undefined;
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
async check() {
|
|
105
|
+
compareVersions(options.packageVersion, options.packageVersion);
|
|
106
|
+
const npm = await npmGlobalExecutable();
|
|
107
|
+
if (npm === undefined) {
|
|
108
|
+
return { kind: "unsupported", reason: "this CLI is not verified in npm's global package directory" };
|
|
109
|
+
}
|
|
110
|
+
const latest = await latestVersion(fetcher, options.packageName);
|
|
111
|
+
const comparison = compareVersions(latest, options.packageVersion);
|
|
112
|
+
if (comparison === 0)
|
|
113
|
+
return { kind: "current", current: options.packageVersion, latest };
|
|
114
|
+
if (comparison < 0)
|
|
115
|
+
return { kind: "ahead", current: options.packageVersion, latest };
|
|
116
|
+
return { kind: "available", current: options.packageVersion, latest };
|
|
117
|
+
},
|
|
118
|
+
async apply(version) {
|
|
119
|
+
let comparison;
|
|
120
|
+
try {
|
|
121
|
+
comparison = compareVersions(version, options.packageVersion);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw new Error(`Refusing invalid Agent Depot version ${JSON.stringify(version)}`);
|
|
125
|
+
}
|
|
126
|
+
if (comparison <= 0)
|
|
127
|
+
throw new Error(`Refusing to downgrade Agent Depot from ${options.packageVersion} to ${version}`);
|
|
128
|
+
const npm = await npmGlobalExecutable();
|
|
129
|
+
if (npm === undefined)
|
|
130
|
+
throw new Error("this CLI is not verified in npm's global package directory");
|
|
131
|
+
const result = await run(npm, [
|
|
132
|
+
"install", "--global", "--no-audit", "--no-fund", "--no-update-notifier", "--logs-max=0", "--ignore-scripts",
|
|
133
|
+
"--registry=https://registry.npmjs.org/", `${options.packageName}@${version}`,
|
|
134
|
+
], { maxStderrBytes: MAX_NPM_STDERR_BYTES, timeoutMs: COMMAND_TIMEOUT_MS });
|
|
135
|
+
if (result.code !== 0 || result.signal !== null || result.timedOut || result.outputTooLarge) {
|
|
136
|
+
const detail = result.timedOut ? "timed out" : result.outputTooLarge ? "produced too much output" : result.signal ? `stopped by ${result.signal}` : `exited with code ${result.code ?? "unknown"}`;
|
|
137
|
+
throw new Error(`npm global update ${detail}${result.stderr ? `: ${result.stderr.trim()}` : ""}`);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -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
|
+
}
|