@iseoane/agent-depot 0.2.1 → 0.4.0
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 +32 -0
- package/README.md +186 -1
- package/dist/src/app-cli.js +172 -0
- package/dist/src/app-flow.js +464 -0
- package/dist/src/app-latest.js +63 -0
- package/dist/src/app-owned-skills.js +42 -0
- package/dist/src/app-recipes.js +116 -0
- package/dist/src/app-schema.js +45 -0
- package/dist/src/app-version.js +46 -0
- package/dist/src/atomic-file.js +26 -0
- package/dist/src/cli.js +117 -20
- package/dist/src/process-runner.js +10 -1
- package/dist/src/profile-cli.js +88 -0
- package/dist/src/profile-export.js +135 -0
- package/dist/src/profile-import.js +218 -0
- package/dist/src/profile.js +130 -0
- package/dist/src/project-manifest.js +6 -0
- package/dist/src/skill-install.js +12 -0
- package/dist/src/source-state.js +3 -28
- package/dist/src/tui/app-actions.js +157 -0
- package/dist/src/tui/app.js +10 -4
- package/dist/src/tui/catalog-installs.js +5 -1
- package/dist/src/tui/installations-browse.js +24 -2
- package/dist/src/tui/installations-tree.js +13 -1
- package/dist/src/tui/installations-view.js +46 -6
- package/dist/src/tui/installations.js +26 -3
- package/dist/src/tui/manage-actions.js +2 -0
- package/dist/src/tui/manage-panel.js +6 -0
- package/dist/src/tui/profile-view.js +249 -0
- package/dist/src/tui/sources-recipes.js +216 -0
- package/dist/src/tui/sources-view.js +16 -7
- package/dist/src/tui/unmanaged-actions.js +2 -2
- package/dist/src/tui/updates-flow.js +18 -3
- package/dist/src/tui/updates-keys.js +9 -0
- package/dist/src/tui/updates-model.js +3 -3
- package/dist/src/tui/updates-panel.js +4 -2
- package/dist/src/tui/updates-view.js +1 -1
- package/dist/src/tui/updates.js +76 -5
- package/dist/src/unmanaged-removal.js +3 -0
- package/dist/src/user-global-skill-inventory.js +7 -1
- package/package.json +4 -4
- package/skills/agent-depot-apprecipe/SKILL.md +44 -0
- package/skills/agent-depot-apprecipe/assets/example.md +25 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { parsePortableInstallationMethod, PROJECT_HOSTS, } from "./project-manifest.js";
|
|
2
|
+
export const APP_GITHUB_PATTERN = "^[\\w-][\\w.-]*/[\\w-][\\w.-]*$";
|
|
3
|
+
export const APP_NPM_PATTERN = "^(?:@[a-z0-9~-][a-z0-9._~-]*/)?[a-z0-9~-][a-z0-9._~-]*$";
|
|
4
|
+
export const APP_SKILL_PATTERN = "^(?!\\*+$)(?!\\s*$)[^/\\\\\\u0000]+$";
|
|
5
|
+
function object(value, keys, field) {
|
|
6
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
7
|
+
throw new Error(`${field}: expected an object`);
|
|
8
|
+
}
|
|
9
|
+
const record = value;
|
|
10
|
+
const unsupported = Object.keys(record).find((key) => !keys.includes(key));
|
|
11
|
+
if (unsupported !== undefined)
|
|
12
|
+
throw new Error(`${field}.${unsupported}: unsupported recipe field`);
|
|
13
|
+
return record;
|
|
14
|
+
}
|
|
15
|
+
function text(value, field) {
|
|
16
|
+
if (typeof value !== "string" || !value.trim())
|
|
17
|
+
throw new Error(`${field}: expected non-empty text`);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function argv(value, field) {
|
|
21
|
+
return parsePortableInstallationMethod({ kind: "command", argv: value }, { label: `${field}:` }).argv;
|
|
22
|
+
}
|
|
23
|
+
function step(value, field) {
|
|
24
|
+
const item = object(value, ["argv", "manual"], field);
|
|
25
|
+
if (item.argv === undefined && item.manual === undefined) {
|
|
26
|
+
throw new Error(`${field}: step needs argv or manual`);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
...(item.argv === undefined ? {} : { argv: argv(item.argv, `${field}.argv`) }),
|
|
30
|
+
...(item.manual === undefined ? {} : { manual: text(item.manual, `${field}.manual`) }),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function version(value, field) {
|
|
34
|
+
const item = object(value, ["argv", "pattern"], field);
|
|
35
|
+
const pattern = text(item.pattern, `${field}.pattern`);
|
|
36
|
+
try {
|
|
37
|
+
new RegExp(pattern);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`${field}.pattern: ${error instanceof Error ? error.message : String(error)}`);
|
|
41
|
+
}
|
|
42
|
+
// Count captures while ignoring escapes, character classes and non-capturing/lookaround groups.
|
|
43
|
+
let captures = 0;
|
|
44
|
+
let inClass = false;
|
|
45
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
46
|
+
if (pattern[i] === "\\") {
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (pattern[i] === "[")
|
|
51
|
+
inClass = true;
|
|
52
|
+
if (pattern[i] === "]")
|
|
53
|
+
inClass = false;
|
|
54
|
+
const namedCapture = /^\?<[^=!]/u.test(pattern.slice(i + 1));
|
|
55
|
+
if (!inClass && pattern[i] === "(" && (pattern[i + 1] !== "?" || namedCapture))
|
|
56
|
+
captures++;
|
|
57
|
+
}
|
|
58
|
+
if (captures !== 1)
|
|
59
|
+
throw new Error(`${field}.pattern: expected exactly one capture group`);
|
|
60
|
+
return { argv: argv(item.argv, `${field}.argv`), pattern };
|
|
61
|
+
}
|
|
62
|
+
function hosts(value, field) {
|
|
63
|
+
return Object.fromEntries(Object.entries(object(value, PROJECT_HOSTS, field)).map(([host, item]) => [host, step(item, `${field}.${host}`)]));
|
|
64
|
+
}
|
|
65
|
+
function latest(value) {
|
|
66
|
+
const item = object(value, ["github", "npm", "argv", "pattern"], "latest");
|
|
67
|
+
if (Object.keys(item).length === 1 && item.github !== undefined) {
|
|
68
|
+
const github = text(item.github, "latest.github");
|
|
69
|
+
if (!new RegExp(APP_GITHUB_PATTERN).test(github))
|
|
70
|
+
throw new Error("latest.github: expected owner/repo");
|
|
71
|
+
return { github };
|
|
72
|
+
}
|
|
73
|
+
if (Object.keys(item).length === 1 && item.npm !== undefined) {
|
|
74
|
+
const npm = text(item.npm, "latest.npm");
|
|
75
|
+
if (npm.length > 214 || !new RegExp(APP_NPM_PATTERN).test(npm) || ["node_modules", "favicon.ico"].includes(npm)) {
|
|
76
|
+
throw new Error("latest.npm: expected a valid npm package name");
|
|
77
|
+
}
|
|
78
|
+
return { npm };
|
|
79
|
+
}
|
|
80
|
+
return version(item, "latest");
|
|
81
|
+
}
|
|
82
|
+
export function parseAppRecipe(value) {
|
|
83
|
+
const item = object(value, [
|
|
84
|
+
"name", "platform", "homepage", "install", "update", "uninstall", "version",
|
|
85
|
+
"latest", "setup", "teardown", "skills",
|
|
86
|
+
], "recipe");
|
|
87
|
+
if (item.platform !== undefined && (typeof item.platform !== "string" || !["linux", "windows", "darwin"].includes(item.platform))) {
|
|
88
|
+
throw new Error("platform: unsupported platform");
|
|
89
|
+
}
|
|
90
|
+
let skills;
|
|
91
|
+
if (item.skills !== undefined) {
|
|
92
|
+
if (!Array.isArray(item.skills))
|
|
93
|
+
throw new Error("skills: expected a list");
|
|
94
|
+
skills = [];
|
|
95
|
+
for (const [index, skill] of item.skills.entries()) {
|
|
96
|
+
const pattern = text(skill, `skills[${index}]`);
|
|
97
|
+
if (!new RegExp(APP_SKILL_PATTERN, "u").test(pattern)) {
|
|
98
|
+
throw new Error(`skills[${index}]: expected a non-empty name pattern without path separators or NUL, and not only stars`);
|
|
99
|
+
}
|
|
100
|
+
skills.push(pattern);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
name: text(item.name, "name"),
|
|
105
|
+
install: step(item.install, "install"),
|
|
106
|
+
update: step(item.update, "update"),
|
|
107
|
+
uninstall: step(item.uninstall, "uninstall"),
|
|
108
|
+
version: version(item.version, "version"),
|
|
109
|
+
...(item.platform === undefined ? {} : { platform: item.platform }),
|
|
110
|
+
...(item.homepage === undefined ? {} : { homepage: text(item.homepage, "homepage") }),
|
|
111
|
+
...(item.latest === undefined ? {} : { latest: latest(item.latest) }),
|
|
112
|
+
...(item.setup === undefined ? {} : { setup: hosts(item.setup, "setup") }),
|
|
113
|
+
...(item.teardown === undefined ? {} : { teardown: hosts(item.teardown, "teardown") }),
|
|
114
|
+
...(skills === undefined ? {} : { skills }),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { APP_GITHUB_PATTERN, APP_NPM_PATTERN, APP_SKILL_PATTERN } from "./app-recipes.js";
|
|
2
|
+
import { PROJECT_HOSTS } from "./project-manifest.js";
|
|
3
|
+
const argv = {
|
|
4
|
+
type: "array", minItems: 1, items: { type: "string", minLength: 1 },
|
|
5
|
+
description: "Bare no-shell executable and arguments; user-method safety rules also apply.",
|
|
6
|
+
};
|
|
7
|
+
const text = { type: "string", minLength: 1 };
|
|
8
|
+
const step = {
|
|
9
|
+
type: "object", additionalProperties: false,
|
|
10
|
+
properties: { argv, manual: text },
|
|
11
|
+
anyOf: [{ required: ["argv"] }, { required: ["manual"] }],
|
|
12
|
+
};
|
|
13
|
+
const version = {
|
|
14
|
+
type: "object", additionalProperties: false, required: ["argv", "pattern"],
|
|
15
|
+
properties: {
|
|
16
|
+
argv, pattern: { ...text, description: "Valid JavaScript regex with exactly one capture group." },
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
const hosts = {
|
|
20
|
+
type: "object", additionalProperties: false,
|
|
21
|
+
properties: Object.fromEntries(PROJECT_HOSTS.map((host) => [host, step])),
|
|
22
|
+
};
|
|
23
|
+
export const APP_RECIPE_SCHEMA = {
|
|
24
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
25
|
+
title: "Agent Depot App recipe", type: "object", additionalProperties: false,
|
|
26
|
+
required: ["name", "install", "update", "uninstall", "version"],
|
|
27
|
+
properties: {
|
|
28
|
+
name: text, platform: { enum: ["linux", "windows", "darwin"] }, homepage: text,
|
|
29
|
+
install: step, update: step, uninstall: step, version,
|
|
30
|
+
latest: {
|
|
31
|
+
oneOf: [
|
|
32
|
+
version,
|
|
33
|
+
{
|
|
34
|
+
type: "object", additionalProperties: false, required: ["github"],
|
|
35
|
+
properties: { github: { ...text, pattern: APP_GITHUB_PATTERN } },
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: "object", additionalProperties: false, required: ["npm"],
|
|
39
|
+
properties: { npm: { ...text, pattern: APP_NPM_PATTERN, maxLength: 214, not: { enum: ["node_modules", "favicon.ico"] } } },
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
setup: hosts, teardown: hosts, skills: { type: "array", items: { ...text, pattern: APP_SKILL_PATTERN } },
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Strict SemVer ordering; unlike Skill's permissive comparator, opaque inputs stay distinguishable. */
|
|
2
|
+
export function compareAppVersions(left, right) {
|
|
3
|
+
const parse = (value) => {
|
|
4
|
+
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?(?:\+[\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*)?$/u.exec(normalizeAppVersion(value));
|
|
5
|
+
if (!match)
|
|
6
|
+
return undefined;
|
|
7
|
+
const pre = match[4]?.split(".") ?? [];
|
|
8
|
+
if (pre.some(part => /^\d+$/u.test(part) && /^0\d/u.test(part)))
|
|
9
|
+
return undefined;
|
|
10
|
+
return { core: match.slice(1, 4).map(part => BigInt(part)), pre };
|
|
11
|
+
};
|
|
12
|
+
const a = parse(left);
|
|
13
|
+
const b = parse(right);
|
|
14
|
+
if (!a || !b)
|
|
15
|
+
return undefined;
|
|
16
|
+
for (let index = 0; index < 3; index++) {
|
|
17
|
+
if (a.core[index] !== b.core[index])
|
|
18
|
+
return a.core[index] < b.core[index] ? -1 : 1;
|
|
19
|
+
}
|
|
20
|
+
if (!a.pre.length || !b.pre.length)
|
|
21
|
+
return Math.sign(b.pre.length - a.pre.length);
|
|
22
|
+
for (let index = 0; index < Math.max(a.pre.length, b.pre.length); index++) {
|
|
23
|
+
const x = a.pre[index];
|
|
24
|
+
const y = b.pre[index];
|
|
25
|
+
if (x === y)
|
|
26
|
+
continue;
|
|
27
|
+
if (x === undefined)
|
|
28
|
+
return -1;
|
|
29
|
+
if (y === undefined)
|
|
30
|
+
return 1;
|
|
31
|
+
const numericX = /^\d+$/u.test(x);
|
|
32
|
+
const numericY = /^\d+$/u.test(y);
|
|
33
|
+
if (numericX && numericY)
|
|
34
|
+
return BigInt(x) < BigInt(y) ? -1 : 1;
|
|
35
|
+
if (numericX !== numericY)
|
|
36
|
+
return numericX ? -1 : 1;
|
|
37
|
+
return x < y ? -1 : 1;
|
|
38
|
+
}
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
export function normalizeAppVersion(version) {
|
|
42
|
+
return version.replace(/^v(?=\d)/u, "");
|
|
43
|
+
}
|
|
44
|
+
export function sameAppVersion(left, right) {
|
|
45
|
+
return compareAppVersions(left, right) === 0 || normalizeAppVersion(left) === normalizeAppVersion(right);
|
|
46
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
/** Write a unique temporary sibling, then publish it with rename. */
|
|
4
|
+
export async function writeFileAtomically(file, contents, options = {}) {
|
|
5
|
+
const temporaryPath = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
6
|
+
try {
|
|
7
|
+
await writeFile(temporaryPath, contents, { encoding: "utf8", flag: "wx", mode: options.mode });
|
|
8
|
+
try {
|
|
9
|
+
await rename(temporaryPath, file);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
const code = error.code;
|
|
13
|
+
if (!options.allowLockedReplaceFallback || !["EEXIST", "EPERM", "ENOTEMPTY"].includes(code ?? "")) {
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
// Source state holds its read-modify-write lock while using this Windows fallback.
|
|
17
|
+
await writeFile(file, contents, "utf8");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
await unlink(temporaryPath).catch((error) => {
|
|
22
|
+
if (error.code !== "ENOENT")
|
|
23
|
+
throw error;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/src/cli.js
CHANGED
|
@@ -3,6 +3,9 @@ import { realpathSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import { checkAppUpdates, createAppOperations } from "./app-flow.js";
|
|
7
|
+
import { describeAppResult, describeManualCompletion, runAppCommand } from "./app-cli.js";
|
|
8
|
+
import { EXPORT_USAGE, IMPORT_USAGE, runProfileExport, runProfileImport } from "./profile-cli.js";
|
|
6
9
|
import { pathsOverlap } from "./path-safety.js";
|
|
7
10
|
import { skillTreeBaseline } from "./skill-discovery.js";
|
|
8
11
|
import { BuiltInSourceError, createSourceOperations, resolveProjectSource, SourceMetadataError, SourceNotFoundError, } from "./sources.js";
|
|
@@ -28,11 +31,19 @@ const USAGE = [
|
|
|
28
31
|
" agent-depot install --scope <project|user-global> --source <id> --skill <path> --host <host>... --version <latest|version> [--ref <git-ref>] [--method <json>] --portable-v1 [--yes] [--confirm-additional-host]",
|
|
29
32
|
" agent-depot install --scope project --manifest --portable-v1 [--yes] [--confirm-additional-host]",
|
|
30
33
|
" agent-depot update check --scope <project|user-global>",
|
|
31
|
-
" agent-depot update apply --scope <project|user-global> (--all | --skill <id|path>...) [--yes] [--confirm-path <relative-path>...]",
|
|
34
|
+
" agent-depot update apply --scope <project|user-global> (--all | --skill <id|path>... | --app <name>...) [--yes] [--confirm-path <relative-path>...]",
|
|
35
|
+
" Apps run after Skills; --all --scope project skips Apps.",
|
|
32
36
|
" agent-depot skill remove <id|path>... [--yes]",
|
|
33
37
|
" agent-depot skill remove <id|path> --host <host>... [--yes]",
|
|
34
38
|
" agent-depot skill host add <id|path> --host <host>... [--yes] [--confirm-additional-host]",
|
|
35
39
|
" agent-depot uninstall [--skills] [--unmanaged-skill <exact-global-path>...] [--data] [--cli] [--yes]",
|
|
40
|
+
" agent-depot app schema|list",
|
|
41
|
+
" agent-depot app validate <file>...",
|
|
42
|
+
" agent-depot app approve <name> [--yes]",
|
|
43
|
+
" agent-depot app install|update|uninstall <name> [--yes] [--manual-done]",
|
|
44
|
+
" agent-depot app uninstall <name> --forget [--yes]",
|
|
45
|
+
` ${EXPORT_USAGE}`,
|
|
46
|
+
` ${IMPORT_USAGE}`,
|
|
36
47
|
" agent-depot tui",
|
|
37
48
|
" agent-depot --version",
|
|
38
49
|
].join("\n");
|
|
@@ -60,6 +71,9 @@ const SOURCE_SUBCOMMANDS = new Map([
|
|
|
60
71
|
["migrate", (values, { operations, dependencies, output }) => runSourceMigration(values, operations, dependencies, output)],
|
|
61
72
|
]);
|
|
62
73
|
const COMMANDS = new Map([
|
|
74
|
+
["import", (values, { operations, dependencies, output }) => runProfileImport(values, operations, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output, dependencies)],
|
|
75
|
+
["export", (values, { operations, dependencies, output }) => runProfileExport(values, operations, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output, dependencies.stderr ?? (line => console.error(line)), dependencies.homeDirectory)],
|
|
76
|
+
["app", (values, { dependencies, output }) => runAppCommand(values, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output)],
|
|
63
77
|
["install", async (values, { operations, dependencies, output }) => {
|
|
64
78
|
await runInstall(values, operations, dependencies, output);
|
|
65
79
|
return 0;
|
|
@@ -669,21 +683,73 @@ async function runUninstall(argv, operations, dependencies, output) {
|
|
|
669
683
|
}
|
|
670
684
|
async function runUpdate(argv, operations, dependencies, output) {
|
|
671
685
|
const options = parseUpdateOptions(argv);
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
686
|
+
const apps = dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory });
|
|
687
|
+
let appUpdates = [];
|
|
688
|
+
let appCheckFailed = false;
|
|
689
|
+
if (options.scope === "user-global") {
|
|
690
|
+
try {
|
|
691
|
+
appUpdates = await checkAppUpdates(apps);
|
|
692
|
+
}
|
|
693
|
+
catch (error) {
|
|
694
|
+
appCheckFailed = true;
|
|
695
|
+
output(`Error (Apps): ${error instanceof Error ? error.message : String(error)}`);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
let loaded;
|
|
699
|
+
try {
|
|
700
|
+
loaded = await loadUpdates({
|
|
701
|
+
scope: options.scope,
|
|
702
|
+
operations,
|
|
703
|
+
...(dependencies.homeDirectory === undefined ? {} : { homeDirectory: dependencies.homeDirectory }),
|
|
704
|
+
...(dependencies.projectRoot === undefined ? {} : { projectRoot: dependencies.projectRoot }),
|
|
705
|
+
...(dependencies.sourceAccess === undefined ? {} : { sourceAccess: dependencies.sourceAccess }),
|
|
706
|
+
...(dependencies.projectManifestStore === undefined ? {} : { projectManifestStore: dependencies.projectManifestStore }),
|
|
707
|
+
...(dependencies.installationOptions?.fileSystem === undefined ? {} : { installationFileSystem: dependencies.installationOptions.fileSystem }),
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
output(`Error (Skills, ${options.scope}): ${error instanceof Error ? error.message : String(error)}`);
|
|
712
|
+
}
|
|
713
|
+
if (loaded) {
|
|
714
|
+
const { assessment, installed, context } = loaded;
|
|
715
|
+
const userGlobalInventory = options.action === "check" && options.scope === "user-global"
|
|
716
|
+
? await scanUserGlobalSkillInventory({ homeDirectory: context.projectRoot, managedInstallations: installed, reportWarning: output })
|
|
717
|
+
: undefined;
|
|
718
|
+
outputUpdateAssessment(assessment, options.scope, output, userGlobalInventory);
|
|
719
|
+
}
|
|
720
|
+
for (const item of appUpdates) {
|
|
721
|
+
output(`App ${item.entry.recipe?.name ?? item.entry.file}: ${item.status}; installed ${item.installedVersion ?? "unknown"}; latest ${item.latestVersion ?? "unknown"}${item.reason ? `; ${item.reason}` : ""}`);
|
|
722
|
+
}
|
|
723
|
+
if (!loaded)
|
|
724
|
+
return 1;
|
|
685
725
|
if (options.action === "check") {
|
|
686
|
-
return 0;
|
|
726
|
+
return appCheckFailed ? 1 : 0;
|
|
727
|
+
}
|
|
728
|
+
const { assessment, context } = loaded;
|
|
729
|
+
const selectedApps = options.all ? appUpdates.filter(item => item.status === "update available")
|
|
730
|
+
: options.requestedApps.map(name => {
|
|
731
|
+
const matches = appUpdates.filter(item => item.entry.recipe?.name === name);
|
|
732
|
+
if (matches.length !== 1 || matches[0].status !== "update available") {
|
|
733
|
+
throw new CliUsageError(`App ${JSON.stringify(name)} is unknown or not updateable`);
|
|
734
|
+
}
|
|
735
|
+
return matches[0];
|
|
736
|
+
});
|
|
737
|
+
const appPlans = [];
|
|
738
|
+
let appFailed = appCheckFailed ? 1 : 0;
|
|
739
|
+
for (const item of selectedApps) {
|
|
740
|
+
try {
|
|
741
|
+
const plan = await apps.planLifecycle(item.entry, "update", undefined, item);
|
|
742
|
+
output(`Preview: update App ${item.entry.recipe.name}: ${JSON.stringify(plan.argv ?? [])}; executable ${plan.executable ?? "unresolved"}; cwd ${plan.cwd}; environment ${plan.environment}`);
|
|
743
|
+
if (plan.warning)
|
|
744
|
+
output(plan.warning);
|
|
745
|
+
if (plan.manual)
|
|
746
|
+
output(`Manual (never executed): ${plan.manual}`);
|
|
747
|
+
appPlans.push(plan);
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
appFailed++;
|
|
751
|
+
output(`App ${item.entry.recipe.name}: preview failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
752
|
+
}
|
|
687
753
|
}
|
|
688
754
|
const selected = selectUpdateCandidates(assessment, options);
|
|
689
755
|
const { plans, failures: previewFailures, nonCanonicalPaths } = await previewUpdateCandidates(selected, context);
|
|
@@ -717,10 +783,32 @@ async function runUpdate(argv, operations, dependencies, output) {
|
|
|
717
783
|
for (const item of result.failed) {
|
|
718
784
|
output(`Failed Skill ${JSON.stringify(item.selection.path)} (${item.id}): ${item.error ?? "unknown error"}`);
|
|
719
785
|
}
|
|
720
|
-
|
|
721
|
-
|
|
786
|
+
let appUpdated = 0;
|
|
787
|
+
for (const plan of appPlans) {
|
|
788
|
+
try {
|
|
789
|
+
const outcome = await apps.executeLifecycle(plan, true);
|
|
790
|
+
output(`App ${plan.entry.recipe.name}: ${describeAppResult(outcome)}${outcome.reason ? `: ${outcome.reason}` : ""}`);
|
|
791
|
+
if (outcome.status === "installed")
|
|
792
|
+
appUpdated++;
|
|
793
|
+
else {
|
|
794
|
+
appFailed++;
|
|
795
|
+
if (outcome.manual) {
|
|
796
|
+
output(`Manual (never executed): ${outcome.manual}`);
|
|
797
|
+
output(describeManualCompletion(plan));
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
appFailed++;
|
|
803
|
+
output(`App ${plan.entry.recipe.name}: failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
output(`Update summary: ${result.updated.length + appUpdated} updated, ${result.failed.length + appFailed} failed`);
|
|
807
|
+
return result.failed.length + appFailed === 0 ? 0 : 1;
|
|
722
808
|
}
|
|
723
809
|
function selectUpdateCandidates(assessment, options) {
|
|
810
|
+
if (!options.all && !options.requested.length)
|
|
811
|
+
return [];
|
|
724
812
|
const requested = options.all
|
|
725
813
|
? "all"
|
|
726
814
|
: resolveUpdateSelectionIds(assessment, options.requested);
|
|
@@ -742,6 +830,7 @@ function parseUpdateOptions(argv) {
|
|
|
742
830
|
let all = false;
|
|
743
831
|
let confirmed = false;
|
|
744
832
|
const requested = [];
|
|
833
|
+
const requestedApps = [];
|
|
745
834
|
const confirmedPaths = [];
|
|
746
835
|
for (let index = 1; index < argv.length; index += 1) {
|
|
747
836
|
switch (argv[index]) {
|
|
@@ -754,6 +843,9 @@ function parseUpdateOptions(argv) {
|
|
|
754
843
|
case "--all":
|
|
755
844
|
all = true;
|
|
756
845
|
break;
|
|
846
|
+
case "--app":
|
|
847
|
+
requestedApps.push(requireOptionValue(argv, ++index, "--app"));
|
|
848
|
+
break;
|
|
757
849
|
case "--skill":
|
|
758
850
|
requested.push(requireOptionValue(argv, ++index, "--skill"));
|
|
759
851
|
break;
|
|
@@ -767,12 +859,16 @@ function parseUpdateOptions(argv) {
|
|
|
767
859
|
if (scope !== "project" && scope !== "user-global") {
|
|
768
860
|
throw new CliUsageError("Update scope must be explicit: use --scope project or --scope user-global");
|
|
769
861
|
}
|
|
770
|
-
if (action === "check" && (all || requested.length > 0 || confirmed || confirmedPaths.length > 0)) {
|
|
862
|
+
if (action === "check" && (all || requested.length > 0 || requestedApps.length > 0 || confirmed || confirmedPaths.length > 0)) {
|
|
771
863
|
throw new CliUsageError("update check only accepts --scope; use update apply to select and apply updates");
|
|
772
864
|
}
|
|
773
|
-
if (action === "apply" && (all === (requested.length > 0))) {
|
|
774
|
-
throw new CliUsageError("update apply requires exactly one selection mode: --all or one or more --skill values");
|
|
865
|
+
if (action === "apply" && (all === (requested.length + requestedApps.length > 0))) {
|
|
866
|
+
throw new CliUsageError("update apply requires exactly one selection mode: --all or one or more --skill/--app values");
|
|
775
867
|
}
|
|
868
|
+
if (requestedApps.length && scope !== "user-global")
|
|
869
|
+
throw new CliUsageError("Apps are user-global only");
|
|
870
|
+
if (new Set(requestedApps).size !== requestedApps.length)
|
|
871
|
+
throw new CliUsageError("Duplicate App selection");
|
|
776
872
|
if (scope === "user-global" && confirmedPaths.length > 0) {
|
|
777
873
|
throw new CliUsageError("--confirm-path only applies to --scope project");
|
|
778
874
|
}
|
|
@@ -781,6 +877,7 @@ function parseUpdateOptions(argv) {
|
|
|
781
877
|
scope: scope,
|
|
782
878
|
all,
|
|
783
879
|
requested: Object.freeze(requested),
|
|
880
|
+
requestedApps: Object.freeze(requestedApps),
|
|
784
881
|
confirmed,
|
|
785
882
|
confirmedPaths: Object.freeze(confirmedPaths),
|
|
786
883
|
};
|
|
@@ -10,6 +10,7 @@ export function runProcess(command, args, options = {}) {
|
|
|
10
10
|
const stdout = [];
|
|
11
11
|
let stdoutBytes = 0;
|
|
12
12
|
let stderr = "";
|
|
13
|
+
let stderrBytes = 0;
|
|
13
14
|
let outputTooLarge = false;
|
|
14
15
|
child.stdout?.on("data", (chunk) => {
|
|
15
16
|
stdoutBytes += chunk.length;
|
|
@@ -22,7 +23,15 @@ export function runProcess(command, args, options = {}) {
|
|
|
22
23
|
});
|
|
23
24
|
child.stderr?.setEncoding("utf8");
|
|
24
25
|
child.stderr?.on("data", (chunk) => {
|
|
25
|
-
|
|
26
|
+
const bytes = Buffer.from(chunk);
|
|
27
|
+
const remaining = options.maxStderrBytes === undefined
|
|
28
|
+
? bytes.length : Math.max(0, options.maxStderrBytes - stderrBytes);
|
|
29
|
+
stderrBytes += bytes.length;
|
|
30
|
+
stderr += bytes.subarray(0, remaining).toString("utf8");
|
|
31
|
+
if (options.maxStderrBytes !== undefined && stderrBytes > options.maxStderrBytes) {
|
|
32
|
+
outputTooLarge = true;
|
|
33
|
+
child.kill();
|
|
34
|
+
}
|
|
26
35
|
});
|
|
27
36
|
child.on("error", reject);
|
|
28
37
|
child.on("close", (code, signal) => {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { buildProfileExport, requireProfileExportSelection, writeProfileExport } from "./profile-export.js";
|
|
3
|
+
import { applyProfileImport, buildProfileImport } from "./profile-import.js";
|
|
4
|
+
import { serializeProfile } from "./profile.js";
|
|
5
|
+
import { CliUsageError } from "./usage-error.js";
|
|
6
|
+
export const EXPORT_USAGE = "agent-depot export [--out <file>] [--no-sources] [--no-skills] [--no-apps] [--source <id|url>...] [--skill <path|name>...] [--app <name>...]";
|
|
7
|
+
export const IMPORT_USAGE = "agent-depot import <file> [--yes] [--no-sources] [--no-skills] [--no-apps] [--source <id|url>...] [--skill <path|name>...] [--app <name>...]";
|
|
8
|
+
export async function runProfileExport(values, operations, apps, output, diagnostic, homeDirectory) {
|
|
9
|
+
const { filters, out } = parseProfileOptions(values, false);
|
|
10
|
+
const plan = await buildProfileExport(operations, apps, { ...filters, homeDirectory });
|
|
11
|
+
for (const line of plan.preview)
|
|
12
|
+
diagnostic(line);
|
|
13
|
+
requireProfileExportSelection(plan.profile);
|
|
14
|
+
const content = serializeProfile(plan.profile);
|
|
15
|
+
if (out === undefined)
|
|
16
|
+
output(content.trimEnd());
|
|
17
|
+
else {
|
|
18
|
+
await writeProfileExport(out, plan.profile);
|
|
19
|
+
diagnostic(`Exported profile to ${out}`);
|
|
20
|
+
}
|
|
21
|
+
return 0;
|
|
22
|
+
}
|
|
23
|
+
function parseProfileOptions(values, importing) {
|
|
24
|
+
const sources = [], skills = [], appNames = [];
|
|
25
|
+
const filters = { sources, skills, apps: appNames };
|
|
26
|
+
let out;
|
|
27
|
+
let confirmed = false;
|
|
28
|
+
for (let index = 0; index < values.length; index++) {
|
|
29
|
+
const option = values[index];
|
|
30
|
+
if (option === "--yes" && importing) {
|
|
31
|
+
if (confirmed)
|
|
32
|
+
throw new CliUsageError("Duplicate --yes");
|
|
33
|
+
confirmed = true;
|
|
34
|
+
}
|
|
35
|
+
else if (option === "--no-sources")
|
|
36
|
+
filters.noSources = true;
|
|
37
|
+
else if (option === "--no-skills")
|
|
38
|
+
filters.noSkills = true;
|
|
39
|
+
else if (option === "--no-apps")
|
|
40
|
+
filters.noApps = true;
|
|
41
|
+
else if (["--out", "--source", "--skill", "--app"].includes(option)) {
|
|
42
|
+
const value = values[++index];
|
|
43
|
+
if (!value || value.startsWith("--"))
|
|
44
|
+
throw new CliUsageError(`${option} requires a value`);
|
|
45
|
+
if (option === "--out") {
|
|
46
|
+
if (importing)
|
|
47
|
+
throw new CliUsageError(`Usage: ${IMPORT_USAGE}`);
|
|
48
|
+
if (out !== undefined)
|
|
49
|
+
throw new CliUsageError("Duplicate --out");
|
|
50
|
+
out = value;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const names = option === "--source" ? sources : option === "--skill" ? skills : appNames;
|
|
54
|
+
if (names.includes(value))
|
|
55
|
+
throw new CliUsageError(`Duplicate ${option} ${JSON.stringify(value)}`);
|
|
56
|
+
names.push(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else
|
|
60
|
+
throw new CliUsageError(`Usage: ${importing ? IMPORT_USAGE : EXPORT_USAGE}`);
|
|
61
|
+
}
|
|
62
|
+
return { filters, out, confirmed };
|
|
63
|
+
}
|
|
64
|
+
export async function runProfileImport(values, operations, apps, output, environment = {}) {
|
|
65
|
+
const file = values[0];
|
|
66
|
+
if (!file || file.startsWith("--"))
|
|
67
|
+
throw new CliUsageError(`Usage: ${IMPORT_USAGE}`);
|
|
68
|
+
const { filters, confirmed } = parseProfileOptions(values.slice(1), true);
|
|
69
|
+
const content = await readFile(file, "utf8");
|
|
70
|
+
let input;
|
|
71
|
+
try {
|
|
72
|
+
input = JSON.parse(content);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw new Error(`${file}: invalid JSON (${error instanceof Error ? error.message : String(error)})`);
|
|
76
|
+
}
|
|
77
|
+
const plan = await buildProfileImport(input, operations, apps, filters);
|
|
78
|
+
for (const line of plan.preview)
|
|
79
|
+
output(line);
|
|
80
|
+
if (!confirmed) {
|
|
81
|
+
output("Import not confirmed; rerun with --yes. Nothing was written.");
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
const results = await applyProfileImport(plan, operations, apps, confirmed, output, environment);
|
|
85
|
+
for (const result of results)
|
|
86
|
+
output(`${result.status}: ${result.item.label}${result.detail ? `; ${result.detail}` : ""}`);
|
|
87
|
+
return results.some(result => result.status === "failed") ? 1 : 0;
|
|
88
|
+
}
|