@iseoane/agent-depot 0.4.1 → 0.5.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 +29 -0
- package/README.es.md +969 -0
- package/README.md +244 -23
- package/dist/src/app-flow.js +52 -51
- package/dist/src/app-recipe-removal.js +63 -0
- package/dist/src/cli.js +122 -11
- package/dist/src/executable-resolution.js +54 -0
- package/dist/src/git-source.js +129 -6
- package/dist/src/package-bundles.js +614 -0
- package/dist/src/package-cli.js +412 -0
- package/dist/src/package-content.js +158 -0
- package/dist/src/package-flow.js +636 -0
- package/dist/src/package-host-plans.js +240 -0
- package/dist/src/package-host-state.js +349 -0
- package/dist/src/package-model.js +33 -0
- package/dist/src/package-owned-skills.js +122 -0
- package/dist/src/package-state.js +404 -0
- package/dist/src/profile-cli.js +15 -9
- package/dist/src/profile-export.js +44 -9
- package/dist/src/profile-import.js +80 -17
- package/dist/src/profile.js +78 -3
- package/dist/src/skill-discovery.js +2 -2
- package/dist/src/skill-install.js +16 -0
- package/dist/src/sources.js +11 -0
- package/dist/src/tui/app-actions.js +1 -1
- package/dist/src/tui/app.js +5 -6
- package/dist/src/tui/catalog-browse.js +10 -3
- package/dist/src/tui/catalog-panel.js +7 -3
- package/dist/src/tui/catalog-state.js +57 -6
- package/dist/src/tui/catalog-tree.js +52 -10
- package/dist/src/tui/catalog-view.js +27 -4
- package/dist/src/tui/environment.js +8 -1
- package/dist/src/tui/installations-browse.js +20 -2
- package/dist/src/tui/installations-rows.js +2 -1
- package/dist/src/tui/installations-tree.js +14 -1
- package/dist/src/tui/installations-view.js +8 -2
- package/dist/src/tui/installations.js +12 -2
- package/dist/src/tui/manage-actions.js +2 -0
- package/dist/src/tui/manage-panel.js +5 -0
- package/dist/src/tui/package-actions.js +165 -0
- package/dist/src/tui/package-rows.js +87 -0
- package/dist/src/tui/profile-view.js +36 -14
- package/dist/src/tui/sources-mode.js +2 -0
- package/dist/src/tui/sources-recipes.js +116 -53
- package/dist/src/tui/sources-view.js +57 -19
- package/dist/src/tui/tree.js +3 -4
- package/dist/src/tui/updates-flow.js +4 -1
- package/dist/src/tui/updates-panel.js +2 -2
- package/dist/src/tui/updates.js +72 -4
- package/dist/src/user-global-skill-inventory.js +7 -2
- package/package.json +2 -1
- package/skills/agent-depot-apprecipe/SKILL.md +29 -5
package/dist/src/cli.js
CHANGED
|
@@ -5,6 +5,9 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { checkAppUpdates, createAppOperations } from "./app-flow.js";
|
|
7
7
|
import { describeAppResult, describeManualCompletion, runAppCommand } from "./app-cli.js";
|
|
8
|
+
import { createPackageOperations } from "./package-flow.js";
|
|
9
|
+
import { describeLifecyclePlan, describePackageResult, describePackageUpdateCheck, formatPackageSelection, packageSelectionMatches, runPackageCommand } from "./package-cli.js";
|
|
10
|
+
import { selectionKey } from "./package-model.js";
|
|
8
11
|
import { EXPORT_USAGE, IMPORT_USAGE, runProfileExport, runProfileImport } from "./profile-cli.js";
|
|
9
12
|
import { pathsOverlap } from "./path-safety.js";
|
|
10
13
|
import { skillTreeBaseline } from "./skill-discovery.js";
|
|
@@ -30,9 +33,9 @@ const USAGE = [
|
|
|
30
33
|
" agent-depot discover <source-id> [source-id...]",
|
|
31
34
|
" 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]",
|
|
32
35
|
" agent-depot install --scope project --manifest --portable-v1 [--yes] [--confirm-additional-host]",
|
|
33
|
-
" agent-depot update check --scope <project|user-global>",
|
|
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.",
|
|
36
|
+
" agent-depot update check --scope <project|user-global> [--package <selection>...]",
|
|
37
|
+
" agent-depot update apply --scope <project|user-global> (--all | --skill <id|path>... | --app <name>... | --package <selection>...) [--yes] [--confirm-path <relative-path>...]",
|
|
38
|
+
" Apps and Packages run after Skills; --all --scope project skips Apps and Packages.",
|
|
36
39
|
" agent-depot skill remove <id|path>... [--yes]",
|
|
37
40
|
" agent-depot skill remove <id|path> --host <host>... [--yes]",
|
|
38
41
|
" agent-depot skill host add <id|path> --host <host>... [--yes] [--confirm-additional-host]",
|
|
@@ -42,6 +45,12 @@ const USAGE = [
|
|
|
42
45
|
" agent-depot app approve <name> [--yes]",
|
|
43
46
|
" agent-depot app install|update|uninstall <name> [--yes] [--manual-done]",
|
|
44
47
|
" agent-depot app uninstall <name> --forget [--yes]",
|
|
48
|
+
" agent-depot package list",
|
|
49
|
+
" agent-depot package discover <source-id>",
|
|
50
|
+
" agent-depot package inspect <selection> --host <host>",
|
|
51
|
+
" agent-depot package approve <selection> --host <host> [--action <install|update|uninstall>]",
|
|
52
|
+
" agent-depot package install|update|uninstall <selection> --host <host> [--yes]",
|
|
53
|
+
" agent-depot package forget <selection> --host <host> --yes",
|
|
45
54
|
` ${EXPORT_USAGE}`,
|
|
46
55
|
` ${IMPORT_USAGE}`,
|
|
47
56
|
" agent-depot tui",
|
|
@@ -70,10 +79,18 @@ const SOURCE_SUBCOMMANDS = new Map([
|
|
|
70
79
|
["remove", (values, { operations, dependencies, output }) => runSourceRemoval(values, operations, dependencies, output)],
|
|
71
80
|
["migrate", (values, { operations, dependencies, output }) => runSourceMigration(values, operations, dependencies, output)],
|
|
72
81
|
]);
|
|
82
|
+
function packageOperationsFor(dependencies, operations) {
|
|
83
|
+
return dependencies.packageOperations ?? createPackageOperations({
|
|
84
|
+
...(dependencies.homeDirectory === undefined ? {} : { homeDirectory: dependencies.homeDirectory }),
|
|
85
|
+
sourceOperations: operations,
|
|
86
|
+
...(dependencies.sourceContentAccess === undefined ? {} : { sourceContentAccess: dependencies.sourceContentAccess }),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
73
89
|
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)],
|
|
90
|
+
["import", (values, { operations, dependencies, output }) => runProfileImport(values, operations, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output, { ...dependencies, packageOperations: packageOperationsFor(dependencies, operations) })],
|
|
91
|
+
["export", (values, { operations, dependencies, output }) => runProfileExport(values, operations, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output, dependencies.stderr ?? (line => console.error(line)), dependencies.homeDirectory, packageOperationsFor(dependencies, operations))],
|
|
76
92
|
["app", (values, { dependencies, output }) => runAppCommand(values, dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory }), output)],
|
|
93
|
+
["package", (values, { operations, dependencies, output }) => runPackageCommand(values, packageOperationsFor(dependencies, operations), operations, output)],
|
|
77
94
|
["install", async (values, { operations, dependencies, output }) => {
|
|
78
95
|
await runInstall(values, operations, dependencies, output);
|
|
79
96
|
return 0;
|
|
@@ -684,8 +701,14 @@ async function runUninstall(argv, operations, dependencies, output) {
|
|
|
684
701
|
async function runUpdate(argv, operations, dependencies, output) {
|
|
685
702
|
const options = parseUpdateOptions(argv);
|
|
686
703
|
const apps = dependencies.appOperations ?? createAppOperations({ homeDirectory: dependencies.homeDirectory });
|
|
704
|
+
const packages = packageOperationsFor(dependencies, operations);
|
|
705
|
+
const requestedPackages = options.scope === "user-global" && options.requestedPackages.length > 0
|
|
706
|
+
? await resolvePackageUpdateSelections(packages, options.requestedPackages)
|
|
707
|
+
: [];
|
|
687
708
|
let appUpdates = [];
|
|
688
709
|
let appCheckFailed = false;
|
|
710
|
+
let packageUpdates = [];
|
|
711
|
+
let packageCheckFailed = false;
|
|
689
712
|
if (options.scope === "user-global") {
|
|
690
713
|
try {
|
|
691
714
|
appUpdates = await checkAppUpdates(apps);
|
|
@@ -694,6 +717,13 @@ async function runUpdate(argv, operations, dependencies, output) {
|
|
|
694
717
|
appCheckFailed = true;
|
|
695
718
|
output(`Error (Apps): ${error instanceof Error ? error.message : String(error)}`);
|
|
696
719
|
}
|
|
720
|
+
try {
|
|
721
|
+
packageUpdates = await packages.checkUpdates(requestedPackages.length === 0 ? undefined : requestedPackages);
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
packageCheckFailed = true;
|
|
725
|
+
output(`Error (Packages): ${error instanceof Error ? error.message : String(error)}`);
|
|
726
|
+
}
|
|
697
727
|
}
|
|
698
728
|
let loaded;
|
|
699
729
|
try {
|
|
@@ -720,10 +750,13 @@ async function runUpdate(argv, operations, dependencies, output) {
|
|
|
720
750
|
for (const item of appUpdates) {
|
|
721
751
|
output(`App ${item.entry.recipe?.name ?? item.entry.file}: ${item.status}; installed ${item.installedVersion ?? "unknown"}; latest ${item.latestVersion ?? "unknown"}${item.reason ? `; ${item.reason}` : ""}`);
|
|
722
752
|
}
|
|
753
|
+
for (const item of packageUpdates) {
|
|
754
|
+
output(describePackageUpdateCheck(item));
|
|
755
|
+
}
|
|
723
756
|
if (!loaded)
|
|
724
757
|
return 1;
|
|
725
758
|
if (options.action === "check") {
|
|
726
|
-
return appCheckFailed ? 1 : 0;
|
|
759
|
+
return appCheckFailed || packageCheckFailed ? 1 : 0;
|
|
727
760
|
}
|
|
728
761
|
const { assessment, context } = loaded;
|
|
729
762
|
const selectedApps = options.all ? appUpdates.filter(item => item.status === "update available")
|
|
@@ -734,8 +767,13 @@ async function runUpdate(argv, operations, dependencies, output) {
|
|
|
734
767
|
}
|
|
735
768
|
return matches[0];
|
|
736
769
|
});
|
|
770
|
+
const selectedPackages = options.all
|
|
771
|
+
? packageUpdates.filter(item => item.status === "update available")
|
|
772
|
+
: selectPackageUpdateChecks(packageUpdates, requestedPackages);
|
|
737
773
|
const appPlans = [];
|
|
774
|
+
const packagePlans = [];
|
|
738
775
|
let appFailed = appCheckFailed ? 1 : 0;
|
|
776
|
+
let packageFailed = packageCheckFailed ? 1 : 0;
|
|
739
777
|
for (const item of selectedApps) {
|
|
740
778
|
try {
|
|
741
779
|
const plan = await apps.planLifecycle(item.entry, "update", undefined, item);
|
|
@@ -751,6 +789,24 @@ async function runUpdate(argv, operations, dependencies, output) {
|
|
|
751
789
|
output(`App ${item.entry.recipe.name}: preview failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
752
790
|
}
|
|
753
791
|
}
|
|
792
|
+
for (const item of selectedPackages) {
|
|
793
|
+
try {
|
|
794
|
+
const plan = await packages.planLifecycle(item.installed.selection, "update");
|
|
795
|
+
const approval = await packages.approvalStatus(item.installed.selection, "update");
|
|
796
|
+
describeLifecyclePlan(plan, approval, output);
|
|
797
|
+
if (approval === "approved") {
|
|
798
|
+
packagePlans.push(plan);
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
packageFailed++;
|
|
802
|
+
output(`Package ${formatPackageSelection(item.installed.selection)}: needs approval for update`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
catch (error) {
|
|
806
|
+
packageFailed++;
|
|
807
|
+
output(`Package ${formatPackageSelection(item.installed.selection)}: preview failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
754
810
|
const selected = selectUpdateCandidates(assessment, options);
|
|
755
811
|
const { plans, failures: previewFailures, nonCanonicalPaths } = await previewUpdateCandidates(selected, context);
|
|
756
812
|
// Successes and failures are reported in candidate order, as the preview runs.
|
|
@@ -803,8 +859,25 @@ async function runUpdate(argv, operations, dependencies, output) {
|
|
|
803
859
|
output(`App ${plan.entry.recipe.name}: failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
804
860
|
}
|
|
805
861
|
}
|
|
806
|
-
|
|
807
|
-
|
|
862
|
+
let packageUpdated = 0;
|
|
863
|
+
for (const plan of packagePlans) {
|
|
864
|
+
try {
|
|
865
|
+
const outcome = await packages.executeLifecycle(plan, true);
|
|
866
|
+
output(`Package ${formatPackageSelection(plan.selection)}: ${describePackageResult(outcome)}`);
|
|
867
|
+
if (outcome.status === "updated") {
|
|
868
|
+
packageUpdated++;
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
packageFailed++;
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
catch (error) {
|
|
875
|
+
packageFailed++;
|
|
876
|
+
output(`Package ${formatPackageSelection(plan.selection)}: failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
output(`Update summary: ${result.updated.length + appUpdated + packageUpdated} updated, ${result.failed.length + appFailed + packageFailed} failed`);
|
|
880
|
+
return result.failed.length + appFailed + packageFailed === 0 ? 0 : 1;
|
|
808
881
|
}
|
|
809
882
|
function selectUpdateCandidates(assessment, options) {
|
|
810
883
|
if (!options.all && !options.requested.length)
|
|
@@ -821,6 +894,35 @@ function selectUpdateCandidates(assessment, options) {
|
|
|
821
894
|
throw error;
|
|
822
895
|
}
|
|
823
896
|
}
|
|
897
|
+
async function resolvePackageUpdateSelections(packages, requested) {
|
|
898
|
+
const records = await packages.list();
|
|
899
|
+
return Object.freeze(requested.map((value) => {
|
|
900
|
+
const matches = records.filter((record) => packageSelectionMatches(record.selection, value));
|
|
901
|
+
if (matches.length !== 1) {
|
|
902
|
+
throw new CliUsageError(matches.length === 0
|
|
903
|
+
? `Unknown Package selection ${JSON.stringify(value)}`
|
|
904
|
+
: `Package selection ${JSON.stringify(value)} is ambiguous`);
|
|
905
|
+
}
|
|
906
|
+
return matches[0].selection;
|
|
907
|
+
}));
|
|
908
|
+
}
|
|
909
|
+
function selectPackageUpdateChecks(checks, selections) {
|
|
910
|
+
return Object.freeze(selections.map((selection) => {
|
|
911
|
+
const label = formatPackageSelection(selection);
|
|
912
|
+
const matches = checks.filter((check) => selectionKey(check.installed.selection) === selectionKey(selection));
|
|
913
|
+
if (matches.length !== 1) {
|
|
914
|
+
throw new CliUsageError(`Package ${JSON.stringify(label)} is unknown or not updateable`);
|
|
915
|
+
}
|
|
916
|
+
const match = matches[0];
|
|
917
|
+
if (match.status === "unknown") {
|
|
918
|
+
throw new CliUsageError(`Package ${JSON.stringify(label)} has unknown version status and is not updateable`);
|
|
919
|
+
}
|
|
920
|
+
if (match.status === "current") {
|
|
921
|
+
throw new CliUsageError(`Package ${JSON.stringify(label)} has no available update`);
|
|
922
|
+
}
|
|
923
|
+
return match;
|
|
924
|
+
}));
|
|
925
|
+
}
|
|
824
926
|
function parseUpdateOptions(argv) {
|
|
825
927
|
const action = argv[0];
|
|
826
928
|
if (action !== "check" && action !== "apply") {
|
|
@@ -831,6 +933,7 @@ function parseUpdateOptions(argv) {
|
|
|
831
933
|
let confirmed = false;
|
|
832
934
|
const requested = [];
|
|
833
935
|
const requestedApps = [];
|
|
936
|
+
const requestedPackages = [];
|
|
834
937
|
const confirmedPaths = [];
|
|
835
938
|
for (let index = 1; index < argv.length; index += 1) {
|
|
836
939
|
switch (argv[index]) {
|
|
@@ -846,6 +949,9 @@ function parseUpdateOptions(argv) {
|
|
|
846
949
|
case "--app":
|
|
847
950
|
requestedApps.push(requireOptionValue(argv, ++index, "--app"));
|
|
848
951
|
break;
|
|
952
|
+
case "--package":
|
|
953
|
+
requestedPackages.push(requireOptionValue(argv, ++index, "--package"));
|
|
954
|
+
break;
|
|
849
955
|
case "--skill":
|
|
850
956
|
requested.push(requireOptionValue(argv, ++index, "--skill"));
|
|
851
957
|
break;
|
|
@@ -860,15 +966,19 @@ function parseUpdateOptions(argv) {
|
|
|
860
966
|
throw new CliUsageError("Update scope must be explicit: use --scope project or --scope user-global");
|
|
861
967
|
}
|
|
862
968
|
if (action === "check" && (all || requested.length > 0 || requestedApps.length > 0 || confirmed || confirmedPaths.length > 0)) {
|
|
863
|
-
throw new CliUsageError("update check only accepts --scope; use update apply to select and apply updates");
|
|
969
|
+
throw new CliUsageError("update check only accepts --scope and optional --package filters; use update apply to select and apply updates");
|
|
864
970
|
}
|
|
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");
|
|
971
|
+
if (action === "apply" && (all === (requested.length + requestedApps.length + requestedPackages.length > 0))) {
|
|
972
|
+
throw new CliUsageError("update apply requires exactly one selection mode: --all or one or more --skill/--app/--package values");
|
|
867
973
|
}
|
|
868
974
|
if (requestedApps.length && scope !== "user-global")
|
|
869
975
|
throw new CliUsageError("Apps are user-global only");
|
|
976
|
+
if (requestedPackages.length && scope !== "user-global")
|
|
977
|
+
throw new CliUsageError("Packages are user-global only");
|
|
870
978
|
if (new Set(requestedApps).size !== requestedApps.length)
|
|
871
979
|
throw new CliUsageError("Duplicate App selection");
|
|
980
|
+
if (new Set(requestedPackages).size !== requestedPackages.length)
|
|
981
|
+
throw new CliUsageError("Duplicate Package selection");
|
|
872
982
|
if (scope === "user-global" && confirmedPaths.length > 0) {
|
|
873
983
|
throw new CliUsageError("--confirm-path only applies to --scope project");
|
|
874
984
|
}
|
|
@@ -878,6 +988,7 @@ function parseUpdateOptions(argv) {
|
|
|
878
988
|
all,
|
|
879
989
|
requested: Object.freeze(requested),
|
|
880
990
|
requestedApps: Object.freeze(requestedApps),
|
|
991
|
+
requestedPackages: Object.freeze(requestedPackages),
|
|
881
992
|
confirmed,
|
|
882
993
|
confirmedPaths: Object.freeze(confirmedPaths),
|
|
883
994
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
/** A POSIX path on a mounted Windows drive, which WSL cannot execute. */
|
|
5
|
+
export function isWindowsExecutableOnWsl(executable, environment) {
|
|
6
|
+
const relative = path.posix.relative(environment.wslMountRoot, executable);
|
|
7
|
+
return environment.isWsl && /^[a-z]\//iu.test(relative);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Resolves one host executable without a shell. The injected resolver is the
|
|
11
|
+
* test and embedding seam; otherwise the name is looked up on PATH, and a
|
|
12
|
+
* Windows executable reachable from WSL is reported as blocked rather than run.
|
|
13
|
+
*/
|
|
14
|
+
export function createPortableExecutableResolver(options = {}) {
|
|
15
|
+
const platform = options.platform ?? process.platform;
|
|
16
|
+
const injected = options.resolveExecutable;
|
|
17
|
+
const wslEnvironment = {
|
|
18
|
+
isWsl: options.isWsl ?? (platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)),
|
|
19
|
+
wslMountRoot: options.wslMountRoot ?? "/mnt",
|
|
20
|
+
};
|
|
21
|
+
return async function resolve(name) {
|
|
22
|
+
if (injected) {
|
|
23
|
+
const executable = await injected(name);
|
|
24
|
+
const target = executable ? await realpath(executable).catch(() => executable) : undefined;
|
|
25
|
+
return { executable, blocked: target !== undefined && isWindowsExecutableOnWsl(target, wslEnvironment) };
|
|
26
|
+
}
|
|
27
|
+
let blockedExecutable;
|
|
28
|
+
const suffixes = platform === "win32"
|
|
29
|
+
? ["", ...(process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")]
|
|
30
|
+
: [""];
|
|
31
|
+
for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
32
|
+
if (!path.isAbsolute(directory)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
for (const suffix of suffixes) {
|
|
36
|
+
const candidate = path.join(directory, name + suffix);
|
|
37
|
+
try {
|
|
38
|
+
await access(candidate, platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
39
|
+
const target = await realpath(candidate);
|
|
40
|
+
if (isWindowsExecutableOnWsl(target, wslEnvironment)) {
|
|
41
|
+
blockedExecutable ??= candidate;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// Preserve shim/multicall identity when spawning; realpath is only a safety check.
|
|
45
|
+
return { executable: candidate, blocked: false };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { executable: blockedExecutable, blocked: blockedExecutable !== undefined };
|
|
53
|
+
};
|
|
54
|
+
}
|
package/dist/src/git-source.js
CHANGED
|
@@ -18,6 +18,24 @@ const EXCLUDED_LIFECYCLE_SEGMENTS = new Set([
|
|
|
18
18
|
"deprecated",
|
|
19
19
|
"retired",
|
|
20
20
|
]);
|
|
21
|
+
/**
|
|
22
|
+
* One file a Source snapshot carries: a Skill manifest, or a bundle manifest the
|
|
23
|
+
* Package discovery pass reads. The Pi root `package.json` and the two Claude
|
|
24
|
+
* `.claude-plugin` manifests sit beside the Skill files in the same immutable
|
|
25
|
+
* Source view, so one read feeds both passes.
|
|
26
|
+
*/
|
|
27
|
+
export function isSourceSnapshotFile(relativePath, scopeDirectory) {
|
|
28
|
+
if (path.posix.basename(relativePath) === "SKILL.md") {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
const segments = relativePath.split("/");
|
|
32
|
+
if (segments.at(-2) === ".claude-plugin") {
|
|
33
|
+
const name = segments.at(-1);
|
|
34
|
+
return name === "plugin.json" || name === "marketplace.json";
|
|
35
|
+
}
|
|
36
|
+
const root = scopeDirectory === undefined || scopeDirectory === "" ? "package.json" : `${scopeDirectory}/package.json`;
|
|
37
|
+
return relativePath === root;
|
|
38
|
+
}
|
|
21
39
|
export class GitSourceAccessError extends Error {
|
|
22
40
|
constructor(source, reason) {
|
|
23
41
|
super(`Unable to refresh Git Source ${JSON.stringify(source.id)} from ${JSON.stringify(source.url)}: ${reason}`);
|
|
@@ -111,14 +129,14 @@ export class GitSourceAccessAdapter {
|
|
|
111
129
|
"fetch",
|
|
112
130
|
"--prune",
|
|
113
131
|
"--force",
|
|
114
|
-
source.url,
|
|
132
|
+
gitSourceLocation(source.url).repositoryUrl,
|
|
115
133
|
"+refs/*:refs/*",
|
|
116
134
|
]);
|
|
117
135
|
return;
|
|
118
136
|
}
|
|
119
137
|
const temporaryDestination = `${destination}.${process.pid}.${randomUUID()}.tmp`;
|
|
120
138
|
try {
|
|
121
|
-
await this.runner.run("git", ["clone", "--mirror", source.url, temporaryDestination]);
|
|
139
|
+
await this.runner.run("git", ["clone", "--mirror", gitSourceLocation(source.url).repositoryUrl, temporaryDestination]);
|
|
122
140
|
await assertNoSymlinkPath(temporaryDestination, "cache path");
|
|
123
141
|
if ((await pathType(temporaryDestination)) !== "directory") {
|
|
124
142
|
throw new Error("Git clone did not create a directory mirror");
|
|
@@ -170,6 +188,10 @@ export class GitSourceSnapshotAccess {
|
|
|
170
188
|
/** Reads Skill bytes and the commit they came from as one immutable snapshot. */
|
|
171
189
|
async readSkillTreeSnapshot(source, skillPath) {
|
|
172
190
|
const relativeSkillPath = validateSkillDirectoryPath(skillPath);
|
|
191
|
+
const directory = gitSourceLocation(source.url).directory;
|
|
192
|
+
if (directory !== undefined && !relativeSkillPath.startsWith(`${directory}/`) && relativeSkillPath !== directory) {
|
|
193
|
+
throw new Error("Selected Skill path is outside the Git Source directory");
|
|
194
|
+
}
|
|
173
195
|
const destination = await this.requireMirror(source);
|
|
174
196
|
const commit = await this.readResolvedCommit(source);
|
|
175
197
|
const tree = await this.runner.run("git", [
|
|
@@ -219,7 +241,7 @@ export class GitSourceSnapshotAccess {
|
|
|
219
241
|
/** Resolves the selected ref to an immutable commit without refreshing the mirror. */
|
|
220
242
|
async readResolvedCommit(source) {
|
|
221
243
|
const destination = await this.requireMirror(source);
|
|
222
|
-
const revision = source.ref
|
|
244
|
+
const revision = validateGitRef(source.ref ?? gitSourceLocation(source.url).ref ?? "HEAD");
|
|
223
245
|
const commit = (await this.runner.run("git", [
|
|
224
246
|
"--git-dir",
|
|
225
247
|
destination,
|
|
@@ -235,7 +257,11 @@ export class GitSourceSnapshotAccess {
|
|
|
235
257
|
async readSnapshot(source) {
|
|
236
258
|
const destination = await this.requireMirror(source);
|
|
237
259
|
const commit = await this.readResolvedCommit(source);
|
|
238
|
-
const
|
|
260
|
+
const directory = gitSourceLocation(source.url).directory;
|
|
261
|
+
const tree = await this.runner.run("git", [
|
|
262
|
+
"--git-dir", destination, "ls-tree", "-r", "-z", commit,
|
|
263
|
+
...(directory === undefined ? [] : ["--", directory]),
|
|
264
|
+
]);
|
|
239
265
|
const files = [];
|
|
240
266
|
for (const record of tree.split("\0")) {
|
|
241
267
|
if (!record) {
|
|
@@ -250,7 +276,8 @@ export class GitSourceSnapshotAccess {
|
|
|
250
276
|
if (metadata[0] === "120000"
|
|
251
277
|
|| metadata[1] !== "blob"
|
|
252
278
|
|| !relativePath
|
|
253
|
-
||
|
|
279
|
+
|| (directory !== undefined && !relativePath.startsWith(`${directory}/`))
|
|
280
|
+
|| !isSourceSnapshotFile(relativePath, directory)
|
|
254
281
|
|| relativePath.split("/").slice(0, -1).some((segment) => EXCLUDED_LIFECYCLE_SEGMENTS.has(segment))) {
|
|
255
282
|
continue;
|
|
256
283
|
}
|
|
@@ -261,6 +288,76 @@ export class GitSourceSnapshotAccess {
|
|
|
261
288
|
}
|
|
262
289
|
return Object.freeze(files);
|
|
263
290
|
}
|
|
291
|
+
/** File paths at or under `root`, from the same immutable commit as the other reads. */
|
|
292
|
+
async listFiles(source, root) {
|
|
293
|
+
const relativeRoot = root === "" ? "" : validateSkillDirectoryPath(root);
|
|
294
|
+
const destination = await this.requireMirror(source);
|
|
295
|
+
const commit = await this.readResolvedCommit(source);
|
|
296
|
+
const tree = await this.runner.run("git", [
|
|
297
|
+
"--git-dir", destination, "ls-tree", "-r", "-z", commit,
|
|
298
|
+
...(relativeRoot === "" ? [] : ["--", relativeRoot]),
|
|
299
|
+
], { maxOutputBytes: SKILL_TREE_LIMITS.maxListingBytes });
|
|
300
|
+
if (Buffer.byteLength(tree, "utf8") > SKILL_TREE_LIMITS.maxListingBytes) {
|
|
301
|
+
throw new Error("Source file listing exceeds the safe size limit");
|
|
302
|
+
}
|
|
303
|
+
const files = [];
|
|
304
|
+
for (const record of tree.split("\0")) {
|
|
305
|
+
if (!record) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const separator = record.indexOf("\t");
|
|
309
|
+
if (separator < 0) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const metadata = record.slice(0, separator).split(" ");
|
|
313
|
+
const relativePath = record.slice(separator + 1);
|
|
314
|
+
if (metadata[0] === "120000"
|
|
315
|
+
|| metadata[1] !== "blob"
|
|
316
|
+
|| !relativePath
|
|
317
|
+
|| (relativeRoot !== "" && !relativePath.startsWith(`${relativeRoot}/`))) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
files.push(relativePath);
|
|
321
|
+
}
|
|
322
|
+
return Object.freeze(files);
|
|
323
|
+
}
|
|
324
|
+
/** One file's text at `path`, or undefined when the commit has no such blob. */
|
|
325
|
+
async readFile(source, path) {
|
|
326
|
+
const safePath = validateSkillDirectoryPath(path);
|
|
327
|
+
const destination = await this.requireMirror(source);
|
|
328
|
+
const commit = await this.readResolvedCommit(source);
|
|
329
|
+
const tree = await this.runner.run("git", [
|
|
330
|
+
"--git-dir", destination, "ls-tree", "-z", commit, "--", safePath,
|
|
331
|
+
], { maxOutputBytes: SKILL_TREE_LIMITS.maxListingBytes });
|
|
332
|
+
for (const record of tree.split("\0")) {
|
|
333
|
+
if (!record) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
const separator = record.indexOf("\t");
|
|
337
|
+
if (separator < 0) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const [mode, objectType, objectId] = record.slice(0, separator).split(" ");
|
|
341
|
+
const relativePath = record.slice(separator + 1);
|
|
342
|
+
if (relativePath !== safePath) {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (objectType !== "blob" || (mode !== "100644" && mode !== "100755")) {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
if (!objectId || !/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(objectId)) {
|
|
349
|
+
throw new Error("Git Source returned an unsafe object reference");
|
|
350
|
+
}
|
|
351
|
+
const content = await this.runner.run("git", [
|
|
352
|
+
"--git-dir", destination, "cat-file", "blob", objectId,
|
|
353
|
+
], { maxOutputBytes: SKILL_TREE_LIMITS.maxFileBytes });
|
|
354
|
+
if (Buffer.byteLength(content, "utf8") > SKILL_TREE_LIMITS.maxFileBytes) {
|
|
355
|
+
throw new Error(`Source file exceeds the safe size limit: ${safePath}`);
|
|
356
|
+
}
|
|
357
|
+
return content;
|
|
358
|
+
}
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
264
361
|
}
|
|
265
362
|
export function defaultGitSourceCachePath(environment = process.env, platform = process.platform, homeDirectory = homedir()) {
|
|
266
363
|
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
@@ -400,7 +497,33 @@ export function canonicalizeGitSourceUrl(input) {
|
|
|
400
497
|
if (parsed.pathname === "/" || parsed.pathname.length < 2) {
|
|
401
498
|
throw new Error("Git Source URL must identify a repository path");
|
|
402
499
|
}
|
|
403
|
-
|
|
500
|
+
const canonicalUrl = parsed.toString();
|
|
501
|
+
gitSourceLocation(canonicalUrl);
|
|
502
|
+
return canonicalUrl;
|
|
503
|
+
}
|
|
504
|
+
/** The GitHub tree/<ref>/<directory> shape, parsed once so the Catalog label cannot describe a different scope than the clone and tree walk use. */
|
|
505
|
+
export function githubTreeLocation(url) {
|
|
506
|
+
const parsed = new URL(url);
|
|
507
|
+
const segments = parsed.pathname.split("/").slice(1);
|
|
508
|
+
if (parsed.hostname !== "github.com" || !["https:", "http:"].includes(parsed.protocol) || segments[2] !== "tree") {
|
|
509
|
+
return undefined;
|
|
510
|
+
}
|
|
511
|
+
if (segments.length < 4 || !segments[0] || !segments[1] || !segments[3]) {
|
|
512
|
+
throw new Error("GitHub directory Source URL must include an owner, repository, and ref");
|
|
513
|
+
}
|
|
514
|
+
const ref = validateGitRef(decodeURIComponent(segments[3]));
|
|
515
|
+
if (ref.startsWith("-"))
|
|
516
|
+
throw new Error("GitHub directory Source ref must not start with a dash");
|
|
517
|
+
const directory = segments.length > 4
|
|
518
|
+
? validateSkillDirectoryPath(segments.slice(4).map((segment) => decodeURIComponent(segment)).join("/"))
|
|
519
|
+
: undefined;
|
|
520
|
+
const repository = `${segments[0]}/${segments[1]}`;
|
|
521
|
+
parsed.pathname = `/${repository}`;
|
|
522
|
+
return { repository, repositoryUrl: parsed.toString(), ref, ...(directory === undefined ? {} : { directory }) };
|
|
523
|
+
}
|
|
524
|
+
/** Keep the registered URL (and identity) while deriving Git transport and discovery scope. */
|
|
525
|
+
function gitSourceLocation(url) {
|
|
526
|
+
return githubTreeLocation(url) ?? { repositoryUrl: url };
|
|
404
527
|
}
|
|
405
528
|
export function sourceIdForUrl(url) {
|
|
406
529
|
const canonicalUrl = canonicalizeGitSourceUrl(url);
|