@iseoane/agent-depot 0.2.1 → 0.3.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 +22 -0
- package/README.md +109 -0
- package/dist/src/app-cli.js +172 -0
- package/dist/src/app-flow.js +457 -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 +112 -20
- package/dist/src/process-runner.js +10 -1
- 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/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/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,8 @@ 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";
|
|
6
8
|
import { pathsOverlap } from "./path-safety.js";
|
|
7
9
|
import { skillTreeBaseline } from "./skill-discovery.js";
|
|
8
10
|
import { BuiltInSourceError, createSourceOperations, resolveProjectSource, SourceMetadataError, SourceNotFoundError, } from "./sources.js";
|
|
@@ -28,11 +30,17 @@ const USAGE = [
|
|
|
28
30
|
" 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
31
|
" agent-depot install --scope project --manifest --portable-v1 [--yes] [--confirm-additional-host]",
|
|
30
32
|
" 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>...]",
|
|
33
|
+
" agent-depot update apply --scope <project|user-global> (--all | --skill <id|path>... | --app <name>...) [--yes] [--confirm-path <relative-path>...]",
|
|
34
|
+
" Apps run after Skills; --all --scope project skips Apps.",
|
|
32
35
|
" agent-depot skill remove <id|path>... [--yes]",
|
|
33
36
|
" agent-depot skill remove <id|path> --host <host>... [--yes]",
|
|
34
37
|
" agent-depot skill host add <id|path> --host <host>... [--yes] [--confirm-additional-host]",
|
|
35
38
|
" agent-depot uninstall [--skills] [--unmanaged-skill <exact-global-path>...] [--data] [--cli] [--yes]",
|
|
39
|
+
" agent-depot app schema|list",
|
|
40
|
+
" agent-depot app validate <file>...",
|
|
41
|
+
" agent-depot app approve <name> [--yes]",
|
|
42
|
+
" agent-depot app install|update|uninstall <name> [--yes] [--manual-done]",
|
|
43
|
+
" agent-depot app uninstall <name> --forget [--yes]",
|
|
36
44
|
" agent-depot tui",
|
|
37
45
|
" agent-depot --version",
|
|
38
46
|
].join("\n");
|
|
@@ -60,6 +68,7 @@ const SOURCE_SUBCOMMANDS = new Map([
|
|
|
60
68
|
["migrate", (values, { operations, dependencies, output }) => runSourceMigration(values, operations, dependencies, output)],
|
|
61
69
|
]);
|
|
62
70
|
const COMMANDS = new Map([
|
|
71
|
+
["app", (values, { dependencies, output }) => runAppCommand(values, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output)],
|
|
63
72
|
["install", async (values, { operations, dependencies, output }) => {
|
|
64
73
|
await runInstall(values, operations, dependencies, output);
|
|
65
74
|
return 0;
|
|
@@ -669,21 +678,73 @@ async function runUninstall(argv, operations, dependencies, output) {
|
|
|
669
678
|
}
|
|
670
679
|
async function runUpdate(argv, operations, dependencies, output) {
|
|
671
680
|
const options = parseUpdateOptions(argv);
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
681
|
+
const apps = dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory });
|
|
682
|
+
let appUpdates = [];
|
|
683
|
+
let appCheckFailed = false;
|
|
684
|
+
if (options.scope === "user-global") {
|
|
685
|
+
try {
|
|
686
|
+
appUpdates = await checkAppUpdates(apps);
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
appCheckFailed = true;
|
|
690
|
+
output(`Error (Apps): ${error instanceof Error ? error.message : String(error)}`);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
let loaded;
|
|
694
|
+
try {
|
|
695
|
+
loaded = await loadUpdates({
|
|
696
|
+
scope: options.scope,
|
|
697
|
+
operations,
|
|
698
|
+
...(dependencies.homeDirectory === undefined ? {} : { homeDirectory: dependencies.homeDirectory }),
|
|
699
|
+
...(dependencies.projectRoot === undefined ? {} : { projectRoot: dependencies.projectRoot }),
|
|
700
|
+
...(dependencies.sourceAccess === undefined ? {} : { sourceAccess: dependencies.sourceAccess }),
|
|
701
|
+
...(dependencies.projectManifestStore === undefined ? {} : { projectManifestStore: dependencies.projectManifestStore }),
|
|
702
|
+
...(dependencies.installationOptions?.fileSystem === undefined ? {} : { installationFileSystem: dependencies.installationOptions.fileSystem }),
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
catch (error) {
|
|
706
|
+
output(`Error (Skills, ${options.scope}): ${error instanceof Error ? error.message : String(error)}`);
|
|
707
|
+
}
|
|
708
|
+
if (loaded) {
|
|
709
|
+
const { assessment, installed, context } = loaded;
|
|
710
|
+
const userGlobalInventory = options.action === "check" && options.scope === "user-global"
|
|
711
|
+
? await scanUserGlobalSkillInventory({ homeDirectory: context.projectRoot, managedInstallations: installed, reportWarning: output })
|
|
712
|
+
: undefined;
|
|
713
|
+
outputUpdateAssessment(assessment, options.scope, output, userGlobalInventory);
|
|
714
|
+
}
|
|
715
|
+
for (const item of appUpdates) {
|
|
716
|
+
output(`App ${item.entry.recipe?.name ?? item.entry.file}: ${item.status}; installed ${item.installedVersion ?? "unknown"}; latest ${item.latestVersion ?? "unknown"}${item.reason ? `; ${item.reason}` : ""}`);
|
|
717
|
+
}
|
|
718
|
+
if (!loaded)
|
|
719
|
+
return 1;
|
|
685
720
|
if (options.action === "check") {
|
|
686
|
-
return 0;
|
|
721
|
+
return appCheckFailed ? 1 : 0;
|
|
722
|
+
}
|
|
723
|
+
const { assessment, context } = loaded;
|
|
724
|
+
const selectedApps = options.all ? appUpdates.filter(item => item.status === "update available")
|
|
725
|
+
: options.requestedApps.map(name => {
|
|
726
|
+
const matches = appUpdates.filter(item => item.entry.recipe?.name === name);
|
|
727
|
+
if (matches.length !== 1 || matches[0].status !== "update available") {
|
|
728
|
+
throw new CliUsageError(`App ${JSON.stringify(name)} is unknown or not updateable`);
|
|
729
|
+
}
|
|
730
|
+
return matches[0];
|
|
731
|
+
});
|
|
732
|
+
const appPlans = [];
|
|
733
|
+
let appFailed = appCheckFailed ? 1 : 0;
|
|
734
|
+
for (const item of selectedApps) {
|
|
735
|
+
try {
|
|
736
|
+
const plan = await apps.planLifecycle(item.entry, "update", undefined, item);
|
|
737
|
+
output(`Preview: update App ${item.entry.recipe.name}: ${JSON.stringify(plan.argv ?? [])}; executable ${plan.executable ?? "unresolved"}; cwd ${plan.cwd}; environment ${plan.environment}`);
|
|
738
|
+
if (plan.warning)
|
|
739
|
+
output(plan.warning);
|
|
740
|
+
if (plan.manual)
|
|
741
|
+
output(`Manual (never executed): ${plan.manual}`);
|
|
742
|
+
appPlans.push(plan);
|
|
743
|
+
}
|
|
744
|
+
catch (error) {
|
|
745
|
+
appFailed++;
|
|
746
|
+
output(`App ${item.entry.recipe.name}: preview failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
747
|
+
}
|
|
687
748
|
}
|
|
688
749
|
const selected = selectUpdateCandidates(assessment, options);
|
|
689
750
|
const { plans, failures: previewFailures, nonCanonicalPaths } = await previewUpdateCandidates(selected, context);
|
|
@@ -717,10 +778,32 @@ async function runUpdate(argv, operations, dependencies, output) {
|
|
|
717
778
|
for (const item of result.failed) {
|
|
718
779
|
output(`Failed Skill ${JSON.stringify(item.selection.path)} (${item.id}): ${item.error ?? "unknown error"}`);
|
|
719
780
|
}
|
|
720
|
-
|
|
721
|
-
|
|
781
|
+
let appUpdated = 0;
|
|
782
|
+
for (const plan of appPlans) {
|
|
783
|
+
try {
|
|
784
|
+
const outcome = await apps.executeLifecycle(plan, true);
|
|
785
|
+
output(`App ${plan.entry.recipe.name}: ${describeAppResult(outcome)}${outcome.reason ? `: ${outcome.reason}` : ""}`);
|
|
786
|
+
if (outcome.status === "installed")
|
|
787
|
+
appUpdated++;
|
|
788
|
+
else {
|
|
789
|
+
appFailed++;
|
|
790
|
+
if (outcome.manual) {
|
|
791
|
+
output(`Manual (never executed): ${outcome.manual}`);
|
|
792
|
+
output(describeManualCompletion(plan));
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
appFailed++;
|
|
798
|
+
output(`App ${plan.entry.recipe.name}: failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
output(`Update summary: ${result.updated.length + appUpdated} updated, ${result.failed.length + appFailed} failed`);
|
|
802
|
+
return result.failed.length + appFailed === 0 ? 0 : 1;
|
|
722
803
|
}
|
|
723
804
|
function selectUpdateCandidates(assessment, options) {
|
|
805
|
+
if (!options.all && !options.requested.length)
|
|
806
|
+
return [];
|
|
724
807
|
const requested = options.all
|
|
725
808
|
? "all"
|
|
726
809
|
: resolveUpdateSelectionIds(assessment, options.requested);
|
|
@@ -742,6 +825,7 @@ function parseUpdateOptions(argv) {
|
|
|
742
825
|
let all = false;
|
|
743
826
|
let confirmed = false;
|
|
744
827
|
const requested = [];
|
|
828
|
+
const requestedApps = [];
|
|
745
829
|
const confirmedPaths = [];
|
|
746
830
|
for (let index = 1; index < argv.length; index += 1) {
|
|
747
831
|
switch (argv[index]) {
|
|
@@ -754,6 +838,9 @@ function parseUpdateOptions(argv) {
|
|
|
754
838
|
case "--all":
|
|
755
839
|
all = true;
|
|
756
840
|
break;
|
|
841
|
+
case "--app":
|
|
842
|
+
requestedApps.push(requireOptionValue(argv, ++index, "--app"));
|
|
843
|
+
break;
|
|
757
844
|
case "--skill":
|
|
758
845
|
requested.push(requireOptionValue(argv, ++index, "--skill"));
|
|
759
846
|
break;
|
|
@@ -767,12 +854,16 @@ function parseUpdateOptions(argv) {
|
|
|
767
854
|
if (scope !== "project" && scope !== "user-global") {
|
|
768
855
|
throw new CliUsageError("Update scope must be explicit: use --scope project or --scope user-global");
|
|
769
856
|
}
|
|
770
|
-
if (action === "check" && (all || requested.length > 0 || confirmed || confirmedPaths.length > 0)) {
|
|
857
|
+
if (action === "check" && (all || requested.length > 0 || requestedApps.length > 0 || confirmed || confirmedPaths.length > 0)) {
|
|
771
858
|
throw new CliUsageError("update check only accepts --scope; use update apply to select and apply updates");
|
|
772
859
|
}
|
|
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");
|
|
860
|
+
if (action === "apply" && (all === (requested.length + requestedApps.length > 0))) {
|
|
861
|
+
throw new CliUsageError("update apply requires exactly one selection mode: --all or one or more --skill/--app values");
|
|
775
862
|
}
|
|
863
|
+
if (requestedApps.length && scope !== "user-global")
|
|
864
|
+
throw new CliUsageError("Apps are user-global only");
|
|
865
|
+
if (new Set(requestedApps).size !== requestedApps.length)
|
|
866
|
+
throw new CliUsageError("Duplicate App selection");
|
|
776
867
|
if (scope === "user-global" && confirmedPaths.length > 0) {
|
|
777
868
|
throw new CliUsageError("--confirm-path only applies to --scope project");
|
|
778
869
|
}
|
|
@@ -781,6 +872,7 @@ function parseUpdateOptions(argv) {
|
|
|
781
872
|
scope: scope,
|
|
782
873
|
all,
|
|
783
874
|
requested: Object.freeze(requested),
|
|
875
|
+
requestedApps: Object.freeze(requestedApps),
|
|
784
876
|
confirmed,
|
|
785
877
|
confirmedPaths: Object.freeze(confirmedPaths),
|
|
786
878
|
};
|
|
@@ -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) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { assertSkillNotAppOwned } from "./app-owned-skills.js";
|
|
2
3
|
import { pathsOverlap } from "./path-safety.js";
|
|
3
4
|
import { inspectProjectSkillInstallation, installProjectSkillTransaction, validateProjectSkillInstallationPreview, } from "./project-installation.js";
|
|
4
5
|
import { AGENT_DEPOT_PACKAGE_VERSION, parseProjectManifest, } from "./project-manifest.js";
|
|
@@ -311,6 +312,7 @@ export async function planSingleInstall(request, context) {
|
|
|
311
312
|
throw new CliUsageError(`Skill selection ${JSON.stringify(selection.path)} is already recorded in ${context.recordedIn}; already managed Skills must be changed with update apply`);
|
|
312
313
|
}
|
|
313
314
|
const { resolvedSource, resolved, installationInspection } = await resolveInstallInspection(selection, source, projectSource, context.root, request, context.operations, context.environment, context.sourceAccess);
|
|
315
|
+
await assertInstallNotAppOwned(request, context, installationInspection);
|
|
314
316
|
return { request, context, selection, source, resolvedSource, resolved, inspection: installationInspection };
|
|
315
317
|
}
|
|
316
318
|
/** Prints the preview the user confirms before {@link executeSingleInstall}. */
|
|
@@ -324,6 +326,7 @@ export function outputSingleInstallPreview(plan, output) {
|
|
|
324
326
|
*/
|
|
325
327
|
export async function executeSingleInstall(plan, persist, output) {
|
|
326
328
|
const { request, context, selection, resolvedSource, resolved, inspection } = plan;
|
|
329
|
+
await assertInstallNotAppOwned(request, context, inspection);
|
|
327
330
|
const protectedPaths = assertNoManagedInstallationOverlap(context.existing, selection, inspection, context.root, request.overwrite);
|
|
328
331
|
rejectInstallationCollision(inspection, request.overwrite);
|
|
329
332
|
const transaction = await installOne(selection, resolvedSource, context.sourceAccess, context.root, context.environment, resolved.method, request.portableV1, resolved.files, resolved.resolvedVersion, request.confirmAdditionalHostExposure, request.overwrite, inspection, protectedPaths);
|
|
@@ -360,3 +363,12 @@ export function needsAdditionalHostExposure(inspection, hosts) {
|
|
|
360
363
|
const missingCanonical = hosts.some((host) => host !== "claude") && !identical.includes(inspection.canonicalPath);
|
|
361
364
|
return missingClaude || missingCanonical;
|
|
362
365
|
}
|
|
366
|
+
async function assertInstallNotAppOwned(request, context, inspection) {
|
|
367
|
+
if (request.scope !== "user-global")
|
|
368
|
+
return;
|
|
369
|
+
for (const location of inspection.locations) {
|
|
370
|
+
if (location.status === "missing")
|
|
371
|
+
continue;
|
|
372
|
+
await assertSkillNotAppOwned(path.basename(location.path), { ...context.environment, homeDirectory: context.root });
|
|
373
|
+
}
|
|
374
|
+
}
|
package/dist/src/source-state.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { mkdir, readFile,
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { mkdir, readFile, rmdir, unlink } from "node:fs/promises";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
3
|
import { setTimeout as delay } from "node:timers/promises";
|
|
5
4
|
import path from "node:path";
|
|
5
|
+
import { writeFileAtomically } from "./atomic-file.js";
|
|
6
6
|
import { canonicalizeGitSourceUrl, sourceIdForUrl } from "./git-source.js";
|
|
7
7
|
import { parseProjectManifest } from "./project-manifest.js";
|
|
8
8
|
const EMPTY_STATE = Object.freeze({
|
|
@@ -204,28 +204,7 @@ export class SourceStateStore {
|
|
|
204
204
|
const normalized = parseState(state, this.path);
|
|
205
205
|
await mkdir(path.dirname(this.path), { recursive: true });
|
|
206
206
|
const contents = `${JSON.stringify(normalized, null, 2)}\n`;
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
await writeFile(temporaryPath, contents, { encoding: "utf8", flag: "wx" });
|
|
210
|
-
try {
|
|
211
|
-
await rename(temporaryPath, this.path);
|
|
212
|
-
}
|
|
213
|
-
catch (error) {
|
|
214
|
-
if (!isWindowsReplaceError(error)) {
|
|
215
|
-
throw error;
|
|
216
|
-
}
|
|
217
|
-
// Windows may not replace an existing file with rename. The lock makes
|
|
218
|
-
// this fallback safe from concurrent read-modify-write operations.
|
|
219
|
-
await writeFile(this.path, contents, "utf8");
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
finally {
|
|
223
|
-
await unlink(temporaryPath).catch((error) => {
|
|
224
|
-
if (!isMissingFile(error)) {
|
|
225
|
-
throw error;
|
|
226
|
-
}
|
|
227
|
-
});
|
|
228
|
-
}
|
|
207
|
+
await writeFileAtomically(this.path, contents, { allowLockedReplaceFallback: true });
|
|
229
208
|
}
|
|
230
209
|
async acquireLock() {
|
|
231
210
|
const lockPath = `${this.path}.lock`;
|
|
@@ -259,7 +238,3 @@ function isMissingFile(error) {
|
|
|
259
238
|
function isAlreadyExists(error) {
|
|
260
239
|
return errorCode(error) === "EEXIST";
|
|
261
240
|
}
|
|
262
|
-
function isWindowsReplaceError(error) {
|
|
263
|
-
const code = errorCode(error);
|
|
264
|
-
return code === "EEXIST" || code === "EPERM" || code === "ENOTEMPTY";
|
|
265
|
-
}
|