@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,102 @@
|
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readlinkSync, rmdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { claudeSkillsDir, lockPath, marketplacePath, pluginDir } from "../paths.js";
|
|
4
|
+
import { readLock, removeEntry, writeLock } from "../lock.js";
|
|
5
|
+
import { readMarketplace, removeMarketplacePlugin, writeMarketplace } from "../marketplace.js";
|
|
6
|
+
import { basename } from "node:path";
|
|
7
|
+
import { resolveAgents } from "../agents/index.js";
|
|
8
|
+
/**
|
|
9
|
+
* Remove an installed plugin: delete its directory, drop it from
|
|
10
|
+
* `.plugin-lock.json` and `marketplace.json`, and clean up any Claude
|
|
11
|
+
* skills-dir symlinks that pointed at it. Only paths under `pluginsDir` (plus
|
|
12
|
+
* symlinks that resolve back into it) are touched.
|
|
13
|
+
*
|
|
14
|
+
* Refuses when another installed plugin depends on it, unless `force` is set.
|
|
15
|
+
*/
|
|
16
|
+
export function removePlugin(opts) {
|
|
17
|
+
const { name } = opts;
|
|
18
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
19
|
+
const lockFile = lockPath(opts.pluginsDir);
|
|
20
|
+
const lock = readLock(lockFile);
|
|
21
|
+
const inLock = name in lock.plugins;
|
|
22
|
+
// Locate the plugin via its recorded origin (nested for remote sources). An
|
|
23
|
+
// orphan that is on disk but absent from the lock falls back to the flat path.
|
|
24
|
+
const dir = inLock
|
|
25
|
+
? pluginDir(opts.pluginsDir, name, lock.plugins[name].origin)
|
|
26
|
+
: join(opts.pluginsDir, name);
|
|
27
|
+
const onDisk = existsSync(dir);
|
|
28
|
+
if (!inLock && !onDisk) {
|
|
29
|
+
throw new Error(`plugin "${name}" is not installed in ${opts.pluginsDir}`);
|
|
30
|
+
}
|
|
31
|
+
if (!opts.force) {
|
|
32
|
+
const dependents = Object.entries(lock.plugins)
|
|
33
|
+
.filter(([n, e]) => n !== name && e.dependencies && name in e.dependencies)
|
|
34
|
+
.map(([n]) => n);
|
|
35
|
+
if (dependents.length > 0) {
|
|
36
|
+
throw new Error(`cannot remove "${name}": required by ${dependents.join(", ")}. ` +
|
|
37
|
+
`Remove the dependents first or pass --force.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Clean up Claude symlinks (created by `adg plugins link --target claude`) in
|
|
41
|
+
// both global and project scopes — but only ones that resolve back to this
|
|
42
|
+
// plugin's directory, so unrelated entries are never disturbed.
|
|
43
|
+
const target = resolve(dir);
|
|
44
|
+
const unlinked = [];
|
|
45
|
+
for (const dir of [claudeSkillsDir(true, cwd), claudeSkillsDir(false, cwd)]) {
|
|
46
|
+
const linkPath = join(dir, name);
|
|
47
|
+
if (isSymlinkTo(linkPath, target)) {
|
|
48
|
+
rmSync(linkPath);
|
|
49
|
+
unlinked.push(linkPath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
let removedDir;
|
|
53
|
+
if (onDisk) {
|
|
54
|
+
rmSync(dir, { recursive: true, force: true });
|
|
55
|
+
removedDir = dir;
|
|
56
|
+
// Drop the per-marketplace bucket if it became empty (nested remote layout).
|
|
57
|
+
pruneEmptyParent(dirname(dir), opts.pluginsDir);
|
|
58
|
+
}
|
|
59
|
+
const removedFromLock = removeEntry(lock, name);
|
|
60
|
+
if (removedFromLock)
|
|
61
|
+
writeLock(lockFile, lock);
|
|
62
|
+
const marketFile = marketplacePath(opts.pluginsDir);
|
|
63
|
+
const market = readMarketplace(marketFile, basename(opts.pluginsDir));
|
|
64
|
+
const removedFromMarketplace = removeMarketplacePlugin(market, name);
|
|
65
|
+
if (removedFromMarketplace)
|
|
66
|
+
writeMarketplace(marketFile, market);
|
|
67
|
+
// Uninstall from the agents so we don't leave them enabled, pointing at a
|
|
68
|
+
// now-deleted directory. Best-effort across all; missing CLIs are skipped.
|
|
69
|
+
let agents;
|
|
70
|
+
if (opts.deactivate) {
|
|
71
|
+
const ctx = { pluginsDir: opts.pluginsDir, plugins: [name], scope: opts.scope ?? "project" };
|
|
72
|
+
agents = (opts.agents ?? resolveAgents()).map((a) => a.deactivate(ctx));
|
|
73
|
+
}
|
|
74
|
+
return { name, removedDir, unlinked, removedFromLock, removedFromMarketplace, agents };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Remove `parent` if it is an empty per-marketplace bucket below `pluginsDir`.
|
|
78
|
+
* Never touches `pluginsDir` itself (that would be the flat local layout).
|
|
79
|
+
*/
|
|
80
|
+
function pruneEmptyParent(parent, pluginsDir) {
|
|
81
|
+
if (resolve(parent) === resolve(pluginsDir))
|
|
82
|
+
return;
|
|
83
|
+
try {
|
|
84
|
+
if (readdirSync(parent).length === 0)
|
|
85
|
+
rmdirSync(parent);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// bucket already gone or not readable — nothing to prune
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function isSymlinkTo(linkPath, target) {
|
|
92
|
+
try {
|
|
93
|
+
if (!lstatSync(linkPath).isSymbolicLink())
|
|
94
|
+
return false;
|
|
95
|
+
// readlinkSync may return a path relative to the link's own directory; resolve
|
|
96
|
+
// it there (resolve ignores the base when the target is already absolute).
|
|
97
|
+
return resolve(dirname(linkPath), readlinkSync(linkPath)) === resolve(target);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { allAgents, detectedAgents } from "../agents/index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Interactively choose which AI agents to adapt an installed plugin for.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors the skills install flow: detect the agents present on the machine,
|
|
8
|
+
* pre-select them, and let the user toggle. When nothing is detected we fall
|
|
9
|
+
* back to pre-selecting every target so a fresh setup still adapts for all.
|
|
10
|
+
*
|
|
11
|
+
* Callers should only reach this in an interactive TTY without an explicit
|
|
12
|
+
* `--target`; non-interactive paths stay flag-driven.
|
|
13
|
+
*/
|
|
14
|
+
export async function selectTargetsInteractive() {
|
|
15
|
+
const agents = allAgents();
|
|
16
|
+
const detectedSet = new Set(detectedAgents().map((a) => a.id));
|
|
17
|
+
const initialValues = (detectedSet.size > 0 ? [...detectedSet] : agents.map((a) => a.id));
|
|
18
|
+
if (detectedSet.size > 0) {
|
|
19
|
+
const names = agents.filter((a) => detectedSet.has(a.id)).map((a) => pc.cyan(a.displayName)).join(", ");
|
|
20
|
+
p.log.info(`Detected: ${names}`);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
p.log.info(pc.dim("No agents detected locally — pre-selecting all."));
|
|
24
|
+
}
|
|
25
|
+
const selected = await p.multiselect({
|
|
26
|
+
message: `Which agents do you want to adapt for? ${pc.dim("(space to toggle)")}`,
|
|
27
|
+
options: agents.map((a) => ({
|
|
28
|
+
value: a.id,
|
|
29
|
+
label: a.displayName,
|
|
30
|
+
...(detectedSet.has(a.id) ? { hint: "detected" } : {}),
|
|
31
|
+
})),
|
|
32
|
+
initialValues,
|
|
33
|
+
required: true,
|
|
34
|
+
});
|
|
35
|
+
if (p.isCancel(selected)) {
|
|
36
|
+
p.cancel("Installation cancelled");
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
return selected;
|
|
40
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import { multiselectSkills } from "./multiselect-skills.js";
|
|
4
|
+
function cancelled(value) {
|
|
5
|
+
if (p.isCancel(value)) {
|
|
6
|
+
p.cancel("Installation cancelled");
|
|
7
|
+
process.exit(0);
|
|
8
|
+
}
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The "install everything?" gate. Returns true to install each selected plugin
|
|
13
|
+
* in full, false to drop into per-plugin component selection. Used by `add` in
|
|
14
|
+
* an interactive terminal when no --only/--skill flags were given.
|
|
15
|
+
*/
|
|
16
|
+
export async function confirmFullInstall(plugins) {
|
|
17
|
+
const list = plugins.length === 1 ? plugins[0] : `${plugins.length} plugins`;
|
|
18
|
+
const choice = await p.select({
|
|
19
|
+
message: `Install everything in ${pc.cyan(list)}?`,
|
|
20
|
+
options: [
|
|
21
|
+
{ value: true, label: "Yes, install all", hint: "every skill, command, agent, mcp…" },
|
|
22
|
+
{ value: false, label: "No, let me choose", hint: "pick components per plugin" },
|
|
23
|
+
],
|
|
24
|
+
initialValue: true,
|
|
25
|
+
});
|
|
26
|
+
if (cancelled(choice))
|
|
27
|
+
return true;
|
|
28
|
+
return choice;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Per-plugin component picker (①). Choose which categories to expose; if skills
|
|
32
|
+
* is kept and the plugin has several, drill in to pick individual skills. Files
|
|
33
|
+
* are still installed in full — this only narrows what the runtime sees.
|
|
34
|
+
*/
|
|
35
|
+
export async function selectComponentsInteractive(req) {
|
|
36
|
+
const components = await p.multiselect({
|
|
37
|
+
message: `${pc.bold(req.name)} — what to install? ${pc.dim("(space to toggle)")}`,
|
|
38
|
+
options: req.present.map((c) => ({ value: c, label: `${c} (${req.contents[c].length})` })),
|
|
39
|
+
initialValues: [...req.present],
|
|
40
|
+
required: true,
|
|
41
|
+
});
|
|
42
|
+
if (cancelled(components))
|
|
43
|
+
return { components: [...req.present] };
|
|
44
|
+
const chosen = components;
|
|
45
|
+
const selection = { components: chosen };
|
|
46
|
+
// Drill into individual skills only when skills is kept and there's a choice.
|
|
47
|
+
if (chosen.includes("skills") && req.contents.skills.length > 1) {
|
|
48
|
+
const message = `${pc.bold(req.name)} — which skills? ${pc.dim("(space to toggle)")}`;
|
|
49
|
+
const options = req.contents.skills.map((s) => ({ value: s, label: s }));
|
|
50
|
+
const initialValues = [...req.contents.skills];
|
|
51
|
+
const skills = req.skillDescription
|
|
52
|
+
? await multiselectSkills({ message, options, initialValues, loadDescription: req.skillDescription })
|
|
53
|
+
: await p.multiselect({ message, options, initialValues, required: true });
|
|
54
|
+
if (!cancelled(skills)) {
|
|
55
|
+
const picked = skills;
|
|
56
|
+
if (picked.length !== req.contents.skills.length)
|
|
57
|
+
selection.skills = picked;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return selection;
|
|
61
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
/**
|
|
4
|
+
* Interactive multiselect for choosing which plugins to install from a source
|
|
5
|
+
* that holds several. Mirrors the skills install flow. Native (reverse-adapted)
|
|
6
|
+
* plugins are flagged so the user knows they came from a Claude/Codex manifest.
|
|
7
|
+
*/
|
|
8
|
+
export async function selectPluginsInteractive(choices) {
|
|
9
|
+
const selected = await p.multiselect({
|
|
10
|
+
message: `Select plugins to install ${pc.dim("(space to toggle)")}`,
|
|
11
|
+
options: choices.map((c) => ({
|
|
12
|
+
value: c.name,
|
|
13
|
+
label: c.native ? `${c.name} ${pc.dim("(native)")}` : c.name,
|
|
14
|
+
...(c.description
|
|
15
|
+
? { hint: c.description.length > 60 ? c.description.slice(0, 57) + "..." : c.description }
|
|
16
|
+
: {}),
|
|
17
|
+
})),
|
|
18
|
+
required: true,
|
|
19
|
+
});
|
|
20
|
+
if (p.isCancel(selected)) {
|
|
21
|
+
p.cancel("Installation cancelled");
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
return selected;
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
/**
|
|
3
|
+
* Ask whether to install into the project (.agents/plugins) or globally
|
|
4
|
+
* (~/.agents/plugins). Returns true for global. Used by `add` when no scope
|
|
5
|
+
* flag (--global/--project/--dir) was given in an interactive terminal.
|
|
6
|
+
*/
|
|
7
|
+
export async function selectScopeInteractive() {
|
|
8
|
+
const scope = await p.select({
|
|
9
|
+
message: "Installation scope",
|
|
10
|
+
options: [
|
|
11
|
+
{ value: false, label: "Project", hint: ".agents/plugins (committed with your project)" },
|
|
12
|
+
{ value: true, label: "Global", hint: "~/.agents/plugins (available across all projects)" },
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
if (p.isCancel(scope)) {
|
|
16
|
+
p.cancel("Installation cancelled");
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
return scope;
|
|
20
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { folderHash } from "../hash.js";
|
|
3
|
+
import { packageFilter } from "../package.js";
|
|
4
|
+
import { lockPath, installedPluginDir } from "../paths.js";
|
|
5
|
+
import { readLock, writeLock } from "../lock.js";
|
|
6
|
+
import { readManifest } from "../manifest.js";
|
|
7
|
+
import { adaptPlugin } from "./adapt.js";
|
|
8
|
+
import { resolveAgents } from "../agents/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* Re-scan installed plugins in a plugins directory, refreshing each lock
|
|
11
|
+
* entry's version and folderHash from disk. Entries whose content or version
|
|
12
|
+
* changed are reported as `changed`. A missing plugin directory is reported as
|
|
13
|
+
* an issue rather than silently dropped.
|
|
14
|
+
*
|
|
15
|
+
* With `resync`, changed plugins are re-adapted (honoring their selection) and
|
|
16
|
+
* re-installed into the agents so Claude/Codex reflect the new content.
|
|
17
|
+
*/
|
|
18
|
+
export function updateLock(pluginsDir, now = new Date().toISOString(), opts = {}) {
|
|
19
|
+
const lockFile = lockPath(pluginsDir);
|
|
20
|
+
const lock = readLock(lockFile);
|
|
21
|
+
const results = [];
|
|
22
|
+
const missing = [];
|
|
23
|
+
const changedNames = [];
|
|
24
|
+
for (const [name, entry] of Object.entries(lock.plugins)) {
|
|
25
|
+
const dir = installedPluginDir(pluginsDir, name, entry.origin);
|
|
26
|
+
if (!existsSync(dir)) {
|
|
27
|
+
missing.push(name);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const manifest = readManifest(dir);
|
|
31
|
+
const hash = folderHash(dir, [".claude-plugin", ".codex-plugin"], packageFilter(manifest, { includeProjections: false }));
|
|
32
|
+
const changed = hash !== entry.folderHash || manifest.version !== entry.version;
|
|
33
|
+
if (changed) {
|
|
34
|
+
entry.folderHash = hash;
|
|
35
|
+
entry.version = manifest.version;
|
|
36
|
+
entry.updatedAt = now;
|
|
37
|
+
// Regenerate runtime manifests from the updated source, honoring selection.
|
|
38
|
+
adaptPlugin(dir, ["claude", "codex"], entry.selection);
|
|
39
|
+
changedNames.push(name);
|
|
40
|
+
}
|
|
41
|
+
results.push({ name, changed, version: manifest.version, folderHash: hash });
|
|
42
|
+
}
|
|
43
|
+
writeLock(lockFile, lock);
|
|
44
|
+
const out = { results, missing };
|
|
45
|
+
if (opts.resync && changedNames.length > 0) {
|
|
46
|
+
const ctx = { pluginsDir, plugins: changedNames, scope: opts.scope ?? "project" };
|
|
47
|
+
out.agents = (opts.agents ?? resolveAgents()).map((a) => a.refresh(ctx));
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { ADG_MANIFEST_PATH, collectIssues } from "../manifest.js";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
/**
|
|
6
|
+
* Validate a plugin's manifest against the ADG schema and check that the
|
|
7
|
+
* directories/files it references actually exist.
|
|
8
|
+
*/
|
|
9
|
+
export function validatePlugin(pluginDir) {
|
|
10
|
+
const manifestFile = join(pluginDir, ADG_MANIFEST_PATH);
|
|
11
|
+
if (!existsSync(manifestFile)) {
|
|
12
|
+
return { ok: false, issues: [`${ADG_MANIFEST_PATH} not found in ${pluginDir}`] };
|
|
13
|
+
}
|
|
14
|
+
let raw;
|
|
15
|
+
try {
|
|
16
|
+
raw = JSON.parse(readFileSync(manifestFile, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
return { ok: false, issues: [`${manifestFile} is not valid JSON: ${err.message}`] };
|
|
20
|
+
}
|
|
21
|
+
const issues = collectIssues(raw);
|
|
22
|
+
if (issues.length > 0)
|
|
23
|
+
return { ok: false, issues };
|
|
24
|
+
// Reference checks (only when structural validation passed).
|
|
25
|
+
const m = raw;
|
|
26
|
+
const pathFields = [
|
|
27
|
+
["agents", m.agents],
|
|
28
|
+
["commands", m.commands],
|
|
29
|
+
["apps", m.apps],
|
|
30
|
+
["hooks", m.hooks],
|
|
31
|
+
["mcp", m.mcp],
|
|
32
|
+
];
|
|
33
|
+
for (const [field, value] of pathFields) {
|
|
34
|
+
if (typeof value === "string" && !existsSync(join(pluginDir, value))) {
|
|
35
|
+
issues.push(`${field} points to "${value}" which does not exist`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (typeof m.skills === "string") {
|
|
39
|
+
if (!existsSync(join(pluginDir, m.skills))) {
|
|
40
|
+
issues.push(`skills root "${m.skills}" does not exist`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (Array.isArray(m.skills)) {
|
|
44
|
+
for (const s of m.skills) {
|
|
45
|
+
if (!existsSync(join(pluginDir, s)))
|
|
46
|
+
issues.push(`skill path "${s}" does not exist`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { ok: issues.length === 0, issues };
|
|
50
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { resolveSkills } from "./skills.js";
|
|
4
|
+
import { COMPONENT_TYPES } from "./types.js";
|
|
5
|
+
export { COMPONENT_TYPES };
|
|
6
|
+
/** Recursively collect regular (non-dotfile) file basenames, without extension. */
|
|
7
|
+
function collectFiles(dir) {
|
|
8
|
+
if (!existsSync(dir))
|
|
9
|
+
return [];
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
12
|
+
if (e.name.startsWith("."))
|
|
13
|
+
continue;
|
|
14
|
+
if (e.isDirectory())
|
|
15
|
+
out.push(...collectFiles(join(dir, e.name)));
|
|
16
|
+
else
|
|
17
|
+
out.push(e.name.replace(/\.[^.]+$/, ""));
|
|
18
|
+
}
|
|
19
|
+
return out.sort();
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A single file's member name: its basename without extension. `base` is
|
|
23
|
+
* injectable so the OS-separator handling can be exercised deterministically in
|
|
24
|
+
* tests (path.win32.basename vs path.posix.basename) regardless of host platform.
|
|
25
|
+
*/
|
|
26
|
+
export function fileMemberName(absPath, base = basename) {
|
|
27
|
+
return base(absPath).replace(/\.[^.]+$/, "");
|
|
28
|
+
}
|
|
29
|
+
/** Members of a declared path: a directory yields its files, a file yields its own name. */
|
|
30
|
+
function membersOf(dir, rel) {
|
|
31
|
+
if (!rel)
|
|
32
|
+
return [];
|
|
33
|
+
const abs = join(dir, rel);
|
|
34
|
+
if (!existsSync(abs))
|
|
35
|
+
return [];
|
|
36
|
+
const files = collectFiles(abs);
|
|
37
|
+
if (files.length > 0)
|
|
38
|
+
return files;
|
|
39
|
+
return [fileMemberName(abs)]; // a single file
|
|
40
|
+
}
|
|
41
|
+
/** Server names declared in an MCP config file (mcpServers/servers map). */
|
|
42
|
+
function mcpServers(file) {
|
|
43
|
+
if (!existsSync(file))
|
|
44
|
+
return [];
|
|
45
|
+
try {
|
|
46
|
+
const json = JSON.parse(readFileSync(file, "utf8"));
|
|
47
|
+
const servers = (json.mcpServers ?? json.servers);
|
|
48
|
+
return servers && typeof servers === "object" ? Object.keys(servers).sort() : [];
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Enumerate what a plugin contains, by reading its manifest's component paths. */
|
|
55
|
+
export function pluginContents(dir, manifest) {
|
|
56
|
+
return {
|
|
57
|
+
skills: resolveSkills(dir, manifest),
|
|
58
|
+
agents: membersOf(dir, manifest.agents),
|
|
59
|
+
commands: membersOf(dir, manifest.commands),
|
|
60
|
+
apps: membersOf(dir, manifest.apps),
|
|
61
|
+
hooks: membersOf(dir, manifest.hooks),
|
|
62
|
+
mcp: manifest.mcp ? mcpServers(join(dir, manifest.mcp)) : [],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Component types this plugin actually has (non-empty). */
|
|
66
|
+
export function presentComponents(contents) {
|
|
67
|
+
return COMPONENT_TYPES.filter((c) => contents[c].length > 0);
|
|
68
|
+
}
|
|
69
|
+
/** Whether a category is exposed under a selection (no selection = everything). */
|
|
70
|
+
export function isExposed(selection, category) {
|
|
71
|
+
return !selection || selection.components.includes(category);
|
|
72
|
+
}
|
|
73
|
+
/** Apply a selection to a contents map, returning only the exposed members. */
|
|
74
|
+
export function exposedContents(contents, selection) {
|
|
75
|
+
if (!selection)
|
|
76
|
+
return contents;
|
|
77
|
+
const out = {};
|
|
78
|
+
for (const c of COMPONENT_TYPES) {
|
|
79
|
+
if (!isExposed(selection, c)) {
|
|
80
|
+
out[c] = [];
|
|
81
|
+
}
|
|
82
|
+
else if (c === "skills" && selection.skills) {
|
|
83
|
+
out[c] = contents.skills.filter((s) => selection.skills.includes(s));
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
out[c] = contents[c];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
package/dist/src/deps.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { satisfies } from "./semver.js";
|
|
2
|
+
export class DependencyError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "DependencyError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Compute a topological install order for `rootName` and its transitive
|
|
10
|
+
* dependencies, using `candidates` (name -> plugin) as the resolution universe.
|
|
11
|
+
*
|
|
12
|
+
* Dependencies are emitted before the plugins that depend on them, with
|
|
13
|
+
* `rootName` last. Missing dependencies, version-constraint violations, and
|
|
14
|
+
* dependency cycles are reported as DependencyError.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveInstallOrder(rootName, candidates) {
|
|
17
|
+
if (!candidates.has(rootName)) {
|
|
18
|
+
throw new DependencyError(`plugin "${rootName}" not found among candidates`);
|
|
19
|
+
}
|
|
20
|
+
const order = [];
|
|
21
|
+
const visited = new Set(); // fully processed
|
|
22
|
+
const onStack = new Set(); // current DFS path (cycle detection)
|
|
23
|
+
const visit = (name, requiredBy, constraint) => {
|
|
24
|
+
const candidate = candidates.get(name);
|
|
25
|
+
if (!candidate) {
|
|
26
|
+
throw new DependencyError(`missing dependency "${name}"${requiredBy ? ` required by "${requiredBy}"` : ""}`);
|
|
27
|
+
}
|
|
28
|
+
if (constraint && !satisfies(candidate.manifest.version, constraint)) {
|
|
29
|
+
throw new DependencyError(`version conflict: "${requiredBy}" requires "${name}@${constraint}" but found ${candidate.manifest.version}`);
|
|
30
|
+
}
|
|
31
|
+
if (visited.has(name))
|
|
32
|
+
return;
|
|
33
|
+
if (onStack.has(name)) {
|
|
34
|
+
throw new DependencyError(`dependency cycle detected at "${name}"`);
|
|
35
|
+
}
|
|
36
|
+
onStack.add(name);
|
|
37
|
+
for (const dep of candidate.manifest.dependencies ?? []) {
|
|
38
|
+
visit(dep.name, name, dep.version);
|
|
39
|
+
}
|
|
40
|
+
onStack.delete(name);
|
|
41
|
+
visited.add(name);
|
|
42
|
+
order.push(name);
|
|
43
|
+
};
|
|
44
|
+
visit(rootName, null, null);
|
|
45
|
+
return order;
|
|
46
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { cpSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, relative } from "node:path";
|
|
3
|
+
export function ensureDir(dir) {
|
|
4
|
+
mkdirSync(dir, { recursive: true });
|
|
5
|
+
}
|
|
6
|
+
export function writeJson(file, value) {
|
|
7
|
+
ensureDir(dirname(file));
|
|
8
|
+
writeFileSync(file, JSON.stringify(value, null, 2) + "\n");
|
|
9
|
+
}
|
|
10
|
+
export function writeText(file, value) {
|
|
11
|
+
ensureDir(dirname(file));
|
|
12
|
+
writeFileSync(file, value);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Copy a plugin directory, skipping VCS and dependency noise. When `include` is
|
|
16
|
+
* given (a root-relative path predicate, e.g. from `packageFilter`), only the
|
|
17
|
+
* declared payload is copied — dev cruft like `src/`/`test/` is left behind.
|
|
18
|
+
*/
|
|
19
|
+
export function copyPluginDir(src, dest, include) {
|
|
20
|
+
ensureDir(dirname(dest));
|
|
21
|
+
cpSync(src, dest, {
|
|
22
|
+
recursive: true,
|
|
23
|
+
filter: (from) => {
|
|
24
|
+
const base = from.split(/[/\\]/).pop() ?? "";
|
|
25
|
+
if (base === ".git" || base === "node_modules" || base === ".DS_Store")
|
|
26
|
+
return false;
|
|
27
|
+
if (!include)
|
|
28
|
+
return true;
|
|
29
|
+
return include(relative(src, from));
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
package/dist/src/hash.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join, relative, sep } from "node:path";
|
|
4
|
+
/** Directory names that never contribute to a plugin's content hash. */
|
|
5
|
+
const HASH_IGNORE = new Set([".git", "node_modules", ".DS_Store"]);
|
|
6
|
+
/**
|
|
7
|
+
* Compute a deterministic content hash for a plugin directory.
|
|
8
|
+
*
|
|
9
|
+
* The hash incorporates each file's POSIX-normalized relative path and its
|
|
10
|
+
* bytes, sorted by path so the result is stable across filesystems and walk
|
|
11
|
+
* order. `extraIgnore` drops directory names by segment (e.g. generated adapter
|
|
12
|
+
* manifests). `include`, when given, restricts the hash to the packaged payload
|
|
13
|
+
* (root-relative path predicate, e.g. from `packageFilter`) so an in-place
|
|
14
|
+
* plugin and a copied install hash identically.
|
|
15
|
+
*/
|
|
16
|
+
export function folderHash(dir, extraIgnore = [], include) {
|
|
17
|
+
const ignore = new Set([...HASH_IGNORE, ...extraIgnore]);
|
|
18
|
+
const files = [];
|
|
19
|
+
collect(dir, dir, ignore, include, files);
|
|
20
|
+
files.sort();
|
|
21
|
+
const hash = createHash("sha256");
|
|
22
|
+
for (const rel of files) {
|
|
23
|
+
hash.update(rel, "utf8");
|
|
24
|
+
hash.update("\0");
|
|
25
|
+
hash.update(readFileSync(join(dir, rel)));
|
|
26
|
+
hash.update("\0");
|
|
27
|
+
}
|
|
28
|
+
// Self-describing digest (SRI-style) so the algorithm can evolve later.
|
|
29
|
+
return `sha256-${hash.digest("hex")}`;
|
|
30
|
+
}
|
|
31
|
+
function collect(root, current, ignore, include, out) {
|
|
32
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
33
|
+
if (ignore.has(entry.name))
|
|
34
|
+
continue;
|
|
35
|
+
const abs = join(current, entry.name);
|
|
36
|
+
const rel = relative(root, abs);
|
|
37
|
+
// Match ignore entries against any path segment (e.g. ".claude-plugin").
|
|
38
|
+
if (rel.split(sep).some((seg) => ignore.has(seg)))
|
|
39
|
+
continue;
|
|
40
|
+
const relPosix = rel.split(sep).join("/");
|
|
41
|
+
// Prune anything outside the packaged payload (directories too, by segment).
|
|
42
|
+
if (include && !include(relPosix))
|
|
43
|
+
continue;
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
collect(root, abs, ignore, include, out);
|
|
46
|
+
}
|
|
47
|
+
else if (entry.isFile()) {
|
|
48
|
+
out.push(relPosix);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/dist/src/lock.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { LOCK_VERSION } from "./types.js";
|
|
3
|
+
export function emptyLock() {
|
|
4
|
+
return { version: LOCK_VERSION, plugins: {} };
|
|
5
|
+
}
|
|
6
|
+
export function readLock(file) {
|
|
7
|
+
if (!existsSync(file))
|
|
8
|
+
return emptyLock();
|
|
9
|
+
const raw = JSON.parse(readFileSync(file, "utf8"));
|
|
10
|
+
if (typeof raw.version !== "number" || typeof raw.plugins !== "object" || raw.plugins === null) {
|
|
11
|
+
throw new Error(`${file} is not a valid .plugin-lock.json`);
|
|
12
|
+
}
|
|
13
|
+
// Pre-release policy: a lock from an older format version is fully
|
|
14
|
+
// regenerable from the plugin directories, so rebuild rather than merge
|
|
15
|
+
// incompatible entry shapes.
|
|
16
|
+
if (raw.version !== LOCK_VERSION)
|
|
17
|
+
return emptyLock();
|
|
18
|
+
return raw;
|
|
19
|
+
}
|
|
20
|
+
export function writeLock(file, lock) {
|
|
21
|
+
writeFileSync(file, JSON.stringify(lock, null, 2) + "\n");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Remove a plugin entry from the lock. Drops it from `lastSelected` too.
|
|
25
|
+
* Returns true if an entry was actually removed.
|
|
26
|
+
*/
|
|
27
|
+
export function removeEntry(lock, name) {
|
|
28
|
+
if (!(name in lock.plugins))
|
|
29
|
+
return false;
|
|
30
|
+
delete lock.plugins[name];
|
|
31
|
+
if (lock.lastSelected) {
|
|
32
|
+
lock.lastSelected = lock.lastSelected.filter((n) => n !== name);
|
|
33
|
+
if (lock.lastSelected.length === 0)
|
|
34
|
+
delete lock.lastSelected;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Insert or update a plugin entry. Preserves the original installedAt on update
|
|
40
|
+
* and always refreshes updatedAt. `lastSelected` is set to the touched plugin.
|
|
41
|
+
*/
|
|
42
|
+
export function upsertEntry(lock, name, entry, now = new Date().toISOString()) {
|
|
43
|
+
const prev = lock.plugins[name];
|
|
44
|
+
lock.plugins[name] = {
|
|
45
|
+
...entry,
|
|
46
|
+
installedAt: prev?.installedAt ?? now,
|
|
47
|
+
updatedAt: now,
|
|
48
|
+
};
|
|
49
|
+
lock.lastSelected = [name];
|
|
50
|
+
return lock;
|
|
51
|
+
}
|