@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +186 -1
  3. package/dist/src/app-cli.js +172 -0
  4. package/dist/src/app-flow.js +464 -0
  5. package/dist/src/app-latest.js +63 -0
  6. package/dist/src/app-owned-skills.js +42 -0
  7. package/dist/src/app-recipes.js +116 -0
  8. package/dist/src/app-schema.js +45 -0
  9. package/dist/src/app-version.js +46 -0
  10. package/dist/src/atomic-file.js +26 -0
  11. package/dist/src/cli.js +117 -20
  12. package/dist/src/process-runner.js +10 -1
  13. package/dist/src/profile-cli.js +88 -0
  14. package/dist/src/profile-export.js +135 -0
  15. package/dist/src/profile-import.js +218 -0
  16. package/dist/src/profile.js +130 -0
  17. package/dist/src/project-manifest.js +6 -0
  18. package/dist/src/skill-install.js +12 -0
  19. package/dist/src/source-state.js +3 -28
  20. package/dist/src/tui/app-actions.js +157 -0
  21. package/dist/src/tui/app.js +10 -4
  22. package/dist/src/tui/catalog-installs.js +5 -1
  23. package/dist/src/tui/installations-browse.js +24 -2
  24. package/dist/src/tui/installations-tree.js +13 -1
  25. package/dist/src/tui/installations-view.js +46 -6
  26. package/dist/src/tui/installations.js +26 -3
  27. package/dist/src/tui/manage-actions.js +2 -0
  28. package/dist/src/tui/manage-panel.js +6 -0
  29. package/dist/src/tui/profile-view.js +249 -0
  30. package/dist/src/tui/sources-recipes.js +216 -0
  31. package/dist/src/tui/sources-view.js +16 -7
  32. package/dist/src/tui/unmanaged-actions.js +2 -2
  33. package/dist/src/tui/updates-flow.js +18 -3
  34. package/dist/src/tui/updates-keys.js +9 -0
  35. package/dist/src/tui/updates-model.js +3 -3
  36. package/dist/src/tui/updates-panel.js +4 -2
  37. package/dist/src/tui/updates-view.js +1 -1
  38. package/dist/src/tui/updates.js +76 -5
  39. package/dist/src/unmanaged-removal.js +3 -0
  40. package/dist/src/user-global-skill-inventory.js +7 -1
  41. package/package.json +4 -4
  42. package/skills/agent-depot-apprecipe/SKILL.md +44 -0
  43. 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 { assessment, installed, context } = await loadUpdates({
673
- scope: options.scope,
674
- operations,
675
- ...(dependencies.homeDirectory === undefined ? {} : { homeDirectory: dependencies.homeDirectory }),
676
- ...(dependencies.projectRoot === undefined ? {} : { projectRoot: dependencies.projectRoot }),
677
- ...(dependencies.sourceAccess === undefined ? {} : { sourceAccess: dependencies.sourceAccess }),
678
- ...(dependencies.projectManifestStore === undefined ? {} : { projectManifestStore: dependencies.projectManifestStore }),
679
- ...(dependencies.installationOptions?.fileSystem === undefined ? {} : { installationFileSystem: dependencies.installationOptions.fileSystem }),
680
- });
681
- const userGlobalInventory = options.action === "check" && options.scope === "user-global"
682
- ? await scanUserGlobalSkillInventory({ homeDirectory: context.projectRoot, managedInstallations: installed })
683
- : undefined;
684
- outputUpdateAssessment(assessment, options.scope, output, userGlobalInventory);
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
- output(`Update summary: ${result.updated.length} updated, ${result.failed.length} failed`);
721
- return result.failed.length === 0 ? 0 : 1;
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
- stderr += chunk;
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
+ }