@rbbtsn0w/adg 0.1.0-alpha.1 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/adg.js +703 -0
- package/dist/src/adapters/anthropic.js +54 -0
- package/dist/src/adapters/index.js +10 -0
- package/dist/src/adapters/openai.js +30 -0
- package/dist/src/adapters/reverse.js +53 -0
- package/dist/src/agents/claude.js +118 -0
- package/dist/src/agents/codex.js +61 -0
- package/{src/agents/index.ts → dist/src/agents/index.js} +6 -8
- package/dist/src/agents/registry.js +24 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/commands/adapt.js +26 -0
- package/dist/src/commands/import.js +51 -0
- package/dist/src/commands/init.js +104 -0
- package/dist/src/commands/install.js +257 -0
- package/dist/src/commands/link.js +34 -0
- package/dist/src/commands/list.js +19 -0
- package/dist/src/commands/marketplace.js +124 -0
- package/dist/src/commands/migrate.js +60 -0
- package/dist/src/commands/multiselect-skills.js +103 -0
- package/dist/src/commands/remove.js +102 -0
- package/dist/src/commands/select-agents.js +40 -0
- package/dist/src/commands/select-components.js +61 -0
- package/dist/src/commands/select-plugins.js +25 -0
- package/dist/src/commands/select-scope.js +20 -0
- package/dist/src/commands/update.js +50 -0
- package/dist/src/commands/validate.js +50 -0
- package/dist/src/components.js +90 -0
- package/dist/src/deps.js +46 -0
- package/dist/src/fsutil.js +32 -0
- package/dist/src/hash.js +51 -0
- package/dist/src/lock.js +51 -0
- package/dist/src/manifest.js +110 -0
- package/dist/src/marketplace.js +39 -0
- package/{src/package.ts → dist/src/package.js} +37 -42
- package/{src/paths.ts → dist/src/paths.js} +54 -60
- package/dist/src/semver.js +55 -0
- package/dist/src/skills.js +79 -0
- package/dist/src/sources.js +122 -0
- package/dist/src/types.js +19 -0
- package/dist/vendor/skills/package.json +143 -0
- package/dist/vendor/skills/src/add.js +1663 -0
- package/dist/vendor/skills/src/agents.js +729 -0
- package/dist/vendor/skills/src/blob.js +436 -0
- package/dist/vendor/skills/src/cli.js +340 -0
- package/dist/vendor/skills/src/constants.js +3 -0
- package/dist/vendor/skills/src/detect-agent.js +56 -0
- package/dist/vendor/skills/src/find.js +294 -0
- package/dist/vendor/skills/src/frontmatter.js +13 -0
- package/dist/vendor/skills/src/git-tree.js +32 -0
- package/dist/vendor/skills/src/git.js +235 -0
- package/dist/vendor/skills/src/install.js +75 -0
- package/dist/vendor/skills/src/installer.js +924 -0
- package/dist/vendor/skills/src/list.js +201 -0
- package/dist/vendor/skills/src/local-lock.js +109 -0
- package/dist/vendor/skills/src/plugin-manifest.js +152 -0
- package/dist/vendor/skills/src/prompts/search-multiselect.js +312 -0
- package/dist/vendor/skills/src/providers/index.js +4 -0
- package/dist/vendor/skills/src/providers/registry.js +42 -0
- package/dist/vendor/skills/src/providers/types.js +1 -0
- package/dist/vendor/skills/src/providers/wellknown.js +625 -0
- package/dist/vendor/skills/src/remove.js +263 -0
- package/dist/vendor/skills/src/sanitize.js +57 -0
- package/dist/vendor/skills/src/self-cli.js +15 -0
- package/dist/vendor/skills/src/skill-lock.js +237 -0
- package/dist/vendor/skills/src/skills.js +264 -0
- package/dist/vendor/skills/src/source-parser.js +367 -0
- package/dist/vendor/skills/src/sync.js +404 -0
- package/dist/vendor/skills/src/telemetry.js +101 -0
- package/dist/vendor/skills/src/test-utils.js +59 -0
- package/dist/vendor/skills/src/types.js +1 -0
- package/dist/vendor/skills/src/update-source.js +76 -0
- package/dist/vendor/skills/src/update.js +590 -0
- package/dist/vendor/skills/src/use.js +505 -0
- package/package.json +15 -7
- package/bin/adg.ts +0 -758
- package/src/adapters/anthropic.ts +0 -54
- package/src/adapters/index.ts +0 -24
- package/src/adapters/openai.ts +0 -37
- package/src/adapters/reverse.ts +0 -60
- package/src/agents/claude.ts +0 -124
- package/src/agents/codex.ts +0 -67
- package/src/agents/registry.ts +0 -30
- package/src/agents/types.ts +0 -47
- package/src/commands/adapt.ts +0 -36
- package/src/commands/import.ts +0 -69
- package/src/commands/init.ts +0 -146
- package/src/commands/install.ts +0 -411
- package/src/commands/link.ts +0 -61
- package/src/commands/list.ts +0 -28
- package/src/commands/marketplace.ts +0 -198
- package/src/commands/migrate.ts +0 -84
- package/src/commands/multiselect-skills.ts +0 -137
- package/src/commands/remove.ts +0 -136
- package/src/commands/select-agents.ts +0 -45
- package/src/commands/select-components.ts +0 -66
- package/src/commands/select-plugins.ts +0 -28
- package/src/commands/select-scope.ts +0 -21
- package/src/commands/update.ts +0 -85
- package/src/commands/validate.ts +0 -57
- package/src/components.ts +0 -90
- package/src/deps.ts +0 -64
- package/src/fsutil.ts +0 -38
- package/src/hash.ts +0 -61
- package/src/lock.ts +0 -57
- package/src/manifest.ts +0 -113
- package/src/marketplace.ts +0 -41
- package/src/semver.ts +0 -67
- package/src/skills.ts +0 -88
- package/src/sources.ts +0 -159
- package/src/types.ts +0 -140
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ADG_SCHEMA_VERSION } from "./types.js";
|
|
4
|
+
/** Canonical, vendor-neutral source manifest location (a plugin). */
|
|
5
|
+
export const ADG_MANIFEST_PATH = join(".agents", ".plugin.json");
|
|
6
|
+
/** Canonical, vendor-neutral source catalog location (a marketplace). */
|
|
7
|
+
export const ADG_MARKETPLACE_PATH = join(".agents", ".marketplace.json");
|
|
8
|
+
/** Legacy location, still read (deprecated) so pre-`.agents/` plugins resolve. */
|
|
9
|
+
export const LEGACY_MANIFEST_PATH = join(".adg-plugin", "plugin.json");
|
|
10
|
+
/**
|
|
11
|
+
* Resolve a plugin's manifest file, preferring the canonical `.agents/.plugin.json`
|
|
12
|
+
* and falling back to the legacy `.adg-plugin/plugin.json`. Returns undefined
|
|
13
|
+
* when neither exists.
|
|
14
|
+
*/
|
|
15
|
+
export function findManifestFile(pluginDir) {
|
|
16
|
+
const primary = join(pluginDir, ADG_MANIFEST_PATH);
|
|
17
|
+
if (existsSync(primary))
|
|
18
|
+
return primary;
|
|
19
|
+
const legacy = join(pluginDir, LEGACY_MANIFEST_PATH);
|
|
20
|
+
if (existsSync(legacy))
|
|
21
|
+
return legacy;
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
const NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
25
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
|
|
26
|
+
export class ManifestError extends Error {
|
|
27
|
+
issues;
|
|
28
|
+
constructor(issues) {
|
|
29
|
+
super(`Invalid ADG manifest:\n - ${issues.join("\n - ")}`);
|
|
30
|
+
this.name = "ManifestError";
|
|
31
|
+
this.issues = issues;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Read and validate a plugin's `.agents/.plugin.json` (or legacy fallback). */
|
|
35
|
+
export function readManifest(pluginDir) {
|
|
36
|
+
const file = findManifestFile(pluginDir);
|
|
37
|
+
if (!file) {
|
|
38
|
+
throw new ManifestError([`${ADG_MANIFEST_PATH} not found in ${pluginDir}`]);
|
|
39
|
+
}
|
|
40
|
+
let raw;
|
|
41
|
+
try {
|
|
42
|
+
raw = JSON.parse(readFileSync(file, "utf8"));
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
throw new ManifestError([`${file} is not valid JSON: ${err.message}`]);
|
|
46
|
+
}
|
|
47
|
+
return validateManifest(raw);
|
|
48
|
+
}
|
|
49
|
+
/** Validate an already-parsed manifest object, throwing ManifestError on failure. */
|
|
50
|
+
export function validateManifest(raw) {
|
|
51
|
+
const issues = collectIssues(raw);
|
|
52
|
+
if (issues.length > 0)
|
|
53
|
+
throw new ManifestError(issues);
|
|
54
|
+
return raw;
|
|
55
|
+
}
|
|
56
|
+
/** Collect validation issues without throwing (used by the `validate` command). */
|
|
57
|
+
export function collectIssues(raw) {
|
|
58
|
+
const issues = [];
|
|
59
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
60
|
+
return ["manifest must be a JSON object"];
|
|
61
|
+
}
|
|
62
|
+
const m = raw;
|
|
63
|
+
if (m.schemaVersion !== ADG_SCHEMA_VERSION) {
|
|
64
|
+
issues.push(`schemaVersion must be "${ADG_SCHEMA_VERSION}"`);
|
|
65
|
+
}
|
|
66
|
+
if (typeof m.name !== "string" || !NAME_RE.test(m.name)) {
|
|
67
|
+
issues.push("name is required and must be kebab-case (^[a-z0-9]+(-[a-z0-9]+)*$)");
|
|
68
|
+
}
|
|
69
|
+
if (typeof m.version !== "string" || !SEMVER_RE.test(m.version)) {
|
|
70
|
+
issues.push("version is required and must be semantic (e.g. 0.1.0)");
|
|
71
|
+
}
|
|
72
|
+
if (typeof m.description !== "string" || m.description.length === 0) {
|
|
73
|
+
issues.push("description is required and must be a non-empty string");
|
|
74
|
+
}
|
|
75
|
+
if (m.skills !== undefined && typeof m.skills !== "string" && !isStringArray(m.skills)) {
|
|
76
|
+
issues.push("skills must be a string or an array of strings");
|
|
77
|
+
}
|
|
78
|
+
for (const key of ["agents", "commands", "apps", "hooks", "mcp", "homepage", "changelog", "license", "category"]) {
|
|
79
|
+
if (m[key] !== undefined && typeof m[key] !== "string") {
|
|
80
|
+
issues.push(`${key} must be a string`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (m.strict !== undefined && typeof m.strict !== "boolean") {
|
|
84
|
+
issues.push("strict must be a boolean");
|
|
85
|
+
}
|
|
86
|
+
if (m.dependencies !== undefined) {
|
|
87
|
+
if (!Array.isArray(m.dependencies)) {
|
|
88
|
+
issues.push("dependencies must be an array");
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
m.dependencies.forEach((dep, i) => {
|
|
92
|
+
if (typeof dep !== "object" || dep === null) {
|
|
93
|
+
issues.push(`dependencies[${i}] must be an object`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const d = dep;
|
|
97
|
+
if (typeof d.name !== "string")
|
|
98
|
+
issues.push(`dependencies[${i}].name must be a string`);
|
|
99
|
+
if (typeof d.version !== "string")
|
|
100
|
+
issues.push(`dependencies[${i}].version must be a string`);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// `adapters` is no longer part of the DSL. A stray one from an old manifest is
|
|
105
|
+
// tolerated (ignored) rather than rejected — output paths are ADG-internal.
|
|
106
|
+
return issues;
|
|
107
|
+
}
|
|
108
|
+
function isStringArray(v) {
|
|
109
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
110
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
export function emptyMarketplace(name) {
|
|
3
|
+
return { name, plugins: [] };
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Read a marketplace file. This file is the runtime-facing export (consumed by
|
|
7
|
+
* Codex), kept in the de-facto shape; ADG never treats it as its own control
|
|
8
|
+
* surface (that is the lock). The reader is tolerant: only structural fields are
|
|
9
|
+
* checked and unknown fields are preserved.
|
|
10
|
+
*/
|
|
11
|
+
export function readMarketplace(file, fallbackName) {
|
|
12
|
+
if (!existsSync(file))
|
|
13
|
+
return emptyMarketplace(fallbackName);
|
|
14
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
15
|
+
if (typeof raw.name !== "string" || !Array.isArray(raw.plugins)) {
|
|
16
|
+
throw new Error(`${file} is not a valid marketplace.json`);
|
|
17
|
+
}
|
|
18
|
+
return raw;
|
|
19
|
+
}
|
|
20
|
+
export function writeMarketplace(file, market) {
|
|
21
|
+
writeFileSync(file, JSON.stringify(market, null, 2) + "\n");
|
|
22
|
+
}
|
|
23
|
+
/** Insert or replace a plugin entry by name, preserving array order. */
|
|
24
|
+
export function upsertMarketplacePlugin(market, plugin) {
|
|
25
|
+
const idx = market.plugins.findIndex((p) => p.name === plugin.name);
|
|
26
|
+
if (idx >= 0)
|
|
27
|
+
market.plugins[idx] = plugin;
|
|
28
|
+
else
|
|
29
|
+
market.plugins.push(plugin);
|
|
30
|
+
return market;
|
|
31
|
+
}
|
|
32
|
+
/** Remove a plugin entry by name. Returns true if one was removed. */
|
|
33
|
+
export function removeMarketplacePlugin(market, name) {
|
|
34
|
+
const idx = market.plugins.findIndex((p) => p.name === name);
|
|
35
|
+
if (idx < 0)
|
|
36
|
+
return false;
|
|
37
|
+
market.plugins.splice(idx, 1);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { AdgManifest } from "./types.ts";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Manifest-driven packaging allowlist.
|
|
5
3
|
*
|
|
@@ -10,45 +8,42 @@ import type { AdgManifest } from "./types.ts";
|
|
|
10
8
|
* is both safer (no accidental secret/dev-file leakage) and consistent with the
|
|
11
9
|
* lock's content hash.
|
|
12
10
|
*/
|
|
13
|
-
|
|
14
11
|
/** Top-level metadata files always shipped with a plugin, matched case-insensitively. */
|
|
15
12
|
const META_RE = /^(README|LICEN[CS]E|CHANGELOG|NOTICE)(\..+)?$/i;
|
|
16
|
-
|
|
17
13
|
/** Generated runtime projections — shipped, but excluded from the content hash. */
|
|
18
14
|
export const PROJECTION_DIRS = [".claude-plugin", ".codex-plugin"];
|
|
19
|
-
|
|
20
15
|
/** Extract the first path segment of a manifest component value (e.g. "./skills/" -> "skills"). */
|
|
21
|
-
function topSegment(p
|
|
22
|
-
|
|
16
|
+
function topSegment(p) {
|
|
17
|
+
return p.replace(/^\.?[/\\]/, "").split(/[/\\]/)[0] ?? "";
|
|
23
18
|
}
|
|
24
|
-
|
|
25
19
|
/**
|
|
26
20
|
* The set of root-level entry names that constitute the authored plugin payload,
|
|
27
21
|
* derived from the manifest's declared components plus the canonical `.agents/`
|
|
28
22
|
* manifest home.
|
|
29
23
|
*/
|
|
30
|
-
export function packagedRoots(manifest
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
24
|
+
export function packagedRoots(manifest) {
|
|
25
|
+
// `.agents` is the canonical manifest home; `.adg-plugin` keeps legacy plugins
|
|
26
|
+
// shippable during the deprecation window.
|
|
27
|
+
const roots = new Set([".agents", ".adg-plugin"]);
|
|
28
|
+
const components = [
|
|
29
|
+
manifest.skills,
|
|
30
|
+
manifest.agents,
|
|
31
|
+
manifest.commands,
|
|
32
|
+
manifest.mcp,
|
|
33
|
+
manifest.hooks,
|
|
34
|
+
manifest.apps,
|
|
35
|
+
];
|
|
36
|
+
for (const c of components) {
|
|
37
|
+
if (c === undefined)
|
|
38
|
+
continue;
|
|
39
|
+
for (const p of Array.isArray(c) ? c : [c]) {
|
|
40
|
+
const seg = topSegment(p);
|
|
41
|
+
if (seg)
|
|
42
|
+
roots.add(seg);
|
|
43
|
+
}
|
|
47
44
|
}
|
|
48
|
-
|
|
49
|
-
return roots;
|
|
45
|
+
return roots;
|
|
50
46
|
}
|
|
51
|
-
|
|
52
47
|
/**
|
|
53
48
|
* Build a predicate over a root-relative path: whether it belongs to the
|
|
54
49
|
* packaged plugin. Used by both the copy step and the content hash so an
|
|
@@ -57,18 +52,18 @@ export function packagedRoots(manifest: AdgManifest): Set<string> {
|
|
|
57
52
|
* `includeProjections` controls whether the generated runtime manifests count:
|
|
58
53
|
* true when copying (they ship), false when hashing (so re-adapting is stable).
|
|
59
54
|
*/
|
|
60
|
-
export function packageFilter(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
55
|
+
export function packageFilter(manifest, opts) {
|
|
56
|
+
const roots = packagedRoots(manifest);
|
|
57
|
+
return (relPath) => {
|
|
58
|
+
if (relPath === "")
|
|
59
|
+
return true; // the plugin root itself
|
|
60
|
+
const segments = relPath.split(/[/\\]/);
|
|
61
|
+
const first = segments[0];
|
|
62
|
+
if (roots.has(first))
|
|
63
|
+
return true;
|
|
64
|
+
if (opts.includeProjections && PROJECTION_DIRS.includes(first))
|
|
65
|
+
return true;
|
|
66
|
+
// Metadata files only count at the root level (a single segment).
|
|
67
|
+
return segments.length === 1 && META_RE.test(first);
|
|
68
|
+
};
|
|
74
69
|
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { basename, dirname, join, relative } from "node:path";
|
|
4
|
-
import type { PluginSource } from "./types.ts";
|
|
5
|
-
|
|
6
4
|
export const LOCK_FILENAME = ".plugin-lock.json";
|
|
7
5
|
export const MARKETPLACE_FILENAME = "marketplace.json";
|
|
8
|
-
|
|
9
6
|
/**
|
|
10
7
|
* Resolve the global plugins directory.
|
|
11
8
|
*
|
|
@@ -13,58 +10,57 @@ export const MARKETPLACE_FILENAME = "marketplace.json";
|
|
|
13
10
|
* ~/.agents/plugins. Only the `plugins/` subtree is ever managed by ADG — the
|
|
14
11
|
* sibling AGENTS.md and skills/ are never read or written by this tool.
|
|
15
12
|
*/
|
|
16
|
-
export function globalPluginsDir(env
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
export function globalPluginsDir(env = process.env) {
|
|
14
|
+
if (env.ADG_PLUGINS_HOME)
|
|
15
|
+
return env.ADG_PLUGINS_HOME;
|
|
16
|
+
if (env.XDG_STATE_HOME)
|
|
17
|
+
return join(env.XDG_STATE_HOME, ".agents", "plugins");
|
|
18
|
+
return join(homedir(), ".agents", "plugins");
|
|
20
19
|
}
|
|
21
|
-
|
|
22
20
|
/**
|
|
23
21
|
* Resolve the project plugins directory by walking up from `start` to find a
|
|
24
22
|
* `.agents/plugins` directory or a repo root (`.git`); defaults to
|
|
25
23
|
* `<start>/.agents/plugins` when none is found.
|
|
26
24
|
*/
|
|
27
|
-
export function projectPluginsDir(start
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
export function projectPluginsDir(start = process.cwd()) {
|
|
26
|
+
let dir = start;
|
|
27
|
+
while (true) {
|
|
28
|
+
const candidate = join(dir, ".agents", "plugins");
|
|
29
|
+
if (existsSync(candidate))
|
|
30
|
+
return candidate;
|
|
31
|
+
if (existsSync(join(dir, ".git")))
|
|
32
|
+
return candidate;
|
|
33
|
+
const parent = dirname(dir);
|
|
34
|
+
if (parent === dir)
|
|
35
|
+
return join(start, ".agents", "plugins");
|
|
36
|
+
dir = parent;
|
|
37
|
+
}
|
|
37
38
|
}
|
|
38
|
-
|
|
39
39
|
/**
|
|
40
40
|
* Resolve Claude's skills-directory plugin root, where a symlinked plugin
|
|
41
41
|
* auto-loads as `<name>@skills-dir`. Global → ~/.claude/skills, project →
|
|
42
42
|
* <cwd>/.claude/skills. This is Claude's own directory, distinct from the
|
|
43
43
|
* never-touched ~/.agents/skills.
|
|
44
44
|
*/
|
|
45
|
-
export function claudeSkillsDir(global
|
|
46
|
-
|
|
45
|
+
export function claudeSkillsDir(global, cwd = process.cwd()) {
|
|
46
|
+
return global ? join(homedir(), ".claude", "skills") : join(cwd, ".claude", "skills");
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return join(pluginsDir, LOCK_FILENAME);
|
|
48
|
+
export function lockPath(pluginsDir) {
|
|
49
|
+
return join(pluginsDir, LOCK_FILENAME);
|
|
51
50
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return join(pluginsDir, MARKETPLACE_FILENAME);
|
|
51
|
+
export function marketplacePath(pluginsDir) {
|
|
52
|
+
return join(pluginsDir, MARKETPLACE_FILENAME);
|
|
55
53
|
}
|
|
56
|
-
|
|
57
54
|
/**
|
|
58
55
|
* Make a filesystem-safe single path segment out of a git URL by replacing the
|
|
59
56
|
* scheme/host/path separators with `__` and dropping a trailing `.git`.
|
|
60
57
|
*/
|
|
61
|
-
function sanitizeGitUrl(url
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
function sanitizeGitUrl(url) {
|
|
59
|
+
return url
|
|
60
|
+
.replace(/\.git$/, "")
|
|
61
|
+
.replace(/[:/@]+/g, "__")
|
|
62
|
+
.replace(/^_+|_+$/g, "");
|
|
66
63
|
}
|
|
67
|
-
|
|
68
64
|
/**
|
|
69
65
|
* The directory bucket a plugin's files live under, grouped by the source it
|
|
70
66
|
* came from: remote sources nest beneath a per-marketplace segment
|
|
@@ -72,40 +68,39 @@ function sanitizeGitUrl(url: string): string {
|
|
|
72
68
|
* This is the on-disk grouping only — plugin name remains the unique key in the
|
|
73
69
|
* lock and marketplace.json.
|
|
74
70
|
*/
|
|
75
|
-
export function sourceDirSegment(origin
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
71
|
+
export function sourceDirSegment(origin) {
|
|
72
|
+
switch (origin.type) {
|
|
73
|
+
case "local":
|
|
74
|
+
return null;
|
|
75
|
+
case "github":
|
|
76
|
+
return origin.repo.replaceAll("/", "__");
|
|
77
|
+
case "git":
|
|
78
|
+
return sanitizeGitUrl(origin.url);
|
|
79
|
+
}
|
|
84
80
|
}
|
|
85
|
-
|
|
86
81
|
/**
|
|
87
82
|
* Resolve where a plugin's files are installed: `<pluginsDir>/<segment>/<name>`
|
|
88
83
|
* for remote sources, or the flat `<pluginsDir>/<name>` for local installs.
|
|
89
84
|
*/
|
|
90
|
-
export function pluginDir(pluginsDir
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
export function pluginDir(pluginsDir, name, origin) {
|
|
86
|
+
const seg = sourceDirSegment(origin);
|
|
87
|
+
return seg ? join(pluginsDir, seg, name) : join(pluginsDir, name);
|
|
93
88
|
}
|
|
94
|
-
|
|
95
89
|
/**
|
|
96
90
|
* Resolve a plugin's *actual* on-disk directory. Prefers the canonical
|
|
97
91
|
* per-marketplace nested path, but falls back to the flat `<pluginsDir>/<name>`
|
|
98
92
|
* for installs made before the nested layout (and not yet `migrate`d). Returns
|
|
99
93
|
* the canonical nested path when neither exists.
|
|
100
94
|
*/
|
|
101
|
-
export function installedPluginDir(pluginsDir
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
95
|
+
export function installedPluginDir(pluginsDir, name, origin) {
|
|
96
|
+
const nested = pluginDir(pluginsDir, name, origin);
|
|
97
|
+
if (existsSync(nested))
|
|
98
|
+
return nested;
|
|
99
|
+
const flat = join(pluginsDir, name);
|
|
100
|
+
if (existsSync(flat))
|
|
101
|
+
return flat;
|
|
102
|
+
return nested;
|
|
107
103
|
}
|
|
108
|
-
|
|
109
104
|
/**
|
|
110
105
|
* The `source.path` to record in marketplace.json for a plugin at `dest`.
|
|
111
106
|
*
|
|
@@ -121,9 +116,8 @@ export function installedPluginDir(pluginsDir: string, name: string, origin: Plu
|
|
|
121
116
|
* This stays correct regardless of where or how deep the store sits, instead of
|
|
122
117
|
* leaking parent-directory names from a fixed two-levels-up assumption.
|
|
123
118
|
*/
|
|
124
|
-
export function marketplaceSourcePath(pluginsDir
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return `./${relative(root, dest).split("\\").join("/")}`;
|
|
119
|
+
export function marketplaceSourcePath(pluginsDir, dest) {
|
|
120
|
+
const isCanonical = basename(pluginsDir) === "plugins" && basename(dirname(pluginsDir)) === ".agents";
|
|
121
|
+
const root = isCanonical ? dirname(dirname(pluginsDir)) : pluginsDir;
|
|
122
|
+
return `./${relative(root, dest).split("\\").join("/")}`;
|
|
129
123
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function parseVersion(v) {
|
|
2
|
+
const core = v.trim().replace(/^[v=]/, "").split(/[-+]/)[0] ?? "";
|
|
3
|
+
const parts = core.split(".");
|
|
4
|
+
if (parts.length !== 3 || parts.some((p) => !/^\d+$/.test(p))) {
|
|
5
|
+
throw new Error(`invalid semantic version: "${v}"`);
|
|
6
|
+
}
|
|
7
|
+
return [Number(parts[0]), Number(parts[1]), Number(parts[2])];
|
|
8
|
+
}
|
|
9
|
+
export function compare(a, b) {
|
|
10
|
+
for (let i = 0; i < 3; i++) {
|
|
11
|
+
if (a[i] !== b[i])
|
|
12
|
+
return a[i] < b[i] ? -1 : 1;
|
|
13
|
+
}
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
export function satisfies(version, range) {
|
|
17
|
+
const v = parseVersion(version);
|
|
18
|
+
const r = range.trim();
|
|
19
|
+
if (r === "" || r === "*" || r === "x" || r === "X")
|
|
20
|
+
return true;
|
|
21
|
+
const cmp = r.match(/^(>=|<=|>|<|=)\s*(.+)$/);
|
|
22
|
+
if (cmp) {
|
|
23
|
+
const target = parseVersion(cmp[2]);
|
|
24
|
+
const c = compare(v, target);
|
|
25
|
+
switch (cmp[1]) {
|
|
26
|
+
case ">=": return c >= 0;
|
|
27
|
+
case "<=": return c <= 0;
|
|
28
|
+
case ">": return c > 0;
|
|
29
|
+
case "<": return c < 0;
|
|
30
|
+
case "=": return c === 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (r.startsWith("^")) {
|
|
34
|
+
const base = parseVersion(r.slice(1));
|
|
35
|
+
if (compare(v, base) < 0)
|
|
36
|
+
return false;
|
|
37
|
+
const upper = caretUpperBound(base);
|
|
38
|
+
return compare(v, upper) < 0;
|
|
39
|
+
}
|
|
40
|
+
if (r.startsWith("~")) {
|
|
41
|
+
const base = parseVersion(r.slice(1));
|
|
42
|
+
if (compare(v, base) < 0)
|
|
43
|
+
return false;
|
|
44
|
+
return compare(v, [base[0], base[1] + 1, 0]) < 0;
|
|
45
|
+
}
|
|
46
|
+
return compare(v, parseVersion(r)) === 0;
|
|
47
|
+
}
|
|
48
|
+
/** npm caret semantics: first non-zero leftmost component is the boundary. */
|
|
49
|
+
function caretUpperBound([major, minor, patch]) {
|
|
50
|
+
if (major > 0)
|
|
51
|
+
return [major + 1, 0, 0];
|
|
52
|
+
if (minor > 0)
|
|
53
|
+
return [0, minor + 1, 0];
|
|
54
|
+
return [0, 0, patch + 1];
|
|
55
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the skills a plugin exposes, paired with their SKILL.md paths.
|
|
6
|
+
*
|
|
7
|
+
* - `skills` array → each entry's basename, SKILL.md resolved under the entry.
|
|
8
|
+
* - otherwise → auto-scan the skills root for sub-dirs that contain a SKILL.md.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveSkillEntries(pluginDir, manifest) {
|
|
11
|
+
const skills = manifest.skills;
|
|
12
|
+
if (Array.isArray(skills)) {
|
|
13
|
+
return skills.map((p) => {
|
|
14
|
+
const rel = p.replace(/\/+$/, "");
|
|
15
|
+
const name = rel.split("/").pop() ?? p;
|
|
16
|
+
const abs = join(pluginDir, rel);
|
|
17
|
+
let skillMd;
|
|
18
|
+
if (existsSync(abs)) {
|
|
19
|
+
const md = statSync(abs).isDirectory() ? join(abs, "SKILL.md") : abs;
|
|
20
|
+
if (existsSync(md))
|
|
21
|
+
skillMd = md;
|
|
22
|
+
}
|
|
23
|
+
return { name, skillMd };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const root = typeof skills === "string" ? skills : "./skills/";
|
|
27
|
+
const abs = join(pluginDir, root);
|
|
28
|
+
if (!existsSync(abs))
|
|
29
|
+
return [];
|
|
30
|
+
return readdirSync(abs, { withFileTypes: true })
|
|
31
|
+
.filter((e) => e.isDirectory() && existsSync(join(abs, e.name, "SKILL.md")))
|
|
32
|
+
.map((e) => ({ name: e.name, skillMd: join(abs, e.name, "SKILL.md") }))
|
|
33
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the list of skill names a plugin exposes.
|
|
37
|
+
*
|
|
38
|
+
* - `strict !== false` with an explicit `skills` array → that array (basenames).
|
|
39
|
+
* - otherwise → auto-scan the skills root directory for sub-dirs that contain a
|
|
40
|
+
* SKILL.md file (skill names are the kebab-case directory names).
|
|
41
|
+
*/
|
|
42
|
+
export function resolveSkills(pluginDir, manifest) {
|
|
43
|
+
return resolveSkillEntries(pluginDir, manifest).map((e) => e.name);
|
|
44
|
+
}
|
|
45
|
+
/** Read a SKILL.md's `description` from its YAML frontmatter (undefined if absent). */
|
|
46
|
+
export function readSkillDescription(skillMd) {
|
|
47
|
+
try {
|
|
48
|
+
const head = readFileSync(skillMd, "utf8").slice(0, 4096);
|
|
49
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(head);
|
|
50
|
+
if (!m)
|
|
51
|
+
return undefined;
|
|
52
|
+
const fm = parseYaml(m[1] ?? "");
|
|
53
|
+
const d = fm?.description;
|
|
54
|
+
return typeof d === "string" && d.trim() ? d.trim() : undefined;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build a lazy, cached description reader keyed by skill name. Files are only
|
|
62
|
+
* read on the first lookup of each name (and never for skills the user doesn't
|
|
63
|
+
* inspect), keeping the interactive picker free of upfront SKILL.md parsing.
|
|
64
|
+
*/
|
|
65
|
+
export function skillDescriptionLoader(pluginDir, manifest) {
|
|
66
|
+
let paths;
|
|
67
|
+
const cache = new Map();
|
|
68
|
+
return (name) => {
|
|
69
|
+
if (cache.has(name))
|
|
70
|
+
return cache.get(name);
|
|
71
|
+
if (!paths) {
|
|
72
|
+
paths = new Map(resolveSkillEntries(pluginDir, manifest).map((e) => [e.name, e.skillMd]));
|
|
73
|
+
}
|
|
74
|
+
const md = paths.get(name);
|
|
75
|
+
const desc = md ? readSkillDescription(md) : undefined;
|
|
76
|
+
cache.set(name, desc);
|
|
77
|
+
return desc;
|
|
78
|
+
};
|
|
79
|
+
}
|