@yagni-app/code 1.0.6 → 1.0.8
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/README.md +91 -46
- package/dist/claudeCompat.d.ts +59 -0
- package/dist/claudeCompat.js +109 -2
- package/dist/claudePlugins.d.ts +45 -5
- package/dist/claudePlugins.js +129 -21
- package/dist/cli.js +16 -4
- package/dist/doctor.d.ts +21 -8
- package/dist/doctor.js +53 -28
- package/dist/extension/askUserQuestionTool.js +7 -2
- package/dist/extension/condensedTools.d.ts +12 -1
- package/dist/extension/condensedTools.js +17 -9
- package/dist/extension/config.d.ts +6 -0
- package/dist/extension/hooks.d.ts +3 -3
- package/dist/extension/hooks.js +30 -5
- package/dist/extension/index.d.ts +19 -0
- package/dist/extension/index.js +248 -43
- package/dist/extension/mcp/cliConfig.d.ts +1 -1
- package/dist/extension/mcp/cliConfig.js +1 -1
- package/dist/extension/mcp/config.d.ts +24 -2
- package/dist/extension/mcp/config.js +75 -3
- package/dist/extension/mcp/manager.d.ts +3 -1
- package/dist/extension/mcp/manager.js +2 -2
- package/dist/extension/mcp/panel.d.ts +0 -1
- package/dist/extension/mcp/panel.js +13 -3
- package/dist/extension/mcp/startup.js +8 -6
- package/dist/extension/permission/gate.d.ts +101 -2
- package/dist/extension/permission/gate.js +485 -42
- package/dist/extension/permissionRules/bashFileArgs.d.ts +39 -0
- package/dist/extension/permissionRules/bashFileArgs.js +236 -0
- package/dist/extension/permissionRules/engine.d.ts +50 -0
- package/dist/extension/permissionRules/engine.js +238 -0
- package/dist/extension/permissionRules/loadConfig.d.ts +64 -0
- package/dist/extension/permissionRules/loadConfig.js +105 -0
- package/dist/extension/permissionRules/parser.d.ts +38 -0
- package/dist/extension/permissionRules/parser.js +136 -0
- package/dist/extension/permissionRules/pathRules.d.ts +60 -0
- package/dist/extension/permissionRules/pathRules.js +122 -0
- package/dist/extension/permissionRules/shellRules.d.ts +52 -0
- package/dist/extension/permissionRules/shellRules.js +221 -0
- package/dist/extension/pipeline/invocation.d.ts +3 -6
- package/dist/extension/pipeline/invocation.js +3 -6
- package/dist/extension/pipeline/runner.d.ts +0 -1
- package/dist/extension/pipeline/runner.js +6 -14
- package/dist/extension/plugins/inventory.d.ts +88 -0
- package/dist/extension/plugins/inventory.js +144 -0
- package/dist/extension/plugins/panel.d.ts +45 -0
- package/dist/extension/plugins/panel.js +293 -0
- package/dist/extension/sandbox/bash.d.ts +99 -0
- package/dist/extension/sandbox/bash.js +190 -0
- package/dist/extension/sandbox/config.d.ts +115 -0
- package/dist/extension/sandbox/config.js +388 -0
- package/dist/extension/sandbox/manager.d.ts +108 -0
- package/dist/extension/sandbox/manager.js +243 -0
- package/dist/extension/sandbox/panel.d.ts +111 -0
- package/dist/extension/sandbox/panel.js +342 -0
- package/dist/extension/sandbox/session.d.ts +85 -0
- package/dist/extension/sandbox/session.js +829 -0
- package/dist/extension/settingsFiles.d.ts +50 -0
- package/dist/extension/settingsFiles.js +206 -0
- package/dist/extension/telemetry/attrs.d.ts +96 -0
- package/dist/extension/telemetry/attrs.js +149 -0
- package/dist/extension/telemetry/config.d.ts +103 -0
- package/dist/extension/telemetry/config.js +193 -0
- package/dist/extension/telemetry/index.d.ts +7 -0
- package/dist/extension/telemetry/index.js +7 -0
- package/dist/extension/telemetry/probe.d.ts +29 -0
- package/dist/extension/telemetry/probe.js +122 -0
- package/dist/extension/telemetry/register.d.ts +47 -0
- package/dist/extension/telemetry/register.js +207 -0
- package/dist/extension/telemetry/sdk.d.ts +63 -0
- package/dist/extension/telemetry/sdk.js +207 -0
- package/dist/extension/telemetry/tracker.d.ts +131 -0
- package/dist/extension/telemetry/tracker.js +551 -0
- package/dist/extension/vendor/IGNORE-LICENSE-MIT +21 -0
- package/dist/extension/vendor/ignore.d.ts +86 -0
- package/dist/extension/vendor/ignore.js +788 -0
- package/dist/goHeadless.d.ts +1 -1
- package/dist/goHeadless.js +2 -2
- package/dist/launch.d.ts +4 -3
- package/dist/launch.js +7 -4
- package/dist/mcpCommand.d.ts +10 -1
- package/dist/mcpCommand.js +42 -10
- package/dist/otel.d.ts +67 -90
- package/dist/otel.js +152 -195
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +18 -0
- package/dist/pluginCommand.d.ts +43 -0
- package/dist/pluginCommand.js +499 -0
- package/dist/pluginStore.d.ts +170 -0
- package/dist/pluginStore.js +554 -0
- package/dist/upgrade.js +10 -1
- package/package.json +19 -3
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `yagni plugin …` — install, remove, and update Claude Code plugins from
|
|
3
|
+
* marketplaces without leaving YAGNI Code, mirroring Claude Code's own
|
|
4
|
+
* `claude plugin` surface (marketplace add/list/update/remove, install,
|
|
5
|
+
* uninstall, enable, disable, update, list).
|
|
6
|
+
*
|
|
7
|
+
* State lives in YAGNI's own ledger (pluginStore.ts). Plugins Claude Code
|
|
8
|
+
* installed are still mirrored read-only (claudePlugins.ts) and show up in
|
|
9
|
+
* `list` labelled as such, so a developer sees one honest inventory: what
|
|
10
|
+
* loads, where it came from, and which components (hooks) are not bridged.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
13
|
+
import { homedir } from "node:os";
|
|
14
|
+
import { delimiter, dirname, isAbsolute, join, sep } from "node:path";
|
|
15
|
+
import { pluginInventory, readTrustDecision } from "./claudeCompat.js";
|
|
16
|
+
import { discoverClaudePlugins } from "./claudePlugins.js";
|
|
17
|
+
import { agentDir } from "./credentials.js";
|
|
18
|
+
import { DISTRIBUTION } from "./distribution.js";
|
|
19
|
+
import { PluginStoreError, addMarketplace, catalog, describeSource, installPlugin, parseMarketplaceSource, pluginsHome, pruneCache, readKnownMarketplaces, readLedger, removeMarketplace, setPluginEnabled, uninstallPlugin, updateMarketplace, updatePlugins, } from "./pluginStore.js";
|
|
20
|
+
import { getActiveProfileName } from "./profiles.js";
|
|
21
|
+
const cmd = DISTRIBUTION.commandName;
|
|
22
|
+
const USAGE = `Usage:
|
|
23
|
+
${cmd} plugin marketplace add <owner/repo | git-url | path>
|
|
24
|
+
${cmd} plugin marketplace list
|
|
25
|
+
${cmd} plugin marketplace update [name]
|
|
26
|
+
${cmd} plugin marketplace remove <name>
|
|
27
|
+
${cmd} plugin install <plugin>[@marketplace] [-s user|project]
|
|
28
|
+
${cmd} plugin uninstall <plugin>[@marketplace] [-s user|project]
|
|
29
|
+
${cmd} plugin enable <plugin>[@marketplace]
|
|
30
|
+
${cmd} plugin disable <plugin>[@marketplace]
|
|
31
|
+
${cmd} plugin update [<plugin>[@marketplace]]
|
|
32
|
+
${cmd} plugin list [--available]
|
|
33
|
+
${cmd} plugin inventory --json (machine-readable; backs the in-session /plugin panel)
|
|
34
|
+
|
|
35
|
+
In a session: /plugin opens the same panel; /reload-plugins applies changes
|
|
36
|
+
without restarting.
|
|
37
|
+
|
|
38
|
+
Scopes (install/uninstall):
|
|
39
|
+
user available in all your projects (default)
|
|
40
|
+
project only inside this repo
|
|
41
|
+
|
|
42
|
+
What loads from a plugin: skills, commands, agents, and MCP servers
|
|
43
|
+
(.mcp.json at the plugin root). Hooks and LSP servers are not bridged;
|
|
44
|
+
\`plugin list\` says so per plugin. State: ~/.yagni-code/plugins.
|
|
45
|
+
Plugins installed with Claude Code itself are picked up too (read-only).
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
${cmd} plugin marketplace add acme/claude-plugins
|
|
49
|
+
${cmd} plugin install code-review@acme-plugins
|
|
50
|
+
${cmd} plugin update
|
|
51
|
+
`;
|
|
52
|
+
export function parsePluginArgs(args) {
|
|
53
|
+
const out = {
|
|
54
|
+
subcommand: undefined,
|
|
55
|
+
positionals: [],
|
|
56
|
+
scope: "user",
|
|
57
|
+
scopeExplicit: false,
|
|
58
|
+
available: false,
|
|
59
|
+
json: false,
|
|
60
|
+
};
|
|
61
|
+
const scopeFrom = (value) => {
|
|
62
|
+
if (value === "user" || value === "project")
|
|
63
|
+
return value;
|
|
64
|
+
throw new Error(`Invalid scope "${value ?? ""}" — expected user or project.`);
|
|
65
|
+
};
|
|
66
|
+
for (let i = 0; i < args.length; i++) {
|
|
67
|
+
const arg = args[i];
|
|
68
|
+
if (arg === "-s" || arg === "--scope") {
|
|
69
|
+
out.scope = scopeFrom(args[++i]);
|
|
70
|
+
out.scopeExplicit = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (arg.startsWith("--scope=")) {
|
|
74
|
+
out.scope = scopeFrom(arg.slice("--scope=".length));
|
|
75
|
+
out.scopeExplicit = true;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (arg === "--available" || arg === "-a") {
|
|
79
|
+
out.available = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (arg === "--json") {
|
|
83
|
+
out.json = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (arg === "-h" || arg === "--help") {
|
|
87
|
+
out.subcommand = "help";
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (arg.startsWith("-"))
|
|
91
|
+
throw new Error(`Unknown option "${arg}".`);
|
|
92
|
+
if (out.subcommand === undefined)
|
|
93
|
+
out.subcommand = arg;
|
|
94
|
+
else
|
|
95
|
+
out.positionals.push(arg);
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
/** The repo root a project-scope install applies to: nearest `.git` ancestor, else cwd. */
|
|
100
|
+
export function resolveRepoRoot(cwd) {
|
|
101
|
+
let current = cwd;
|
|
102
|
+
for (;;) {
|
|
103
|
+
if (existsSync(join(current, ".git")))
|
|
104
|
+
return current;
|
|
105
|
+
const parent = dirname(current);
|
|
106
|
+
if (parent === current)
|
|
107
|
+
return cwd;
|
|
108
|
+
current = parent;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export async function pluginCommand(args, deps = {}) {
|
|
112
|
+
const stdout = deps.stdout ?? ((t) => process.stdout.write(t));
|
|
113
|
+
const stderr = deps.stderr ?? ((t) => process.stderr.write(t));
|
|
114
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
115
|
+
const home = deps.home ?? homedir();
|
|
116
|
+
const env = deps.env ?? process.env;
|
|
117
|
+
const io = {
|
|
118
|
+
cwd,
|
|
119
|
+
home,
|
|
120
|
+
env,
|
|
121
|
+
store: { home: pluginsHome(env, home), ...(deps.git ? { git: deps.git } : {}), ...(deps.now ? { now: deps.now } : {}) },
|
|
122
|
+
stdout,
|
|
123
|
+
stderr,
|
|
124
|
+
agentDir: deps.agentDir ?? (async () => agentDir(await getActiveProfileName())),
|
|
125
|
+
};
|
|
126
|
+
let parsed;
|
|
127
|
+
try {
|
|
128
|
+
parsed = parsePluginArgs(args);
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
stderr(`${err instanceof Error ? err.message : String(err)}\n${USAGE}`);
|
|
132
|
+
return 1;
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
switch (parsed.subcommand) {
|
|
136
|
+
case undefined:
|
|
137
|
+
case "help":
|
|
138
|
+
stdout(USAGE);
|
|
139
|
+
return 0;
|
|
140
|
+
case "marketplace":
|
|
141
|
+
return await marketplaceCommand(parsed, io);
|
|
142
|
+
case "install":
|
|
143
|
+
case "add":
|
|
144
|
+
return await install(parsed, io);
|
|
145
|
+
case "uninstall":
|
|
146
|
+
case "remove":
|
|
147
|
+
return uninstall(parsed, io);
|
|
148
|
+
case "enable":
|
|
149
|
+
return toggle(parsed, true, io);
|
|
150
|
+
case "disable":
|
|
151
|
+
return toggle(parsed, false, io);
|
|
152
|
+
case "update":
|
|
153
|
+
return await update(parsed, io);
|
|
154
|
+
case "list":
|
|
155
|
+
return parsed.available ? listAvailable(io) : await list(io);
|
|
156
|
+
case "inventory":
|
|
157
|
+
return await inventory(io);
|
|
158
|
+
default:
|
|
159
|
+
stderr(`Unknown plugin subcommand "${parsed.subcommand}".\n${USAGE}`);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
if (err instanceof PluginStoreError) {
|
|
165
|
+
stderr(`Error: ${err.message}\n`);
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// ── marketplace ─────────────────────────────────────────────────────────────
|
|
172
|
+
async function marketplaceCommand(parsed, io) {
|
|
173
|
+
const [action, target] = parsed.positionals;
|
|
174
|
+
switch (action) {
|
|
175
|
+
case "add": {
|
|
176
|
+
if (!target) {
|
|
177
|
+
io.stderr(`Error: a marketplace source is required.\nUsage: ${cmd} plugin marketplace add <owner/repo | git-url | path>\n`);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
const source = parseMarketplaceSource(target, io.cwd);
|
|
181
|
+
if (!source) {
|
|
182
|
+
io.stderr(`Error: "${target}" is not a GitHub owner/repo, a git URL, or an existing directory.\n`);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
if (source.source !== "directory")
|
|
186
|
+
io.stdout(`Cloning ${describeSource(source)}…\n`);
|
|
187
|
+
const added = await addMarketplace(source, io.store);
|
|
188
|
+
const names = added.marketplace.plugins.map((p) => p.name);
|
|
189
|
+
io.stdout(`Added marketplace "${added.name}" (${describeSource(source)}) with ${names.length} plugin(s)${names.length ? `: ${names.join(", ")}` : ""}.\n`);
|
|
190
|
+
if (names[0])
|
|
191
|
+
io.stdout(`Install one with: ${cmd} plugin install ${names[0]}@${added.name}\n`);
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
case "list":
|
|
195
|
+
case undefined: {
|
|
196
|
+
const known = readKnownMarketplaces(io.store.home);
|
|
197
|
+
const entries = Object.entries(known);
|
|
198
|
+
if (entries.length === 0) {
|
|
199
|
+
io.stdout(`No marketplaces added. Add one with: ${cmd} plugin marketplace add <owner/repo | git-url | path>\n`);
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
for (const [name, mp] of entries) {
|
|
203
|
+
io.stdout(`${name}\n Source: ${describeSource(mp.source)}\n Location: ${mp.installLocation}\n`);
|
|
204
|
+
}
|
|
205
|
+
return 0;
|
|
206
|
+
}
|
|
207
|
+
case "update": {
|
|
208
|
+
const known = readKnownMarketplaces(io.store.home);
|
|
209
|
+
const names = target ? [target] : Object.keys(known);
|
|
210
|
+
if (names.length === 0) {
|
|
211
|
+
io.stdout("No marketplaces to update.\n");
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
let failed = false;
|
|
215
|
+
for (const name of names) {
|
|
216
|
+
try {
|
|
217
|
+
const { changed } = await updateMarketplace(name, io.store);
|
|
218
|
+
io.stdout(`${name}: ${changed ? "updated" : "already up to date"}\n`);
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
failed = true;
|
|
222
|
+
io.stderr(`${name}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!failed)
|
|
226
|
+
io.stdout(`Installed plugins are re-synced by: ${cmd} plugin update\n`);
|
|
227
|
+
return failed ? 1 : 0;
|
|
228
|
+
}
|
|
229
|
+
case "remove": {
|
|
230
|
+
if (!target) {
|
|
231
|
+
io.stderr(`Error: a marketplace name is required.\nUsage: ${cmd} plugin marketplace remove <name>\n`);
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
const { removedPlugins } = removeMarketplace(target, io.store);
|
|
235
|
+
pruneCache(io.store.home);
|
|
236
|
+
io.stdout(`Removed marketplace "${target}"${removedPlugins.length ? ` and its installed plugin(s): ${removedPlugins.join(", ")}` : ""}.\n`);
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
default:
|
|
240
|
+
io.stderr(`Unknown marketplace action "${action}".\n${USAGE}`);
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// ── install / uninstall / enable / disable / update ─────────────────────────
|
|
245
|
+
async function install(parsed, io) {
|
|
246
|
+
const spec = parsed.positionals[0];
|
|
247
|
+
if (!spec) {
|
|
248
|
+
io.stderr(`Error: a plugin is required.\nUsage: ${cmd} plugin install <plugin>[@marketplace] [-s user|project]\n`);
|
|
249
|
+
return 1;
|
|
250
|
+
}
|
|
251
|
+
const projectPath = parsed.scope === "project" ? resolveRepoRoot(io.cwd) : undefined;
|
|
252
|
+
const { resolved, installPath, replaced } = await installPlugin(spec, { scope: parsed.scope, projectPath }, io.store);
|
|
253
|
+
const assets = discoverClaudePlugins({ cwd: io.cwd, homeDir: io.home, yagniLedger: readLedger(io.store.home) }).user.find((p) => p.key === resolved.key);
|
|
254
|
+
const where = parsed.scope === "project" ? `project scope, ${projectPath}` : "user scope";
|
|
255
|
+
io.stdout(`${replaced ? "Reinstalled" : "Installed"} ${resolved.key} v${resolved.version} (${where}).\n`);
|
|
256
|
+
if (assets) {
|
|
257
|
+
io.stdout(` Loads: ${componentSummary(assets)}\n`);
|
|
258
|
+
for (const line of mcpNotes(assets, io.env))
|
|
259
|
+
io.stdout(` ${line}\n`);
|
|
260
|
+
if (assets.unsupported.length > 0)
|
|
261
|
+
io.stdout(` Not bridged: ${assets.unsupported.join(", ")}\n`);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
io.stdout(` Warning: nothing loadable found in ${installPath} (no skills, commands, agents, or MCP servers).\n`);
|
|
265
|
+
}
|
|
266
|
+
io.stdout(`Takes effect on the next ${cmd} launch.\n`);
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
function uninstall(parsed, io) {
|
|
270
|
+
const spec = parsed.positionals[0];
|
|
271
|
+
if (!spec) {
|
|
272
|
+
io.stderr(`Error: a plugin is required.\nUsage: ${cmd} plugin uninstall <plugin>[@marketplace] [-s user|project]\n`);
|
|
273
|
+
return 1;
|
|
274
|
+
}
|
|
275
|
+
const { key, removed } = uninstallPlugin(spec, { ...(parsed.scopeExplicit ? { scope: parsed.scope } : {}), projectPath: resolveRepoRoot(io.cwd) }, io.store);
|
|
276
|
+
pruneCache(io.store.home);
|
|
277
|
+
io.stdout(`Uninstalled ${key} (${removed.map((r) => r.scope).join(", ")} scope).\n`);
|
|
278
|
+
return 0;
|
|
279
|
+
}
|
|
280
|
+
function toggle(parsed, enabled, io) {
|
|
281
|
+
const spec = parsed.positionals[0];
|
|
282
|
+
if (!spec) {
|
|
283
|
+
io.stderr(`Error: a plugin is required.\nUsage: ${cmd} plugin ${enabled ? "enable" : "disable"} <plugin>[@marketplace]\n`);
|
|
284
|
+
return 1;
|
|
285
|
+
}
|
|
286
|
+
const { key } = setPluginEnabled(spec, enabled, io.store);
|
|
287
|
+
io.stdout(`${enabled ? "Enabled" : "Disabled"} ${key}. Takes effect on the next ${cmd} launch.\n`);
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
async function update(parsed, io) {
|
|
291
|
+
const spec = parsed.positionals[0];
|
|
292
|
+
if (!spec && Object.keys(readLedger(io.store.home).plugins).length === 0) {
|
|
293
|
+
io.stdout(`No plugins installed via ${cmd}. Install one with: ${cmd} plugin install <plugin>[@marketplace]\n`);
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
const report = await updatePlugins(spec, io.store);
|
|
297
|
+
for (const mp of report.marketplaces)
|
|
298
|
+
io.stdout(`Marketplace ${mp.name}: ${mp.changed ? "updated" : "already up to date"}\n`);
|
|
299
|
+
for (const p of report.plugins) {
|
|
300
|
+
io.stdout(`${p.key}: ${p.from === p.to ? `v${p.to} re-synced` : `v${p.from} → v${p.to}`}\n`);
|
|
301
|
+
}
|
|
302
|
+
for (const e of report.errors)
|
|
303
|
+
io.stderr(`Error: ${e}\n`);
|
|
304
|
+
pruneCache(io.store.home);
|
|
305
|
+
if (report.plugins.length > 0)
|
|
306
|
+
io.stdout(`Takes effect on the next ${cmd} launch.\n`);
|
|
307
|
+
return report.errors.length > 0 ? 1 : 0;
|
|
308
|
+
}
|
|
309
|
+
// ── inventory (machine-readable, for the in-session panel) ─────────────────
|
|
310
|
+
async function inventory(io) {
|
|
311
|
+
const result = pluginInventory({ cwd: io.cwd, agentDir: await io.agentDir(), homeDir: io.home, env: io.env });
|
|
312
|
+
io.stdout(`${JSON.stringify(result)}\n`);
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
// ── list ────────────────────────────────────────────────────────────────────
|
|
316
|
+
async function list(io) {
|
|
317
|
+
const discovered = discoverClaudePlugins({ cwd: io.cwd, homeDir: io.home, yagniLedger: readLedger(io.store.home) });
|
|
318
|
+
const ledger = readLedger(io.store.home);
|
|
319
|
+
const repoRoot = resolveRepoRoot(io.cwd);
|
|
320
|
+
const all = [...discovered.user, ...discovered.project];
|
|
321
|
+
const disabled = Object.entries(ledger.plugins).filter(([, entries]) => entries.every((e) => !e.enabled));
|
|
322
|
+
const disabledLine = disabled.length > 0
|
|
323
|
+
? `Disabled: ${disabled.map(([key]) => key).join(", ")} (re-enable with \`${cmd} plugin enable <plugin>\`)\n`
|
|
324
|
+
: "";
|
|
325
|
+
if (all.length === 0) {
|
|
326
|
+
io.stdout("No plugins load here.\n");
|
|
327
|
+
if (disabledLine)
|
|
328
|
+
io.stdout(disabledLine);
|
|
329
|
+
else
|
|
330
|
+
io.stdout(`Add a marketplace with \`${cmd} plugin marketplace add <owner/repo | git-url | path>\`, then \`${cmd} plugin install <plugin>\`.\n`);
|
|
331
|
+
return 0;
|
|
332
|
+
}
|
|
333
|
+
let trust = null;
|
|
334
|
+
try {
|
|
335
|
+
trust = readTrustDecision(await io.agentDir(), io.cwd);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
trust = null;
|
|
339
|
+
}
|
|
340
|
+
const describeOrigin = (p) => {
|
|
341
|
+
if (p.origin === "yagni") {
|
|
342
|
+
const entries = ledger.plugins[p.key] ?? [];
|
|
343
|
+
const scopes = entries
|
|
344
|
+
.filter((e) => e.scope === "user" || e.projectPath === repoRoot || (e.projectPath && repoRoot.startsWith(e.projectPath + sep)))
|
|
345
|
+
.map((e) => e.scope);
|
|
346
|
+
return `installed via ${cmd} (${[...new Set(scopes)].join(", ") || "user"})`;
|
|
347
|
+
}
|
|
348
|
+
if (p.origin === "claude-code")
|
|
349
|
+
return "mirrored from Claude Code";
|
|
350
|
+
return "from this repo's marketplace";
|
|
351
|
+
};
|
|
352
|
+
const render = (p, gated) => {
|
|
353
|
+
io.stdout(` ${p.key}${p.version ? ` v${p.version}` : ""} ${describeOrigin(p)}${gated && trust !== true ? " (loads once this folder is trusted)" : ""}\n`);
|
|
354
|
+
io.stdout(` ${componentSummary(p)}\n`);
|
|
355
|
+
for (const line of mcpNotes(p, io.env))
|
|
356
|
+
io.stdout(` ${line}\n`);
|
|
357
|
+
if (p.unsupported.length > 0)
|
|
358
|
+
io.stdout(` not bridged: ${p.unsupported.join(", ")}\n`);
|
|
359
|
+
};
|
|
360
|
+
if (discovered.user.length > 0) {
|
|
361
|
+
io.stdout(`Plugins (${discovered.user.length}):\n`);
|
|
362
|
+
for (const p of discovered.user)
|
|
363
|
+
render(p, false);
|
|
364
|
+
}
|
|
365
|
+
if (discovered.project.length > 0) {
|
|
366
|
+
io.stdout(`Plugins from this repo's config (${discovered.project.length}):\n`);
|
|
367
|
+
for (const p of discovered.project)
|
|
368
|
+
render(p, true);
|
|
369
|
+
}
|
|
370
|
+
if (disabledLine)
|
|
371
|
+
io.stdout(disabledLine);
|
|
372
|
+
return 0;
|
|
373
|
+
}
|
|
374
|
+
function listAvailable(io) {
|
|
375
|
+
const cat = catalog(io.store);
|
|
376
|
+
if (cat.length === 0) {
|
|
377
|
+
io.stdout(`No marketplaces added. Add one with: ${cmd} plugin marketplace add <owner/repo | git-url | path>\n`);
|
|
378
|
+
return 0;
|
|
379
|
+
}
|
|
380
|
+
const installed = new Set(Object.keys(readLedger(io.store.home).plugins));
|
|
381
|
+
for (const mp of cat) {
|
|
382
|
+
io.stdout(`${mp.marketplace}:\n`);
|
|
383
|
+
if (mp.entries.length === 0)
|
|
384
|
+
io.stdout(" (no plugins listed)\n");
|
|
385
|
+
for (const e of mp.entries) {
|
|
386
|
+
const key = `${e.name}@${mp.marketplace}`;
|
|
387
|
+
const flags = [installed.has(key) ? "installed" : null, e.installable ? null : "not installable (remote source)"].filter(Boolean);
|
|
388
|
+
io.stdout(` ${e.name}${e.version ? ` v${e.version}` : ""}${flags.length ? ` [${flags.join(", ")}]` : ""}\n`);
|
|
389
|
+
if (e.description)
|
|
390
|
+
io.stdout(` ${e.description}\n`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return 0;
|
|
394
|
+
}
|
|
395
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
396
|
+
function isDir(path) {
|
|
397
|
+
try {
|
|
398
|
+
return statSync(path).isDirectory();
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function countSkills(paths) {
|
|
405
|
+
let n = 0;
|
|
406
|
+
for (const path of paths) {
|
|
407
|
+
if (!isDir(path)) {
|
|
408
|
+
n += 1;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (existsSync(join(path, "SKILL.md"))) {
|
|
412
|
+
n += 1;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
for (const child of readdirSync(path)) {
|
|
417
|
+
if (existsSync(join(path, child, "SKILL.md")))
|
|
418
|
+
n += 1;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// unreadable dir → 0
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return n;
|
|
426
|
+
}
|
|
427
|
+
function countMarkdown(paths) {
|
|
428
|
+
let n = 0;
|
|
429
|
+
for (const path of paths) {
|
|
430
|
+
if (!isDir(path)) {
|
|
431
|
+
if (path.endsWith(".md"))
|
|
432
|
+
n += 1;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
n += readdirSync(path).filter((f) => f.endsWith(".md")).length;
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
// unreadable dir → 0
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return n;
|
|
443
|
+
}
|
|
444
|
+
export function componentSummary(p) {
|
|
445
|
+
const parts = [];
|
|
446
|
+
const skills = countSkills(p.skillPaths);
|
|
447
|
+
const commands = countMarkdown(p.commandPaths);
|
|
448
|
+
const agents = countMarkdown(p.agentDirs);
|
|
449
|
+
const mcp = p.mcp ? Object.keys(p.mcp.servers).length : 0;
|
|
450
|
+
if (skills)
|
|
451
|
+
parts.push(`${skills} skill${skills === 1 ? "" : "s"}`);
|
|
452
|
+
if (commands)
|
|
453
|
+
parts.push(`${commands} command${commands === 1 ? "" : "s"}`);
|
|
454
|
+
if (agents)
|
|
455
|
+
parts.push(`${agents} agent${agents === 1 ? "" : "s"}`);
|
|
456
|
+
if (mcp)
|
|
457
|
+
parts.push(`${mcp} MCP server${mcp === 1 ? "" : "s"}`);
|
|
458
|
+
return parts.length ? parts.join(" · ") : "nothing loadable";
|
|
459
|
+
}
|
|
460
|
+
/** Whether a stdio `command` resolves on PATH (or is an existing path). Unknown when it has an unexpanded ${VAR}. */
|
|
461
|
+
export function commandOnPath(command, env) {
|
|
462
|
+
if (command.includes("${"))
|
|
463
|
+
return null;
|
|
464
|
+
if (isAbsolute(command) || command.includes("/") || command.includes("\\"))
|
|
465
|
+
return existsSync(command);
|
|
466
|
+
const exts = process.platform === "win32" ? ["", ...(env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")] : [""];
|
|
467
|
+
for (const dir of (env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
468
|
+
for (const ext of exts) {
|
|
469
|
+
if (existsSync(join(dir, command + ext)))
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
/** Per-server notes for a plugin's MCP block: names, and a missing-binary warning for stdio servers. */
|
|
476
|
+
export function mcpNotes(p, env) {
|
|
477
|
+
if (!p.mcp)
|
|
478
|
+
return [];
|
|
479
|
+
const lines = [];
|
|
480
|
+
const names = Object.keys(p.mcp.servers);
|
|
481
|
+
lines.push(`MCP: ${names.join(", ")} (check with \`${cmd} mcp list\`)`);
|
|
482
|
+
const missing = new Set();
|
|
483
|
+
for (const value of Object.values(p.mcp.servers)) {
|
|
484
|
+
if (!value || typeof value !== "object")
|
|
485
|
+
continue;
|
|
486
|
+
const config = value;
|
|
487
|
+
if (config.type !== undefined && config.type !== "stdio")
|
|
488
|
+
continue;
|
|
489
|
+
if (typeof config.command !== "string")
|
|
490
|
+
continue;
|
|
491
|
+
if (commandOnPath(config.command, env) === false)
|
|
492
|
+
missing.add(config.command);
|
|
493
|
+
}
|
|
494
|
+
for (const command of missing) {
|
|
495
|
+
lines.push(`Note: "${command}" is not on your PATH — install it before launching or its server will fail to start.`);
|
|
496
|
+
}
|
|
497
|
+
return lines;
|
|
498
|
+
}
|
|
499
|
+
//# sourceMappingURL=pluginCommand.js.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* YAGNI Code's own plugin ledger — the state behind `yagni plugin …`.
|
|
3
|
+
*
|
|
4
|
+
* Layout under `~/.yagni-code/plugins/` (Claude Code's shape, our own files —
|
|
5
|
+
* we never write into `~/.claude`, whose ledger format is versioned and
|
|
6
|
+
* undocumented; we only read it, see claudePlugins.ts):
|
|
7
|
+
*
|
|
8
|
+
* known_marketplaces.json marketplace name → source + local checkout
|
|
9
|
+
* installed_plugins.json `name@marketplace` → installed entries
|
|
10
|
+
* marketplaces/<name>/ git checkout of a marketplace repo
|
|
11
|
+
* cache/<mp>/<plugin>/<ver>/ the installed copy of a plugin
|
|
12
|
+
*
|
|
13
|
+
* Marketplace sources: `owner/repo` (GitHub over HTTPS, the developer's own
|
|
14
|
+
* git credentials), any git URL (ssh or https), or a local directory (its
|
|
15
|
+
* catalog is read in place; installs still copy into the cache, so edits land
|
|
16
|
+
* on the next `plugin update`). Only marketplace entries with relative-path
|
|
17
|
+
* sources install;
|
|
18
|
+
* object sources (github/url/npm/archive) are network installs of a second
|
|
19
|
+
* repo and are reported as unsupported rather than fetched.
|
|
20
|
+
*
|
|
21
|
+
* All git calls go through an injectable runner so tests never touch the
|
|
22
|
+
* network. Writes are atomic (temp + rename).
|
|
23
|
+
*/
|
|
24
|
+
import { type Marketplace, type MarketplaceEntry } from "./claudePlugins.js";
|
|
25
|
+
export type MarketplaceSource = {
|
|
26
|
+
source: "github";
|
|
27
|
+
repo: string;
|
|
28
|
+
} | {
|
|
29
|
+
source: "git";
|
|
30
|
+
url: string;
|
|
31
|
+
} | {
|
|
32
|
+
source: "directory";
|
|
33
|
+
path: string;
|
|
34
|
+
};
|
|
35
|
+
export interface KnownMarketplace {
|
|
36
|
+
source: MarketplaceSource;
|
|
37
|
+
/** Marketplace root: the checkout (git) or the directory itself. */
|
|
38
|
+
installLocation: string;
|
|
39
|
+
addedAt: string;
|
|
40
|
+
}
|
|
41
|
+
export type PluginInstallScope = "user" | "project";
|
|
42
|
+
export interface InstalledPlugin {
|
|
43
|
+
scope: PluginInstallScope;
|
|
44
|
+
/** Absolute path of the installed copy (cache dir). */
|
|
45
|
+
installPath: string;
|
|
46
|
+
version: string;
|
|
47
|
+
/** Repo root the project-scope install applies to. */
|
|
48
|
+
projectPath?: string;
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
installedAt: string;
|
|
51
|
+
}
|
|
52
|
+
export interface PluginLedger {
|
|
53
|
+
version: 1;
|
|
54
|
+
plugins: Record<string, InstalledPlugin[]>;
|
|
55
|
+
}
|
|
56
|
+
export interface GitResult {
|
|
57
|
+
ok: boolean;
|
|
58
|
+
stdout: string;
|
|
59
|
+
stderr: string;
|
|
60
|
+
}
|
|
61
|
+
export type GitRunner = (args: string[], opts: {
|
|
62
|
+
cwd?: string;
|
|
63
|
+
}) => Promise<GitResult>;
|
|
64
|
+
export declare const defaultGit: GitRunner;
|
|
65
|
+
export declare class PluginStoreError extends Error {
|
|
66
|
+
}
|
|
67
|
+
export declare function pluginsHome(env?: NodeJS.ProcessEnv, userHome?: string): string;
|
|
68
|
+
export declare function readKnownMarketplaces(home: string): Record<string, KnownMarketplace>;
|
|
69
|
+
export declare function writeKnownMarketplaces(home: string, value: Record<string, KnownMarketplace>): void;
|
|
70
|
+
export declare function readLedger(home: string): PluginLedger;
|
|
71
|
+
export declare function writeLedger(home: string, ledger: PluginLedger): void;
|
|
72
|
+
/** Interpret the `marketplace add` argument. Null when it is none of the shapes. */
|
|
73
|
+
export declare function parseMarketplaceSource(input: string, cwd: string): MarketplaceSource | null;
|
|
74
|
+
export declare function cloneUrl(source: MarketplaceSource): string | null;
|
|
75
|
+
export declare function describeSource(source: MarketplaceSource): string;
|
|
76
|
+
export interface StoreDeps {
|
|
77
|
+
home: string;
|
|
78
|
+
git?: GitRunner;
|
|
79
|
+
now?: () => Date;
|
|
80
|
+
}
|
|
81
|
+
export declare function addMarketplace(source: MarketplaceSource, deps: StoreDeps): Promise<{
|
|
82
|
+
name: string;
|
|
83
|
+
marketplace: Marketplace;
|
|
84
|
+
installLocation: string;
|
|
85
|
+
}>;
|
|
86
|
+
export declare function knownMarketplaceOrThrow(name: string, deps: StoreDeps): KnownMarketplace;
|
|
87
|
+
/** Pull a git marketplace; a directory marketplace is always current. Returns whether anything changed. */
|
|
88
|
+
export declare function updateMarketplace(name: string, deps: StoreDeps): Promise<{
|
|
89
|
+
changed: boolean;
|
|
90
|
+
head: string | null;
|
|
91
|
+
}>;
|
|
92
|
+
export declare function removeMarketplace(name: string, deps: StoreDeps): {
|
|
93
|
+
removedPlugins: string[];
|
|
94
|
+
};
|
|
95
|
+
export declare function splitKey(key: string): {
|
|
96
|
+
plugin: string;
|
|
97
|
+
marketplace: string | null;
|
|
98
|
+
};
|
|
99
|
+
export interface ResolvedPlugin {
|
|
100
|
+
key: string;
|
|
101
|
+
plugin: string;
|
|
102
|
+
marketplace: string;
|
|
103
|
+
marketplaceRoot: string;
|
|
104
|
+
entry: MarketplaceEntry;
|
|
105
|
+
/** Plugin source dir inside the marketplace checkout. */
|
|
106
|
+
sourceRoot: string;
|
|
107
|
+
version: string;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Find `<plugin>[@marketplace]` across known marketplaces. Without an
|
|
111
|
+
* explicit marketplace the name must be unique across them.
|
|
112
|
+
*/
|
|
113
|
+
export declare function resolvePluginSpec(spec: string, deps: StoreDeps): Promise<ResolvedPlugin>;
|
|
114
|
+
export interface InstallOptions {
|
|
115
|
+
scope: PluginInstallScope;
|
|
116
|
+
/** Required for scope "project": the repo root the install applies to. */
|
|
117
|
+
projectPath?: string;
|
|
118
|
+
}
|
|
119
|
+
export declare function installPlugin(spec: string, opts: InstallOptions, deps: StoreDeps): Promise<{
|
|
120
|
+
resolved: ResolvedPlugin;
|
|
121
|
+
installPath: string;
|
|
122
|
+
replaced: InstalledPlugin | null;
|
|
123
|
+
}>;
|
|
124
|
+
/** The ledger key an uninstall/enable/disable spec refers to; must be unambiguous. */
|
|
125
|
+
export declare function resolveInstalledKey(spec: string, deps: StoreDeps): string;
|
|
126
|
+
export interface UninstallOptions {
|
|
127
|
+
scope?: PluginInstallScope;
|
|
128
|
+
projectPath?: string;
|
|
129
|
+
}
|
|
130
|
+
export declare function uninstallPlugin(spec: string, opts: UninstallOptions, deps: StoreDeps): {
|
|
131
|
+
key: string;
|
|
132
|
+
removed: InstalledPlugin[];
|
|
133
|
+
};
|
|
134
|
+
export declare function setPluginEnabled(spec: string, enabled: boolean, deps: StoreDeps): {
|
|
135
|
+
key: string;
|
|
136
|
+
count: number;
|
|
137
|
+
};
|
|
138
|
+
export interface UpdateReport {
|
|
139
|
+
key: string;
|
|
140
|
+
from: string;
|
|
141
|
+
to: string;
|
|
142
|
+
/** Files re-synced from the marketplace even when the version string is unchanged. */
|
|
143
|
+
refreshed: boolean;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Refresh marketplaces and re-sync installed plugins from them. Every
|
|
147
|
+
* installed plugin is re-copied (a content change with no version bump still
|
|
148
|
+
* lands), and version changes are reported.
|
|
149
|
+
*/
|
|
150
|
+
export declare function updatePlugins(spec: string | undefined, deps: StoreDeps): Promise<{
|
|
151
|
+
marketplaces: {
|
|
152
|
+
name: string;
|
|
153
|
+
changed: boolean;
|
|
154
|
+
}[];
|
|
155
|
+
plugins: UpdateReport[];
|
|
156
|
+
errors: string[];
|
|
157
|
+
}>;
|
|
158
|
+
/** Marketplace catalog entries with their install status (for `plugin list --available`). */
|
|
159
|
+
export declare function catalog(deps: StoreDeps): {
|
|
160
|
+
marketplace: string;
|
|
161
|
+
entries: {
|
|
162
|
+
name: string;
|
|
163
|
+
version: string | null;
|
|
164
|
+
description: string | null;
|
|
165
|
+
installable: boolean;
|
|
166
|
+
}[];
|
|
167
|
+
}[];
|
|
168
|
+
/** Remove empty cache parents left behind by uninstalls (best effort). */
|
|
169
|
+
export declare function pruneCache(home: string): void;
|
|
170
|
+
//# sourceMappingURL=pluginStore.d.ts.map
|