@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,257 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { basename, join, relative, resolve } from "node:path";
|
|
4
|
+
import { ADAPTER_TARGETS } from "../adapters/index.js";
|
|
5
|
+
import { fromNativeManifest } from "../adapters/reverse.js";
|
|
6
|
+
import { adaptPlugin } from "./adapt.js";
|
|
7
|
+
import { copyPluginDir, writeJson } from "../fsutil.js";
|
|
8
|
+
import { folderHash } from "../hash.js";
|
|
9
|
+
import { packageFilter } from "../package.js";
|
|
10
|
+
import { lockPath, marketplacePath, marketplaceSourcePath, pluginDir } from "../paths.js";
|
|
11
|
+
import { readLock, upsertEntry, writeLock } from "../lock.js";
|
|
12
|
+
import { ADG_MANIFEST_PATH, readManifest } from "../manifest.js";
|
|
13
|
+
import { readMarketplace, upsertMarketplacePlugin, writeMarketplace } from "../marketplace.js";
|
|
14
|
+
import { resolveInstallOrder } from "../deps.js";
|
|
15
|
+
import { cloneGitHub, parseSource, scanNativePlugins, scanPlugins } from "../sources.js";
|
|
16
|
+
import { sameSource, COMPONENT_TYPES } from "../types.js";
|
|
17
|
+
import { pluginContents, presentComponents } from "../components.js";
|
|
18
|
+
import { skillDescriptionLoader } from "../skills.js";
|
|
19
|
+
import { resolveAgents } from "../agents/index.js";
|
|
20
|
+
const HASH_IGNORE = [".claude-plugin", ".codex-plugin"];
|
|
21
|
+
function toPosix(p) {
|
|
22
|
+
return p.split("\\").join("/");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Install a single local plugin directory into a plugins directory: copy the
|
|
26
|
+
* source, generate adapter manifests, compute the folder hash, and update both
|
|
27
|
+
* .plugin-lock.json and marketplace.json (with denormalized discovery metadata
|
|
28
|
+
* and an integrity digest).
|
|
29
|
+
*
|
|
30
|
+
* Refuses to overwrite a same-named plugin that came from a different upstream
|
|
31
|
+
* source (cross-marketplace name collision). Only files under `pluginsDir` are
|
|
32
|
+
* written — sibling files such as AGENTS.md or a global skills/ are untouched.
|
|
33
|
+
*/
|
|
34
|
+
export function installPlugin(opts) {
|
|
35
|
+
const source = resolve(opts.source);
|
|
36
|
+
const manifest = readManifest(source);
|
|
37
|
+
const name = manifest.name;
|
|
38
|
+
// Local installs stay flat; remote sources derive a per-marketplace dir from
|
|
39
|
+
// their origin. The default (no origin) is a flat local copy-in.
|
|
40
|
+
const origin = opts.origin ?? { type: "local", path: `./${toPosix(name)}` };
|
|
41
|
+
const dest = pluginDir(opts.pluginsDir, name, origin);
|
|
42
|
+
// When the source already lives at the destination (e.g. adapting an
|
|
43
|
+
// in-repo reference plugin) skip the copy to avoid copying onto itself.
|
|
44
|
+
// Only the manifest-declared payload is copied (projections included) — dev
|
|
45
|
+
// cruft like src/ or test/ never ships.
|
|
46
|
+
if (resolve(dest) !== source) {
|
|
47
|
+
copyPluginDir(source, dest, packageFilter(manifest, { includeProjections: true }));
|
|
48
|
+
}
|
|
49
|
+
const lockFile = lockPath(opts.pluginsDir);
|
|
50
|
+
const lock = readLock(lockFile);
|
|
51
|
+
const prev = lock.plugins[name];
|
|
52
|
+
if (prev && !sameSource(prev.origin, origin)) {
|
|
53
|
+
throw new Error(`name collision: "${name}" is already installed from a different source ` +
|
|
54
|
+
`(${describe(prev.origin)} vs ${describe(origin)}). Rename one to avoid the conflict.`);
|
|
55
|
+
}
|
|
56
|
+
// A new selection wins; otherwise keep whatever a prior install recorded so
|
|
57
|
+
// partial installs survive re-install / `marketplace upgrade`.
|
|
58
|
+
const selection = opts.selection ?? prev?.selection;
|
|
59
|
+
const targets = opts.targets ?? [...ADAPTER_TARGETS];
|
|
60
|
+
const adapted = adaptPlugin(dest, targets, selection).map((r) => r.file);
|
|
61
|
+
const hash = folderHash(dest, HASH_IGNORE, packageFilter(manifest, { includeProjections: false }));
|
|
62
|
+
const entry = {
|
|
63
|
+
origin,
|
|
64
|
+
version: manifest.version,
|
|
65
|
+
folderHash: hash,
|
|
66
|
+
};
|
|
67
|
+
if (manifest.dependencies?.length) {
|
|
68
|
+
entry.dependencies = Object.fromEntries(manifest.dependencies.map((d) => [d.name, d.version]));
|
|
69
|
+
}
|
|
70
|
+
if (selection)
|
|
71
|
+
entry.selection = selection;
|
|
72
|
+
upsertEntry(lock, name, entry, opts.now);
|
|
73
|
+
writeLock(lockFile, lock);
|
|
74
|
+
const marketFile = marketplacePath(opts.pluginsDir);
|
|
75
|
+
const fallbackName = opts.marketplaceName ?? basename(opts.pluginsDir);
|
|
76
|
+
const market = readMarketplace(marketFile, fallbackName);
|
|
77
|
+
// marketplace.json is a pure runtime-facing export in the de-facto shape;
|
|
78
|
+
// version/integrity/provenance live in the lock, not here.
|
|
79
|
+
upsertMarketplacePlugin(market, {
|
|
80
|
+
name,
|
|
81
|
+
source: { source: "local", path: marketplaceSourcePath(opts.pluginsDir, dest) },
|
|
82
|
+
policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
|
|
83
|
+
...(manifest.category ? { category: manifest.category } : {}),
|
|
84
|
+
});
|
|
85
|
+
writeMarketplace(marketFile, market);
|
|
86
|
+
return { name, installedTo: dest, folderHash: hash, adapted };
|
|
87
|
+
}
|
|
88
|
+
function describe(s) {
|
|
89
|
+
switch (s.type) {
|
|
90
|
+
case "local": return `local:${s.path}`;
|
|
91
|
+
case "github": return `github:${s.repo}${s.path ? `/${s.path}` : ""}`;
|
|
92
|
+
case "git": return `git:${s.url}${s.path ? `/${s.path}` : ""}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Reverse-adapt any native (Claude/Codex) manifests under `root` into
|
|
97
|
+
* `.agents/.plugin.json`, then return every ADG plugin found. After this the
|
|
98
|
+
* whole source speaks ADG, so selection and install treat all plugins uniformly.
|
|
99
|
+
*/
|
|
100
|
+
function discoverPlugins(root) {
|
|
101
|
+
const converted = [];
|
|
102
|
+
for (const native of scanNativePlugins(root)) {
|
|
103
|
+
if (native.kind === "adg")
|
|
104
|
+
continue;
|
|
105
|
+
const raw = JSON.parse(readFileSync(native.manifestFile, "utf8"));
|
|
106
|
+
const manifest = fromNativeManifest(raw, native.kind);
|
|
107
|
+
writeJson(join(native.dir, ADG_MANIFEST_PATH), manifest);
|
|
108
|
+
converted.push(manifest.name);
|
|
109
|
+
}
|
|
110
|
+
return { candidates: scanPlugins(root), converted };
|
|
111
|
+
}
|
|
112
|
+
/** Resolve which discovered plugins to install from the selection options. */
|
|
113
|
+
async function selectPluginNames(opts, candidates, workRoot, converted) {
|
|
114
|
+
const names = [...candidates.keys()];
|
|
115
|
+
if (opts.plugins?.length) {
|
|
116
|
+
const missing = opts.plugins.filter((p) => !candidates.has(p));
|
|
117
|
+
if (missing.length) {
|
|
118
|
+
throw new Error(`plugin(s) not found in source: ${missing.join(", ")}.\nAvailable: ${names.join(", ")}`);
|
|
119
|
+
}
|
|
120
|
+
return opts.plugins;
|
|
121
|
+
}
|
|
122
|
+
if (opts.path) {
|
|
123
|
+
const target = resolve(join(workRoot, opts.path));
|
|
124
|
+
const hit = [...candidates.values()].find((c) => resolve(c.dir) === target);
|
|
125
|
+
if (!hit)
|
|
126
|
+
throw new Error(`no plugin found at --path ${opts.path}`);
|
|
127
|
+
return [hit.manifest.name];
|
|
128
|
+
}
|
|
129
|
+
if (opts.all || candidates.size === 1)
|
|
130
|
+
return names;
|
|
131
|
+
if (opts.selectPlugins) {
|
|
132
|
+
const convertedSet = new Set(converted);
|
|
133
|
+
const choices = [...candidates.values()].map((c) => ({
|
|
134
|
+
name: c.manifest.name,
|
|
135
|
+
description: c.manifest.description,
|
|
136
|
+
native: convertedSet.has(c.manifest.name),
|
|
137
|
+
}));
|
|
138
|
+
return opts.selectPlugins(choices);
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`source "${opts.spec}" contains ${candidates.size} plugins: ${names.join(", ")}.\n` +
|
|
141
|
+
`Pick with --plugin <name> (repeatable), --all for everything, or run in a terminal to choose interactively.`);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Decide a partial-install selection for each user-chosen plugin.
|
|
145
|
+
*
|
|
146
|
+
* Precedence: explicit flags (--only / --skill) win and apply to every chosen
|
|
147
|
+
* plugin; otherwise an interactive gate asks whether to install in full, and if
|
|
148
|
+
* not, a per-plugin component picker runs (skipped for plugins with nothing
|
|
149
|
+
* meaningful to choose). No selection for a plugin = expose everything.
|
|
150
|
+
*/
|
|
151
|
+
async function resolveSelections(opts, selected, candidates) {
|
|
152
|
+
const selections = new Map();
|
|
153
|
+
if (opts.only || opts.skillsSubset) {
|
|
154
|
+
const flagSelection = {
|
|
155
|
+
components: opts.only ?? [...COMPONENT_TYPES],
|
|
156
|
+
...(opts.skillsSubset ? { skills: opts.skillsSubset } : {}),
|
|
157
|
+
};
|
|
158
|
+
for (const name of selected)
|
|
159
|
+
selections.set(name, flagSelection);
|
|
160
|
+
return selections;
|
|
161
|
+
}
|
|
162
|
+
if (!opts.confirmFull || !opts.selectComponents)
|
|
163
|
+
return selections; // non-interactive default: full
|
|
164
|
+
if (await opts.confirmFull(selected))
|
|
165
|
+
return selections; // user kept everything
|
|
166
|
+
for (const name of selected) {
|
|
167
|
+
const cand = candidates.get(name);
|
|
168
|
+
const contents = pluginContents(cand.dir, cand.manifest);
|
|
169
|
+
const present = presentComponents(contents);
|
|
170
|
+
// Nothing meaningful to pick: a lone category with at most one member.
|
|
171
|
+
if (present.length <= 1 && contents.skills.length <= 1)
|
|
172
|
+
continue;
|
|
173
|
+
const skillDescription = skillDescriptionLoader(cand.dir, cand.manifest);
|
|
174
|
+
selections.set(name, await opts.selectComponents({ name, contents, present, skillDescription }));
|
|
175
|
+
}
|
|
176
|
+
return selections;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The unified install entrypoint. Treats any source as a marketplace: clone or
|
|
180
|
+
* read it, discover every plugin (ADG plus reverse-adapted native), choose a
|
|
181
|
+
* subset (--all / --plugin / --path / sole plugin / interactive picker), then
|
|
182
|
+
* install the selection in dependency-first order.
|
|
183
|
+
*/
|
|
184
|
+
export async function addPlugins(opts) {
|
|
185
|
+
const parsed = parseSource(opts.spec);
|
|
186
|
+
let workRoot;
|
|
187
|
+
let buildOrigin;
|
|
188
|
+
let cleanup;
|
|
189
|
+
if (parsed.kind === "local") {
|
|
190
|
+
workRoot = resolve(parsed.dir);
|
|
191
|
+
buildOrigin = (dir) => ({ type: "local", path: `./${toPosix(relative(workRoot, dir)) || basename(dir)}` });
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const ref = opts.ref ?? parsed.ref;
|
|
195
|
+
const tmp = mkdtempSync(join(tmpdir(), "adg-clone-"));
|
|
196
|
+
cleanup = () => rmSync(tmp, { recursive: true, force: true });
|
|
197
|
+
cloneGitHub({ ...parsed, ref }, tmp, { sparse: opts.sparse, runner: opts.gitRunner });
|
|
198
|
+
workRoot = tmp;
|
|
199
|
+
buildOrigin = (dir) => ({
|
|
200
|
+
type: "github",
|
|
201
|
+
repo: parsed.source,
|
|
202
|
+
...(ref ? { ref } : {}),
|
|
203
|
+
path: toPosix(relative(tmp, dir)) || ".",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
const { candidates, converted } = discoverPlugins(workRoot);
|
|
208
|
+
if (candidates.size === 0) {
|
|
209
|
+
throw new Error(`no plugin found in "${opts.spec}" (no .agents/.plugin.json, .claude-plugin or .codex-plugin manifest).`);
|
|
210
|
+
}
|
|
211
|
+
const selected = await selectPluginNames(opts, candidates, workRoot, converted);
|
|
212
|
+
if (selected.length === 0)
|
|
213
|
+
throw new Error("no plugins selected");
|
|
214
|
+
// Resolve adapter targets after the plugin choice (lets a CLI agent picker
|
|
215
|
+
// run once we know what's being installed). undefined → installPlugin's all.
|
|
216
|
+
const targets = opts.targets ?? (opts.selectTargets ? await opts.selectTargets() : undefined);
|
|
217
|
+
// Partial-install selection per user-chosen plugin (auto-deps install full).
|
|
218
|
+
const selections = await resolveSelections(opts, selected, candidates);
|
|
219
|
+
// Dependency-first order across every selected plugin (chains deduped).
|
|
220
|
+
const order = [];
|
|
221
|
+
const seen = new Set();
|
|
222
|
+
for (const name of selected) {
|
|
223
|
+
const chain = opts.withDeps === false ? [name] : resolveInstallOrder(name, candidates);
|
|
224
|
+
for (const n of chain) {
|
|
225
|
+
if (!seen.has(n)) {
|
|
226
|
+
seen.add(n);
|
|
227
|
+
order.push(n);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const installed = [];
|
|
232
|
+
for (const name of order) {
|
|
233
|
+
const candidate = candidates.get(name);
|
|
234
|
+
installed.push(installPlugin({
|
|
235
|
+
source: candidate.dir,
|
|
236
|
+
pluginsDir: opts.pluginsDir,
|
|
237
|
+
origin: buildOrigin(candidate.dir),
|
|
238
|
+
marketplaceName: opts.marketplaceName,
|
|
239
|
+
targets,
|
|
240
|
+
selection: selections.get(name),
|
|
241
|
+
now: opts.now,
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
// Activate into the selected agents so the plugins are actually usable, not
|
|
245
|
+
// just recorded/discoverable — each agent enables them via its own CLI.
|
|
246
|
+
// undefined targets = all registered agents.
|
|
247
|
+
let agents;
|
|
248
|
+
if (opts.activate) {
|
|
249
|
+
const ctx = { pluginsDir: opts.pluginsDir, plugins: installed.map((r) => r.name), scope: opts.scope ?? "project" };
|
|
250
|
+
agents = (opts.agents ?? resolveAgents(targets)).map((a) => a.activate(ctx));
|
|
251
|
+
}
|
|
252
|
+
return { order, installed, converted, available: [...candidates.keys()], agents };
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
cleanup?.();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { adaptPlugin } from "./adapt.js";
|
|
3
|
+
import { listPlugins } from "./list.js";
|
|
4
|
+
import { installedPluginDir } from "../paths.js";
|
|
5
|
+
import { getAgent } from "../agents/index.js";
|
|
6
|
+
/**
|
|
7
|
+
* Project installed plugins into an agent: (re)generate that agent's manifest
|
|
8
|
+
* from each plugin (honoring its partial-install selection), then enable the
|
|
9
|
+
* plugins through the agent's CLI. The pure manifest transform comes from
|
|
10
|
+
* `ADAPTERS[agent.adaptTarget]`; the enable step is the agent's `activate`.
|
|
11
|
+
*/
|
|
12
|
+
export function linkPlugins(opts) {
|
|
13
|
+
const agent = opts.agent ?? getAgent(opts.target);
|
|
14
|
+
const adaptTarget = agent?.adaptTarget ?? opts.target;
|
|
15
|
+
const actions = [];
|
|
16
|
+
for (const p of listPlugins(opts.pluginsDir)) {
|
|
17
|
+
const dir = installedPluginDir(opts.pluginsDir, p.name, p.origin);
|
|
18
|
+
if (!existsSync(dir))
|
|
19
|
+
continue;
|
|
20
|
+
const adapted = adaptPlugin(dir, [adaptTarget], p.selection).map((r) => r.file);
|
|
21
|
+
actions.push({ name: p.name, adapted });
|
|
22
|
+
}
|
|
23
|
+
if (!agent)
|
|
24
|
+
return { target: opts.target, actions };
|
|
25
|
+
const res = agent.activate({
|
|
26
|
+
pluginsDir: opts.pluginsDir,
|
|
27
|
+
plugins: actions.map((a) => a.name),
|
|
28
|
+
scope: opts.global ? "user" : "project",
|
|
29
|
+
});
|
|
30
|
+
for (const a of actions)
|
|
31
|
+
if (res.affected.includes(a.name))
|
|
32
|
+
a.linkedTo = agent.displayName;
|
|
33
|
+
return { target: opts.target, actions, cliSkipped: res.skipped };
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { installedPluginDir, lockPath } from "../paths.js";
|
|
2
|
+
import { readLock } from "../lock.js";
|
|
3
|
+
import { readManifest } from "../manifest.js";
|
|
4
|
+
import { exposedContents, pluginContents } from "../components.js";
|
|
5
|
+
/** List plugins recorded in a plugins directory's .plugin-lock.json. */
|
|
6
|
+
export function listPlugins(pluginsDir) {
|
|
7
|
+
const lock = readLock(lockPath(pluginsDir));
|
|
8
|
+
return Object.entries(lock.plugins).map(([name, entry]) => {
|
|
9
|
+
const dir = installedPluginDir(pluginsDir, name, entry.origin);
|
|
10
|
+
let contents;
|
|
11
|
+
try {
|
|
12
|
+
contents = exposedContents(pluginContents(dir, readManifest(dir)), entry.selection);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
contents = undefined; // no/invalid manifest on disk — show provenance only
|
|
16
|
+
}
|
|
17
|
+
return { name, ...entry, contents };
|
|
18
|
+
});
|
|
19
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { lockPath } from "../paths.js";
|
|
2
|
+
import { readLock } from "../lock.js";
|
|
3
|
+
import { addPlugins } from "./install.js";
|
|
4
|
+
import { removePlugin } from "./remove.js";
|
|
5
|
+
/**
|
|
6
|
+
* A stable key identifying the source a plugin came from. GitHub/git plugins
|
|
7
|
+
* group by repo/url (the marketplace root, ignoring per-plugin sub-paths);
|
|
8
|
+
* local installs share a single "(local)" bucket since their original source
|
|
9
|
+
* directory isn't recoverable for re-sync.
|
|
10
|
+
*/
|
|
11
|
+
export function sourceKey(origin) {
|
|
12
|
+
switch (origin.type) {
|
|
13
|
+
case "github":
|
|
14
|
+
return origin.repo;
|
|
15
|
+
case "git":
|
|
16
|
+
return origin.url;
|
|
17
|
+
case "local":
|
|
18
|
+
return "(local)";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/** True for sources that can be re-fetched (have a recoverable remote spec). */
|
|
22
|
+
function isRemoteKey(key) {
|
|
23
|
+
return key !== "(local)";
|
|
24
|
+
}
|
|
25
|
+
/** Group installed plugins by source, read straight from the lock. */
|
|
26
|
+
export function marketplaceList(opts) {
|
|
27
|
+
const lock = readLock(lockPath(opts.pluginsDir));
|
|
28
|
+
const groups = new Map();
|
|
29
|
+
for (const [name, entry] of Object.entries(lock.plugins)) {
|
|
30
|
+
const key = sourceKey(entry.origin);
|
|
31
|
+
const ref = entry.origin.type === "github" || entry.origin.type === "git" ? entry.origin.ref : undefined;
|
|
32
|
+
const g = groups.get(key) ?? { ref, installed: [] };
|
|
33
|
+
g.installed.push(name);
|
|
34
|
+
if (g.ref === undefined && ref !== undefined)
|
|
35
|
+
g.ref = ref;
|
|
36
|
+
groups.set(key, g);
|
|
37
|
+
}
|
|
38
|
+
return [...groups.entries()]
|
|
39
|
+
.map(([source, g]) => ({
|
|
40
|
+
source,
|
|
41
|
+
...(g.ref ? { ref: g.ref } : {}),
|
|
42
|
+
installed: g.installed.sort(),
|
|
43
|
+
remote: isRemoteKey(source),
|
|
44
|
+
}))
|
|
45
|
+
.sort((a, b) => a.source.localeCompare(b.source));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Find one source group by key, or throw with the available keys. The error is
|
|
49
|
+
* scope-aware: it names the scope/path it searched, and — when the source isn't
|
|
50
|
+
* here but exists globally — suggests re-running with `-g`.
|
|
51
|
+
*/
|
|
52
|
+
function requireGroup(pluginsDir, source, scope) {
|
|
53
|
+
const groups = marketplaceList({ pluginsDir });
|
|
54
|
+
const hit = groups.find((g) => g.source === source);
|
|
55
|
+
if (hit)
|
|
56
|
+
return hit;
|
|
57
|
+
const keys = groups.map((g) => g.source).join(", ") || "(none)";
|
|
58
|
+
const where = scope?.label ? `${scope.label}: ${pluginsDir}` : pluginsDir;
|
|
59
|
+
let msg = `no installed source "${source}" in ${where}. Known sources: ${keys}`;
|
|
60
|
+
if (scope?.globalDir && scope.globalDir !== pluginsDir) {
|
|
61
|
+
const inGlobal = marketplaceList({ pluginsDir: scope.globalDir }).some((g) => g.source === source);
|
|
62
|
+
if (inGlobal)
|
|
63
|
+
msg += ` — found in global; did you mean \`-g\`?`;
|
|
64
|
+
}
|
|
65
|
+
throw new Error(msg);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Re-fetch a source and update the plugins installed from it. By default only
|
|
69
|
+
* already-installed plugins are refreshed; `all` also installs everything new
|
|
70
|
+
* the source now offers. With no `source`, every remote source is upgraded.
|
|
71
|
+
*/
|
|
72
|
+
export async function marketplaceUpgrade(opts) {
|
|
73
|
+
const groups = opts.source
|
|
74
|
+
? [requireGroup(opts.pluginsDir, opts.source, opts.scope)]
|
|
75
|
+
: marketplaceList({ pluginsDir: opts.pluginsDir }).filter((g) => g.remote);
|
|
76
|
+
if (opts.source && !groups[0].remote) {
|
|
77
|
+
throw new Error(`source "${opts.source}" is local and cannot be re-synced; re-run \`adg plugins add\`.`);
|
|
78
|
+
}
|
|
79
|
+
if (groups.length === 0) {
|
|
80
|
+
throw new Error(`no remote sources installed in ${opts.pluginsDir}`);
|
|
81
|
+
}
|
|
82
|
+
const now = opts.now ?? new Date().toISOString();
|
|
83
|
+
const results = [];
|
|
84
|
+
for (const group of groups) {
|
|
85
|
+
const { installed, converted, available } = await addPlugins({
|
|
86
|
+
spec: group.source,
|
|
87
|
+
pluginsDir: opts.pluginsDir,
|
|
88
|
+
ref: group.ref,
|
|
89
|
+
// Default: refresh what's installed. --all: install everything too.
|
|
90
|
+
...(opts.all ? { all: true } : { plugins: group.installed }),
|
|
91
|
+
targets: opts.targets,
|
|
92
|
+
marketplaceName: group.source,
|
|
93
|
+
gitRunner: opts.gitRunner,
|
|
94
|
+
// Re-activate so the agents pick up the upgraded content, not just the store.
|
|
95
|
+
activate: opts.activate,
|
|
96
|
+
scope: opts.agentScope,
|
|
97
|
+
now,
|
|
98
|
+
});
|
|
99
|
+
const installedSet = new Set(group.installed);
|
|
100
|
+
results.push({
|
|
101
|
+
source: group.source,
|
|
102
|
+
updated: installed,
|
|
103
|
+
converted,
|
|
104
|
+
available: available.filter((n) => !installedSet.has(n)),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return results;
|
|
108
|
+
}
|
|
109
|
+
/** Uninstall every plugin that came from a given source (and from the agents). */
|
|
110
|
+
export function marketplaceRemove(opts) {
|
|
111
|
+
const group = requireGroup(opts.pluginsDir, opts.source, opts.scope);
|
|
112
|
+
const removed = [];
|
|
113
|
+
for (const name of group.installed) {
|
|
114
|
+
removePlugin({
|
|
115
|
+
pluginsDir: opts.pluginsDir,
|
|
116
|
+
name,
|
|
117
|
+
force: opts.force,
|
|
118
|
+
deactivate: opts.deactivate,
|
|
119
|
+
scope: opts.agentScope,
|
|
120
|
+
});
|
|
121
|
+
removed.push(name);
|
|
122
|
+
}
|
|
123
|
+
return { source: opts.source, removed };
|
|
124
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, renameSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { lockPath, marketplacePath, marketplaceSourcePath, pluginDir } from "../paths.js";
|
|
4
|
+
import { readLock } from "../lock.js";
|
|
5
|
+
import { readMarketplace, upsertMarketplacePlugin, writeMarketplace } from "../marketplace.js";
|
|
6
|
+
/**
|
|
7
|
+
* Migrate a flat plugins directory to the per-marketplace nested layout.
|
|
8
|
+
*
|
|
9
|
+
* For every locked plugin, move `<pluginsDir>/<name>` to the origin-derived
|
|
10
|
+
* `<pluginsDir>/<segment>/<name>` (remote sources only; local installs stay
|
|
11
|
+
* flat) and rewrite its marketplace.json `source.path` to match. Idempotent:
|
|
12
|
+
* plugins already at their target path are reported as unchanged.
|
|
13
|
+
*/
|
|
14
|
+
export function migrateLayout(pluginsDir) {
|
|
15
|
+
const lock = readLock(lockPath(pluginsDir));
|
|
16
|
+
const moved = [];
|
|
17
|
+
const unchanged = [];
|
|
18
|
+
const missing = [];
|
|
19
|
+
const marketFile = marketplacePath(pluginsDir);
|
|
20
|
+
const market = readMarketplace(marketFile, "");
|
|
21
|
+
let marketDirty = false;
|
|
22
|
+
for (const [name, entry] of Object.entries(lock.plugins)) {
|
|
23
|
+
const flat = join(pluginsDir, name);
|
|
24
|
+
const target = pluginDir(pluginsDir, name, entry.origin);
|
|
25
|
+
if (target === flat) {
|
|
26
|
+
unchanged.push(name);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (existsSync(target)) {
|
|
30
|
+
// Already migrated; nothing to move, but make sure the export agrees.
|
|
31
|
+
if (rewriteMarketplacePath(market, name, pluginsDir, target))
|
|
32
|
+
marketDirty = true;
|
|
33
|
+
unchanged.push(name);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (!existsSync(flat)) {
|
|
37
|
+
missing.push(name);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
41
|
+
renameSync(flat, target);
|
|
42
|
+
if (rewriteMarketplacePath(market, name, pluginsDir, target))
|
|
43
|
+
marketDirty = true;
|
|
44
|
+
moved.push({ name, from: flat, to: target });
|
|
45
|
+
}
|
|
46
|
+
if (marketDirty)
|
|
47
|
+
writeMarketplace(marketFile, market);
|
|
48
|
+
return { moved, unchanged, missing };
|
|
49
|
+
}
|
|
50
|
+
/** Point a marketplace entry's `source.path` at the plugin's on-disk dir. */
|
|
51
|
+
function rewriteMarketplacePath(market, name, pluginsDir, dir) {
|
|
52
|
+
const existing = market.plugins.find((p) => p.name === name);
|
|
53
|
+
if (!existing)
|
|
54
|
+
return false;
|
|
55
|
+
const path = marketplaceSourcePath(pluginsDir, dir);
|
|
56
|
+
if (existing.source.path === path)
|
|
57
|
+
return false;
|
|
58
|
+
upsertMarketplacePlugin(market, { ...existing, source: { ...existing.source, path } });
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { MultiSelectPrompt } from "@clack/core";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
/**
|
|
4
|
+
* A skills multiselect with an on-demand description toggle. `@clack/prompts`'
|
|
5
|
+
* `multiselect` can't intercept extra keys, so this builds directly on
|
|
6
|
+
* `@clack/core`'s `MultiSelectPrompt` and replicates clack's chrome, adding a
|
|
7
|
+
* `d` key that lazily loads and inlines each skill's description as a hint.
|
|
8
|
+
*
|
|
9
|
+
* Descriptions are read only while the toggle is on (and cached by the loader),
|
|
10
|
+
* so a list of many skills costs nothing until the user actually asks for them.
|
|
11
|
+
*/
|
|
12
|
+
// Match @clack/prompts' unicode-aware glyphs so the picker is visually seamless.
|
|
13
|
+
const unicode = process.platform !== "win32" ||
|
|
14
|
+
Boolean(process.env.WT_SESSION) ||
|
|
15
|
+
process.env.TERM_PROGRAM === "vscode" ||
|
|
16
|
+
process.env.TERM === "xterm-256color";
|
|
17
|
+
const g = (a, b) => (unicode ? a : b);
|
|
18
|
+
const S_BAR = g("│", "|");
|
|
19
|
+
const S_BAR_END = g("└", "—");
|
|
20
|
+
const S_STEP_ACTIVE = g("◆", "*");
|
|
21
|
+
const S_STEP_SUBMIT = g("◇", "o");
|
|
22
|
+
const S_STEP_CANCEL = g("■", "x");
|
|
23
|
+
const S_CHECK_ACTIVE = g("◻", "[•]");
|
|
24
|
+
const S_CHECK_SELECTED = g("◼", "[+]");
|
|
25
|
+
const S_CHECK_INACTIVE = g("◻", "[ ]");
|
|
26
|
+
/** Format one option row, appending the description as a dim hint when present. */
|
|
27
|
+
export function formatSkillRow(label, hint, kind) {
|
|
28
|
+
const tail = hint ? ` ${pc.dim(hint)}` : "";
|
|
29
|
+
switch (kind) {
|
|
30
|
+
case "active":
|
|
31
|
+
return `${pc.cyan(S_CHECK_ACTIVE)} ${label}${tail}`;
|
|
32
|
+
case "active-selected":
|
|
33
|
+
return `${pc.green(S_CHECK_SELECTED)} ${label}${tail}`;
|
|
34
|
+
case "selected":
|
|
35
|
+
return `${pc.green(S_CHECK_SELECTED)} ${pc.dim(label)}${tail}`;
|
|
36
|
+
default:
|
|
37
|
+
return `${pc.dim(S_CHECK_INACTIVE)} ${pc.dim(label)}`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Build the rendered option rows. Descriptions are resolved only when
|
|
42
|
+
* `showDesc` is true, so the picker performs zero SKILL.md reads until the user
|
|
43
|
+
* presses `d`. Exported as the pure core of the picker for direct testing.
|
|
44
|
+
*/
|
|
45
|
+
export function buildSkillRows(options, state) {
|
|
46
|
+
return options.map((o, i) => {
|
|
47
|
+
const sel = state.selected.includes(o.value);
|
|
48
|
+
const active = i === state.cursor;
|
|
49
|
+
const hint = state.showDesc ? state.loadDescription(o.value) : undefined;
|
|
50
|
+
const kind = active && sel ? "active-selected" : sel ? "selected" : active ? "active" : "inactive";
|
|
51
|
+
return formatSkillRow(o.label, hint, kind);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Resolves to the chosen values, or a cancel symbol (use `isCancel` to detect). */
|
|
55
|
+
export function multiselectSkills(opts) {
|
|
56
|
+
let showDesc = false;
|
|
57
|
+
const symbol = (state) => {
|
|
58
|
+
if (state === "submit")
|
|
59
|
+
return pc.green(S_STEP_SUBMIT);
|
|
60
|
+
if (state === "cancel")
|
|
61
|
+
return pc.red(S_STEP_CANCEL);
|
|
62
|
+
return pc.cyan(S_STEP_ACTIVE);
|
|
63
|
+
};
|
|
64
|
+
const footer = () => pc.dim(`space ${pc.gray("toggle")} · a ${pc.gray("all")} · d ${pc.gray(showDesc ? "hide" : "show")} descriptions`);
|
|
65
|
+
const prompt = new MultiSelectPrompt({
|
|
66
|
+
input: opts.input,
|
|
67
|
+
output: opts.output,
|
|
68
|
+
options: opts.options,
|
|
69
|
+
initialValues: opts.initialValues,
|
|
70
|
+
required: true,
|
|
71
|
+
validate(value) {
|
|
72
|
+
if (this.required && Array.isArray(value) && value.length === 0) {
|
|
73
|
+
return "Please select at least one option.";
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
},
|
|
77
|
+
render() {
|
|
78
|
+
const head = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
|
|
79
|
+
if (this.state === "submit" || this.state === "cancel") {
|
|
80
|
+
const chosen = this.options
|
|
81
|
+
.filter((o) => this.value.includes(o.value))
|
|
82
|
+
.map((o) => pc.dim(o.label))
|
|
83
|
+
.join(pc.dim(", "));
|
|
84
|
+
return `${head}${pc.gray(S_BAR)} ${chosen || pc.dim("none")}`;
|
|
85
|
+
}
|
|
86
|
+
const rows = buildSkillRows(this.options, {
|
|
87
|
+
cursor: this.cursor,
|
|
88
|
+
selected: this.value,
|
|
89
|
+
showDesc,
|
|
90
|
+
loadDescription: opts.loadDescription,
|
|
91
|
+
}).join(`\n${pc.cyan(S_BAR)} `);
|
|
92
|
+
return `${head}${pc.cyan(S_BAR)} ${rows}\n${pc.cyan(S_BAR)} ${footer()}\n${pc.cyan(S_BAR_END)}\n`;
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
// `a` (toggle-all) is bound by MultiSelectPrompt itself; `d` is free for us.
|
|
96
|
+
// Touching showDesc here re-renders on the same keypress (onKeypress renders
|
|
97
|
+
// after emitting "key"), so the first `d` press shows descriptions at once.
|
|
98
|
+
prompt.on("key", (c) => {
|
|
99
|
+
if (c === "d")
|
|
100
|
+
showDesc = !showDesc;
|
|
101
|
+
});
|
|
102
|
+
return prompt.prompt();
|
|
103
|
+
}
|