@yagni-app/code 1.0.6 → 1.0.7
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 +84 -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/condensedTools.d.ts +12 -1
- package/dist/extension/condensedTools.js +17 -9
- package/dist/extension/index.d.ts +13 -0
- package/dist/extension/index.js +230 -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 +97 -2
- package/dist/extension/permission/gate.js +375 -26
- 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 +53 -0
- package/dist/extension/permissionRules/loadConfig.js +90 -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 +58 -0
- package/dist/extension/permissionRules/pathRules.js +120 -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 +114 -0
- package/dist/extension/sandbox/config.js +366 -0
- package/dist/extension/sandbox/manager.d.ts +98 -0
- package/dist/extension/sandbox/manager.js +216 -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 +775 -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 +99 -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 +40 -0
- package/dist/extension/telemetry/register.js +192 -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/package.json +19 -3
|
@@ -0,0 +1,554 @@
|
|
|
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 { execFile } from "node:child_process";
|
|
25
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
26
|
+
import { homedir } from "node:os";
|
|
27
|
+
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
28
|
+
import { isSafeSegment, readMarketplace, resolveLocalPluginRoot, } from "./claudePlugins.js";
|
|
29
|
+
export const defaultGit = (args, opts) => new Promise((resolveResult) => {
|
|
30
|
+
execFile("git", args, {
|
|
31
|
+
cwd: opts.cwd,
|
|
32
|
+
// Never hang on a credential prompt inside a non-interactive install.
|
|
33
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
34
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
35
|
+
}, (err, stdout, stderr) => {
|
|
36
|
+
resolveResult({ ok: !err, stdout: String(stdout ?? ""), stderr: String(stderr ?? (err?.message ?? "")) });
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
export class PluginStoreError extends Error {
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Every path we delete, copy into, or rename to must sit inside the plugin
|
|
43
|
+
* home. Names/versions are already segment-checked at parse time; this is the
|
|
44
|
+
* belt-and-braces guard right before the destructive call.
|
|
45
|
+
*/
|
|
46
|
+
function assertInside(home, path, what) {
|
|
47
|
+
const base = resolve(home);
|
|
48
|
+
const real = resolve(path);
|
|
49
|
+
if (real !== base && !real.startsWith(base + sep)) {
|
|
50
|
+
throw new PluginStoreError(`Refusing to touch ${what} outside the plugin home: ${real}`);
|
|
51
|
+
}
|
|
52
|
+
return real;
|
|
53
|
+
}
|
|
54
|
+
// ── paths ───────────────────────────────────────────────────────────────────
|
|
55
|
+
export function pluginsHome(env = process.env, userHome = homedir()) {
|
|
56
|
+
return join(env.YAGNI_CODE_HOME ?? join(userHome, ".yagni-code"), "plugins");
|
|
57
|
+
}
|
|
58
|
+
function knownMarketplacesPath(home) {
|
|
59
|
+
return join(home, "known_marketplaces.json");
|
|
60
|
+
}
|
|
61
|
+
function ledgerPath(home) {
|
|
62
|
+
return join(home, "installed_plugins.json");
|
|
63
|
+
}
|
|
64
|
+
function isDirectory(path) {
|
|
65
|
+
try {
|
|
66
|
+
return statSync(path).isDirectory();
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function readJson(path) {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function writeJsonAtomic(path, value) {
|
|
81
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
82
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
83
|
+
writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
84
|
+
renameSync(tmp, path);
|
|
85
|
+
}
|
|
86
|
+
// ── known marketplaces ──────────────────────────────────────────────────────
|
|
87
|
+
export function readKnownMarketplaces(home) {
|
|
88
|
+
const parsed = readJson(knownMarketplacesPath(home));
|
|
89
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
90
|
+
return {};
|
|
91
|
+
const out = {};
|
|
92
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
93
|
+
if (!value || typeof value !== "object")
|
|
94
|
+
continue;
|
|
95
|
+
const v = value;
|
|
96
|
+
if (typeof v.installLocation !== "string" || !v.source || typeof v.source !== "object")
|
|
97
|
+
continue;
|
|
98
|
+
out[name] = {
|
|
99
|
+
source: v.source,
|
|
100
|
+
installLocation: v.installLocation,
|
|
101
|
+
addedAt: typeof v.addedAt === "string" ? v.addedAt : "",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
export function writeKnownMarketplaces(home, value) {
|
|
107
|
+
writeJsonAtomic(knownMarketplacesPath(home), value);
|
|
108
|
+
}
|
|
109
|
+
// ── installed-plugin ledger ─────────────────────────────────────────────────
|
|
110
|
+
export function readLedger(home) {
|
|
111
|
+
const parsed = readJson(ledgerPath(home));
|
|
112
|
+
const empty = { version: 1, plugins: {} };
|
|
113
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
114
|
+
return empty;
|
|
115
|
+
const plugins = parsed.plugins;
|
|
116
|
+
if (!plugins || typeof plugins !== "object" || Array.isArray(plugins))
|
|
117
|
+
return empty;
|
|
118
|
+
const out = { version: 1, plugins: {} };
|
|
119
|
+
for (const [key, value] of Object.entries(plugins)) {
|
|
120
|
+
if (!Array.isArray(value))
|
|
121
|
+
continue;
|
|
122
|
+
const entries = [];
|
|
123
|
+
for (const item of value) {
|
|
124
|
+
if (!item || typeof item !== "object")
|
|
125
|
+
continue;
|
|
126
|
+
const e = item;
|
|
127
|
+
if (typeof e.installPath !== "string" || (e.scope !== "user" && e.scope !== "project"))
|
|
128
|
+
continue;
|
|
129
|
+
entries.push({
|
|
130
|
+
scope: e.scope,
|
|
131
|
+
installPath: e.installPath,
|
|
132
|
+
version: typeof e.version === "string" ? e.version : "unknown",
|
|
133
|
+
...(typeof e.projectPath === "string" ? { projectPath: e.projectPath } : {}),
|
|
134
|
+
enabled: e.enabled !== false,
|
|
135
|
+
installedAt: typeof e.installedAt === "string" ? e.installedAt : "",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (entries.length > 0)
|
|
139
|
+
out.plugins[key] = entries;
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
export function writeLedger(home, ledger) {
|
|
144
|
+
writeJsonAtomic(ledgerPath(home), ledger);
|
|
145
|
+
}
|
|
146
|
+
// ── sources ─────────────────────────────────────────────────────────────────
|
|
147
|
+
const GITHUB_SHORTHAND = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
148
|
+
const GIT_URL = /^(https?:\/\/|git@|ssh:\/\/|git:\/\/|file:\/\/)/;
|
|
149
|
+
/** Interpret the `marketplace add` argument. Null when it is none of the shapes. */
|
|
150
|
+
export function parseMarketplaceSource(input, cwd) {
|
|
151
|
+
const trimmed = input.trim();
|
|
152
|
+
if (!trimmed || trimmed.startsWith("-"))
|
|
153
|
+
return null;
|
|
154
|
+
const asPath = isAbsolute(trimmed) ? trimmed : resolve(cwd, trimmed);
|
|
155
|
+
if (isDirectory(asPath))
|
|
156
|
+
return { source: "directory", path: asPath };
|
|
157
|
+
if (GIT_URL.test(trimmed) || trimmed.endsWith(".git"))
|
|
158
|
+
return { source: "git", url: trimmed };
|
|
159
|
+
if (GITHUB_SHORTHAND.test(trimmed) && !trimmed.startsWith("."))
|
|
160
|
+
return { source: "github", repo: trimmed };
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
export function cloneUrl(source) {
|
|
164
|
+
if (source.source === "github")
|
|
165
|
+
return `https://github.com/${source.repo}.git`;
|
|
166
|
+
if (source.source === "git")
|
|
167
|
+
return source.url;
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
export function describeSource(source) {
|
|
171
|
+
if (source.source === "github")
|
|
172
|
+
return `github:${source.repo}`;
|
|
173
|
+
if (source.source === "git")
|
|
174
|
+
return source.url;
|
|
175
|
+
return source.path;
|
|
176
|
+
}
|
|
177
|
+
function stamp(deps) {
|
|
178
|
+
return (deps.now ?? (() => new Date()))().toISOString();
|
|
179
|
+
}
|
|
180
|
+
function marketplaceOrThrow(root, label) {
|
|
181
|
+
const mp = readMarketplace(root);
|
|
182
|
+
if (!mp) {
|
|
183
|
+
throw new PluginStoreError(`${label} has no usable .claude-plugin/marketplace.json (expected a "name" and a "plugins" array).`);
|
|
184
|
+
}
|
|
185
|
+
return mp;
|
|
186
|
+
}
|
|
187
|
+
export async function addMarketplace(source, deps) {
|
|
188
|
+
const known = readKnownMarketplaces(deps.home);
|
|
189
|
+
if (source.source === "directory") {
|
|
190
|
+
const mp = marketplaceOrThrow(source.path, source.path);
|
|
191
|
+
if (known[mp.name])
|
|
192
|
+
throw new PluginStoreError(`Marketplace "${mp.name}" is already added (${describeSource(known[mp.name].source)}).`);
|
|
193
|
+
known[mp.name] = { source, installLocation: source.path, addedAt: stamp(deps) };
|
|
194
|
+
writeKnownMarketplaces(deps.home, known);
|
|
195
|
+
return { name: mp.name, marketplace: mp, installLocation: source.path };
|
|
196
|
+
}
|
|
197
|
+
const url = cloneUrl(source);
|
|
198
|
+
const git = deps.git ?? defaultGit;
|
|
199
|
+
const marketplacesDir = join(deps.home, "marketplaces");
|
|
200
|
+
mkdirSync(marketplacesDir, { recursive: true });
|
|
201
|
+
const tmp = join(marketplacesDir, `.tmp-${process.pid}-${Date.now()}`);
|
|
202
|
+
// `--` so a URL can never be read as a git option.
|
|
203
|
+
const result = await git(["clone", "--quiet", "--", url, tmp], {});
|
|
204
|
+
if (!result.ok) {
|
|
205
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
206
|
+
const hint = source.source === "github" && /authentication|could not read username|permission denied|403|404/i.test(result.stderr)
|
|
207
|
+
? `\nIf your GitHub access is over SSH, add it as: git@github.com:${source.repo}.git`
|
|
208
|
+
: "";
|
|
209
|
+
throw new PluginStoreError(`git clone failed for ${url}:\n${result.stderr.trim()}${hint}`);
|
|
210
|
+
}
|
|
211
|
+
let mp;
|
|
212
|
+
try {
|
|
213
|
+
mp = marketplaceOrThrow(tmp, url);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
const dest = assertInside(marketplacesDir, join(marketplacesDir, mp.name), "a marketplace checkout");
|
|
220
|
+
if (known[mp.name] || existsSync(dest)) {
|
|
221
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
222
|
+
throw new PluginStoreError(`Marketplace "${mp.name}" is already added. Run \`marketplace update ${mp.name}\` to refresh it.`);
|
|
223
|
+
}
|
|
224
|
+
renameSync(tmp, dest);
|
|
225
|
+
known[mp.name] = { source, installLocation: dest, addedAt: stamp(deps) };
|
|
226
|
+
writeKnownMarketplaces(deps.home, known);
|
|
227
|
+
return { name: mp.name, marketplace: readMarketplace(dest) ?? { ...mp, root: dest }, installLocation: dest };
|
|
228
|
+
}
|
|
229
|
+
export function knownMarketplaceOrThrow(name, deps) {
|
|
230
|
+
const known = readKnownMarketplaces(deps.home);
|
|
231
|
+
const entry = known[name];
|
|
232
|
+
if (!entry) {
|
|
233
|
+
const names = Object.keys(known);
|
|
234
|
+
throw new PluginStoreError(names.length === 0
|
|
235
|
+
? `No marketplaces added yet. Run \`plugin marketplace add <owner/repo | git-url | path>\` first.`
|
|
236
|
+
: `Unknown marketplace "${name}". Known: ${names.join(", ")}.`);
|
|
237
|
+
}
|
|
238
|
+
return entry;
|
|
239
|
+
}
|
|
240
|
+
/** Pull a git marketplace; a directory marketplace is always current. Returns whether anything changed. */
|
|
241
|
+
export async function updateMarketplace(name, deps) {
|
|
242
|
+
const entry = knownMarketplaceOrThrow(name, deps);
|
|
243
|
+
if (entry.source.source === "directory")
|
|
244
|
+
return { changed: false, head: null };
|
|
245
|
+
const git = deps.git ?? defaultGit;
|
|
246
|
+
const before = await git(["rev-parse", "HEAD"], { cwd: entry.installLocation });
|
|
247
|
+
const pull = await git(["pull", "--ff-only", "--quiet"], { cwd: entry.installLocation });
|
|
248
|
+
if (!pull.ok)
|
|
249
|
+
throw new PluginStoreError(`git pull failed for marketplace "${name}":\n${pull.stderr.trim()}`);
|
|
250
|
+
const after = await git(["rev-parse", "HEAD"], { cwd: entry.installLocation });
|
|
251
|
+
const head = after.ok ? after.stdout.trim() : null;
|
|
252
|
+
return { changed: !before.ok || !after.ok || before.stdout.trim() !== after.stdout.trim(), head };
|
|
253
|
+
}
|
|
254
|
+
export function removeMarketplace(name, deps) {
|
|
255
|
+
const entry = knownMarketplaceOrThrow(name, deps);
|
|
256
|
+
const ledger = readLedger(deps.home);
|
|
257
|
+
const removedPlugins = [];
|
|
258
|
+
for (const key of Object.keys(ledger.plugins)) {
|
|
259
|
+
if (splitKey(key).marketplace === name) {
|
|
260
|
+
for (const inst of ledger.plugins[key] ?? [])
|
|
261
|
+
dropCacheDir(inst.installPath, deps.home);
|
|
262
|
+
delete ledger.plugins[key];
|
|
263
|
+
removedPlugins.push(key);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
writeLedger(deps.home, ledger);
|
|
267
|
+
const known = readKnownMarketplaces(deps.home);
|
|
268
|
+
delete known[name];
|
|
269
|
+
writeKnownMarketplaces(deps.home, known);
|
|
270
|
+
if (entry.source.source !== "directory") {
|
|
271
|
+
rmSync(assertInside(join(deps.home, "marketplaces"), entry.installLocation, "a marketplace checkout"), {
|
|
272
|
+
recursive: true,
|
|
273
|
+
force: true,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return { removedPlugins };
|
|
277
|
+
}
|
|
278
|
+
// ── plugin operations ───────────────────────────────────────────────────────
|
|
279
|
+
export function splitKey(key) {
|
|
280
|
+
const at = key.lastIndexOf("@");
|
|
281
|
+
if (at <= 0)
|
|
282
|
+
return { plugin: key, marketplace: null };
|
|
283
|
+
return { plugin: key.slice(0, at), marketplace: key.slice(at + 1) };
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Find `<plugin>[@marketplace]` across known marketplaces. Without an
|
|
287
|
+
* explicit marketplace the name must be unique across them.
|
|
288
|
+
*/
|
|
289
|
+
export async function resolvePluginSpec(spec, deps) {
|
|
290
|
+
const { plugin, marketplace } = splitKey(spec);
|
|
291
|
+
const known = readKnownMarketplaces(deps.home);
|
|
292
|
+
if (Object.keys(known).length === 0) {
|
|
293
|
+
throw new PluginStoreError(`No marketplaces added yet. Run \`plugin marketplace add <owner/repo | git-url | path>\` first.`);
|
|
294
|
+
}
|
|
295
|
+
const candidates = [];
|
|
296
|
+
for (const [name, entry] of Object.entries(known)) {
|
|
297
|
+
if (marketplace && name !== marketplace)
|
|
298
|
+
continue;
|
|
299
|
+
const mp = readMarketplace(entry.installLocation);
|
|
300
|
+
if (!mp)
|
|
301
|
+
continue;
|
|
302
|
+
const found = mp.plugins.find((p) => p.name === plugin);
|
|
303
|
+
if (!found)
|
|
304
|
+
continue;
|
|
305
|
+
const sourceRoot = resolveLocalPluginRoot(mp, found);
|
|
306
|
+
if (!sourceRoot) {
|
|
307
|
+
throw new PluginStoreError(`Plugin "${plugin}" in marketplace "${name}" uses a source YAGNI cannot install (${describeEntrySource(found)}). ` +
|
|
308
|
+
`Only relative-path sources inside the marketplace repo are supported.`);
|
|
309
|
+
}
|
|
310
|
+
candidates.push({
|
|
311
|
+
key: `${plugin}@${name}`,
|
|
312
|
+
plugin,
|
|
313
|
+
marketplace: name,
|
|
314
|
+
marketplaceRoot: entry.installLocation,
|
|
315
|
+
entry: found,
|
|
316
|
+
sourceRoot,
|
|
317
|
+
version: await pluginVersion(found, entry, deps),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (candidates.length === 0) {
|
|
321
|
+
if (marketplace && !known[marketplace]) {
|
|
322
|
+
throw new PluginStoreError(`Unknown marketplace "${marketplace}". Known: ${Object.keys(known).join(", ")}.`);
|
|
323
|
+
}
|
|
324
|
+
throw new PluginStoreError(`Plugin "${plugin}" not found in ${marketplace ? `marketplace "${marketplace}"` : "any known marketplace"}.`);
|
|
325
|
+
}
|
|
326
|
+
if (candidates.length > 1) {
|
|
327
|
+
throw new PluginStoreError(`Plugin "${plugin}" exists in several marketplaces (${candidates.map((c) => c.marketplace).join(", ")}). ` +
|
|
328
|
+
`Use ${plugin}@<marketplace>.`);
|
|
329
|
+
}
|
|
330
|
+
return candidates[0];
|
|
331
|
+
}
|
|
332
|
+
function describeEntrySource(entry) {
|
|
333
|
+
const src = entry.source;
|
|
334
|
+
if (typeof src === "string")
|
|
335
|
+
return `"${src}"`;
|
|
336
|
+
if (src && typeof src === "object") {
|
|
337
|
+
const kind = src.source;
|
|
338
|
+
return typeof kind === "string" ? `${kind} source` : "object source";
|
|
339
|
+
}
|
|
340
|
+
return "missing source";
|
|
341
|
+
}
|
|
342
|
+
async function pluginVersion(entry, mp, deps) {
|
|
343
|
+
if (isSafeSegment(entry.version))
|
|
344
|
+
return entry.version;
|
|
345
|
+
if (mp.source.source !== "directory") {
|
|
346
|
+
const git = deps.git ?? defaultGit;
|
|
347
|
+
const head = await git(["rev-parse", "--short", "HEAD"], { cwd: mp.installLocation });
|
|
348
|
+
if (head.ok && head.stdout.trim())
|
|
349
|
+
return head.stdout.trim();
|
|
350
|
+
}
|
|
351
|
+
return "local";
|
|
352
|
+
}
|
|
353
|
+
function cacheDir(home, resolved) {
|
|
354
|
+
for (const [label, value] of [
|
|
355
|
+
["marketplace", resolved.marketplace],
|
|
356
|
+
["plugin", resolved.plugin],
|
|
357
|
+
["version", resolved.version],
|
|
358
|
+
]) {
|
|
359
|
+
if (!isSafeSegment(value))
|
|
360
|
+
throw new PluginStoreError(`Refusing to install: unsafe ${label} name "${value}".`);
|
|
361
|
+
}
|
|
362
|
+
return assertInside(join(home, "cache"), join(home, "cache", resolved.marketplace, resolved.plugin, resolved.version), "a cache dir");
|
|
363
|
+
}
|
|
364
|
+
/** Copy the plugin source into its cache dir (replacing any stale copy). */
|
|
365
|
+
function materialize(resolved, deps) {
|
|
366
|
+
const dest = cacheDir(deps.home, resolved);
|
|
367
|
+
rmSync(dest, { recursive: true, force: true });
|
|
368
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
369
|
+
cpSync(resolved.sourceRoot, dest, { recursive: true, dereference: true });
|
|
370
|
+
return dest;
|
|
371
|
+
}
|
|
372
|
+
function dropCacheDir(installPath, home) {
|
|
373
|
+
const cacheRoot = resolve(join(home, "cache"));
|
|
374
|
+
const real = resolve(installPath);
|
|
375
|
+
if (real !== cacheRoot && real.startsWith(cacheRoot + sep))
|
|
376
|
+
rmSync(real, { recursive: true, force: true });
|
|
377
|
+
}
|
|
378
|
+
function sameTarget(a, scope, projectPath) {
|
|
379
|
+
if (a.scope !== scope)
|
|
380
|
+
return false;
|
|
381
|
+
return scope === "user" || a.projectPath === projectPath;
|
|
382
|
+
}
|
|
383
|
+
export async function installPlugin(spec, opts, deps) {
|
|
384
|
+
if (opts.scope === "project" && !opts.projectPath)
|
|
385
|
+
throw new PluginStoreError("A project-scope install needs a project path.");
|
|
386
|
+
const resolved = await resolvePluginSpec(spec, deps);
|
|
387
|
+
const installPath = materialize(resolved, deps);
|
|
388
|
+
const ledger = readLedger(deps.home);
|
|
389
|
+
const entries = ledger.plugins[resolved.key] ?? [];
|
|
390
|
+
const replaced = entries.find((e) => sameTarget(e, opts.scope, opts.projectPath)) ?? null;
|
|
391
|
+
const next = {
|
|
392
|
+
scope: opts.scope,
|
|
393
|
+
installPath,
|
|
394
|
+
version: resolved.version,
|
|
395
|
+
...(opts.scope === "project" ? { projectPath: opts.projectPath } : {}),
|
|
396
|
+
enabled: true,
|
|
397
|
+
installedAt: stamp(deps),
|
|
398
|
+
};
|
|
399
|
+
ledger.plugins[resolved.key] = [...entries.filter((e) => e !== replaced), next];
|
|
400
|
+
writeLedger(deps.home, ledger);
|
|
401
|
+
if (replaced && replaced.installPath !== installPath && !isReferenced(ledger, replaced.installPath)) {
|
|
402
|
+
dropCacheDir(replaced.installPath, deps.home);
|
|
403
|
+
}
|
|
404
|
+
return { resolved, installPath, replaced };
|
|
405
|
+
}
|
|
406
|
+
function isReferenced(ledger, installPath) {
|
|
407
|
+
return Object.values(ledger.plugins).some((list) => list.some((e) => e.installPath === installPath));
|
|
408
|
+
}
|
|
409
|
+
/** The ledger key an uninstall/enable/disable spec refers to; must be unambiguous. */
|
|
410
|
+
export function resolveInstalledKey(spec, deps) {
|
|
411
|
+
const ledger = readLedger(deps.home);
|
|
412
|
+
const { plugin, marketplace } = splitKey(spec);
|
|
413
|
+
const keys = Object.keys(ledger.plugins).filter((k) => {
|
|
414
|
+
const s = splitKey(k);
|
|
415
|
+
return s.plugin === plugin && (!marketplace || s.marketplace === marketplace);
|
|
416
|
+
});
|
|
417
|
+
if (keys.length === 0)
|
|
418
|
+
throw new PluginStoreError(`Plugin "${spec}" is not installed here.`);
|
|
419
|
+
if (keys.length > 1)
|
|
420
|
+
throw new PluginStoreError(`"${plugin}" is installed from several marketplaces (${keys.join(", ")}). Use ${plugin}@<marketplace>.`);
|
|
421
|
+
return keys[0];
|
|
422
|
+
}
|
|
423
|
+
export function uninstallPlugin(spec, opts, deps) {
|
|
424
|
+
const key = resolveInstalledKey(spec, deps);
|
|
425
|
+
const ledger = readLedger(deps.home);
|
|
426
|
+
const entries = ledger.plugins[key] ?? [];
|
|
427
|
+
const removed = entries.filter((e) => {
|
|
428
|
+
if (opts.scope)
|
|
429
|
+
return sameTarget(e, opts.scope, opts.projectPath);
|
|
430
|
+
// No scope: everything that applies here — the user-scope install plus
|
|
431
|
+
// this project's install.
|
|
432
|
+
return e.scope === "user" || e.projectPath === opts.projectPath;
|
|
433
|
+
});
|
|
434
|
+
if (removed.length === 0) {
|
|
435
|
+
throw new PluginStoreError(`Plugin "${key}" is not installed at ${opts.scope ?? "user or project"} scope here.`);
|
|
436
|
+
}
|
|
437
|
+
const kept = entries.filter((e) => !removed.includes(e));
|
|
438
|
+
if (kept.length > 0)
|
|
439
|
+
ledger.plugins[key] = kept;
|
|
440
|
+
else
|
|
441
|
+
delete ledger.plugins[key];
|
|
442
|
+
writeLedger(deps.home, ledger);
|
|
443
|
+
for (const e of removed) {
|
|
444
|
+
if (!isReferenced(ledger, e.installPath))
|
|
445
|
+
dropCacheDir(e.installPath, deps.home);
|
|
446
|
+
}
|
|
447
|
+
return { key, removed };
|
|
448
|
+
}
|
|
449
|
+
export function setPluginEnabled(spec, enabled, deps) {
|
|
450
|
+
const key = resolveInstalledKey(spec, deps);
|
|
451
|
+
const ledger = readLedger(deps.home);
|
|
452
|
+
const entries = ledger.plugins[key] ?? [];
|
|
453
|
+
for (const e of entries)
|
|
454
|
+
e.enabled = enabled;
|
|
455
|
+
writeLedger(deps.home, ledger);
|
|
456
|
+
return { key, count: entries.length };
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Refresh marketplaces and re-sync installed plugins from them. Every
|
|
460
|
+
* installed plugin is re-copied (a content change with no version bump still
|
|
461
|
+
* lands), and version changes are reported.
|
|
462
|
+
*/
|
|
463
|
+
export async function updatePlugins(spec, deps) {
|
|
464
|
+
const ledger = readLedger(deps.home);
|
|
465
|
+
const keys = spec ? [resolveInstalledKey(spec, deps)] : Object.keys(ledger.plugins);
|
|
466
|
+
const marketplaces = [...new Set(keys.map((k) => splitKey(k).marketplace).filter((m) => !!m))];
|
|
467
|
+
const out = { marketplaces: [], plugins: [], errors: [] };
|
|
468
|
+
for (const name of marketplaces) {
|
|
469
|
+
try {
|
|
470
|
+
const { changed } = await updateMarketplace(name, deps);
|
|
471
|
+
out.marketplaces.push({ name, changed });
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
out.errors.push(err instanceof Error ? err.message : String(err));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
for (const key of keys) {
|
|
478
|
+
const entries = ledger.plugins[key] ?? [];
|
|
479
|
+
let resolved;
|
|
480
|
+
try {
|
|
481
|
+
resolved = await resolvePluginSpec(key, deps);
|
|
482
|
+
}
|
|
483
|
+
catch (err) {
|
|
484
|
+
out.errors.push(err instanceof Error ? err.message : String(err));
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const installPath = materialize(resolved, deps);
|
|
488
|
+
const stale = new Set();
|
|
489
|
+
const from = [...new Set(entries.map((e) => e.version))].join("/");
|
|
490
|
+
for (const e of entries) {
|
|
491
|
+
if (e.installPath !== installPath)
|
|
492
|
+
stale.add(e.installPath);
|
|
493
|
+
e.installPath = installPath;
|
|
494
|
+
e.version = resolved.version;
|
|
495
|
+
}
|
|
496
|
+
out.plugins.push({ key, from, to: resolved.version, refreshed: true });
|
|
497
|
+
writeLedger(deps.home, ledger);
|
|
498
|
+
for (const path of stale) {
|
|
499
|
+
if (!isReferenced(ledger, path))
|
|
500
|
+
dropCacheDir(path, deps.home);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
/** Marketplace catalog entries with their install status (for `plugin list --available`). */
|
|
506
|
+
export function catalog(deps) {
|
|
507
|
+
const known = readKnownMarketplaces(deps.home);
|
|
508
|
+
const out = [];
|
|
509
|
+
for (const [name, entry] of Object.entries(known)) {
|
|
510
|
+
const mp = readMarketplace(entry.installLocation);
|
|
511
|
+
if (!mp) {
|
|
512
|
+
out.push({ marketplace: name, entries: [] });
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
out.push({
|
|
516
|
+
marketplace: name,
|
|
517
|
+
entries: mp.plugins.map((p) => {
|
|
518
|
+
const extra = p;
|
|
519
|
+
return {
|
|
520
|
+
name: p.name,
|
|
521
|
+
version: typeof extra.version === "string" ? extra.version : null,
|
|
522
|
+
description: typeof extra.description === "string" ? extra.description : null,
|
|
523
|
+
installable: resolveLocalPluginRoot(mp, p) !== null,
|
|
524
|
+
};
|
|
525
|
+
}),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
return out;
|
|
529
|
+
}
|
|
530
|
+
/** Remove empty cache parents left behind by uninstalls (best effort). */
|
|
531
|
+
export function pruneCache(home) {
|
|
532
|
+
const root = join(home, "cache");
|
|
533
|
+
if (!isDirectory(root))
|
|
534
|
+
return;
|
|
535
|
+
const prune = (dir, depth) => {
|
|
536
|
+
if (depth === 0)
|
|
537
|
+
return;
|
|
538
|
+
for (const child of readdirSync(dir)) {
|
|
539
|
+
const path = join(dir, child);
|
|
540
|
+
if (!isDirectory(path))
|
|
541
|
+
continue;
|
|
542
|
+
prune(path, depth - 1);
|
|
543
|
+
try {
|
|
544
|
+
if (readdirSync(path).length === 0)
|
|
545
|
+
rmSync(path, { recursive: true });
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
// best effort
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
prune(root, 2);
|
|
553
|
+
}
|
|
554
|
+
//# sourceMappingURL=pluginStore.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.7",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -34,13 +34,29 @@
|
|
|
34
34
|
"main": "./dist/cli.js",
|
|
35
35
|
"types": "./dist/cli.d.ts",
|
|
36
36
|
"dependencies": {
|
|
37
|
+
"@anthropic-ai/sandbox-runtime": "0.0.74",
|
|
37
38
|
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
38
39
|
"@earendil-works/pi-tui": "0.84.1",
|
|
40
|
+
"@grpc/grpc-js": "^1.14.4",
|
|
39
41
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
40
|
-
"
|
|
42
|
+
"@opentelemetry/api": "^1.9.1",
|
|
43
|
+
"@opentelemetry/api-logs": "^0.222.0",
|
|
44
|
+
"@opentelemetry/exporter-logs-otlp-grpc": "^0.222.0",
|
|
45
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.222.0",
|
|
46
|
+
"@opentelemetry/exporter-logs-otlp-proto": "^0.222.0",
|
|
47
|
+
"@opentelemetry/exporter-metrics-otlp-grpc": "^0.222.0",
|
|
48
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.222.0",
|
|
49
|
+
"@opentelemetry/exporter-metrics-otlp-proto": "^0.222.0",
|
|
50
|
+
"@opentelemetry/exporter-trace-otlp-grpc": "^0.222.0",
|
|
51
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.222.0",
|
|
52
|
+
"@opentelemetry/exporter-trace-otlp-proto": "^0.222.0",
|
|
53
|
+
"@opentelemetry/resources": "^2.11.0",
|
|
54
|
+
"@opentelemetry/sdk-logs": "^0.222.0",
|
|
55
|
+
"@opentelemetry/sdk-metrics": "^2.11.0",
|
|
56
|
+
"@opentelemetry/sdk-trace": "^2.11.0",
|
|
41
57
|
"smol-toml": "^1.8.0",
|
|
42
58
|
"turndown": "^7.2.4",
|
|
43
59
|
"typebox": "^1.3.15"
|
|
44
60
|
},
|
|
45
|
-
"yagniSourceSha": "
|
|
61
|
+
"yagniSourceSha": "49b4f1be0c81923619b1dd54a977da9653deee1d"
|
|
46
62
|
}
|