@rbbtsn0w/adg 0.3.0-beta.2 → 0.3.0-beta.4
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 +12 -127
- package/dist/src/adapters/anthropic.js +8 -16
- package/dist/src/adapters/{openai.js → codex.js} +4 -14
- package/dist/src/adapters/index.js +4 -2
- package/dist/src/adapters/reverse.js +25 -2
- package/dist/src/agents/claude.js +1 -4
- package/dist/src/commands/install.js +1 -4
- package/dist/src/fsutil.js +4 -0
- package/dist/src/render/plugins.js +100 -0
- package/dist/src/render/ui.js +51 -0
- package/dist/src/semver.js +76 -0
- package/dist/src/skills.js +31 -0
- package/dist/src/sources.js +2 -2
- package/dist/src/update-check.js +56 -10
- package/package.json +6 -5
package/dist/bin/adg.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
|
-
import pc from "picocolors";
|
|
4
3
|
import { spawnSync } from "node:child_process";
|
|
5
4
|
import { readFileSync, realpathSync } from "node:fs";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
5
|
import { dirname, join, resolve } from "node:path";
|
|
8
6
|
import { fileURLToPath } from "node:url";
|
|
9
7
|
import { checkForUpdate, formatUpdateNotice } from "../src/update-check.js";
|
|
@@ -23,24 +21,11 @@ import { selectTargetsInteractive } from "../src/commands/select-agents.js";
|
|
|
23
21
|
import { selectPluginsInteractive } from "../src/commands/select-plugins.js";
|
|
24
22
|
import { selectScopeInteractive } from "../src/commands/select-scope.js";
|
|
25
23
|
import { confirmFullInstall, selectComponentsInteractive } from "../src/commands/select-components.js";
|
|
26
|
-
import { globalPluginsDir,
|
|
24
|
+
import { globalPluginsDir, projectPluginsDir } from "../src/paths.js";
|
|
27
25
|
import { COMPONENT_TYPES } from "../src/types.js";
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// commands: cyan = primary identifiers (plugins, agents, sources), dim =
|
|
32
|
-
// secondary metadata (paths, hashes, sub-details), green = success, yellow =
|
|
33
|
-
// notes/warnings, red = errors, bold = section titles. picocolors auto-disables
|
|
34
|
-
// on non-TTY / NO_COLOR, so piped output and tests stay plain.
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
const ui = {
|
|
37
|
-
title: (s) => pc.bold(s),
|
|
38
|
-
name: (s) => pc.cyan(s),
|
|
39
|
-
meta: (s) => pc.dim(s),
|
|
40
|
-
ok: (s) => pc.green(s),
|
|
41
|
-
warn: (s) => pc.yellow(s),
|
|
42
|
-
err: (s) => pc.red(s),
|
|
43
|
-
};
|
|
26
|
+
import { getAgent } from "../src/agents/index.js";
|
|
27
|
+
import { ui } from "../src/render/ui.js";
|
|
28
|
+
import { renderAgentReport, renderMarketplaceList, renderPluginList, } from "../src/render/plugins.js";
|
|
44
29
|
const FLAGS = {
|
|
45
30
|
// Short flags are first-letter aliases. Where several long flags share a
|
|
46
31
|
// first letter, the highest-frequency one wins the short and the rest stay
|
|
@@ -243,16 +228,6 @@ function scopeInfo(values) {
|
|
|
243
228
|
function scopeOf(values) {
|
|
244
229
|
return values.global ? "user" : "project";
|
|
245
230
|
}
|
|
246
|
-
/** Print per-agent sync outcomes (enabled/disabled/re-synced) generically. */
|
|
247
|
-
function reportAgents(agents, verb) {
|
|
248
|
-
for (const r of agents ?? []) {
|
|
249
|
-
const name = getAgent(r.agent)?.displayName ?? r.agent;
|
|
250
|
-
if (r.affected.length > 0)
|
|
251
|
-
console.log(`${ui.ok(verb)} in ${ui.name(name)}: ${r.affected.join(", ")}`);
|
|
252
|
-
else if (r.skipped)
|
|
253
|
-
console.log(ui.warn(`note: \`${r.agent}\` CLI not found — run \`adg plugins link --target ${r.agent}\` after installing it.`));
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
231
|
/** Friendly `--target` aliases mapped onto canonical adapter target ids. */
|
|
257
232
|
const TARGET_ALIASES = {
|
|
258
233
|
anthropic: "claude",
|
|
@@ -294,45 +269,6 @@ function parseVerb(name, flags, rest) {
|
|
|
294
269
|
process.exit(1);
|
|
295
270
|
}
|
|
296
271
|
}
|
|
297
|
-
/**
|
|
298
|
-
* Lay items out in aligned columns sized to the terminal width (row-major).
|
|
299
|
-
* Items longer than `maxColWidth` are truncated with an ellipsis. Falls back to
|
|
300
|
-
* a single column on narrow terminals. Returns the block as a string.
|
|
301
|
-
*/
|
|
302
|
-
function formatColumns(items, opts = {}) {
|
|
303
|
-
const indent = opts.indent ?? 2;
|
|
304
|
-
const gutter = opts.gutter ?? 2;
|
|
305
|
-
const maxColWidth = opts.maxColWidth ?? 24;
|
|
306
|
-
const termWidth = opts.width ?? process.stdout.columns ?? 80;
|
|
307
|
-
const cells = items.map((s) => (s.length > maxColWidth ? s.slice(0, maxColWidth - 1) + "…" : s));
|
|
308
|
-
const colWidth = Math.min(Math.max(1, ...cells.map((c) => c.length)), maxColWidth);
|
|
309
|
-
const cols = Math.max(1, Math.floor((termWidth - indent + gutter) / (colWidth + gutter)));
|
|
310
|
-
const lines = [];
|
|
311
|
-
for (let i = 0; i < cells.length; i += cols) {
|
|
312
|
-
const row = cells.slice(i, i + cols);
|
|
313
|
-
const padded = row.map((c, j) => (j === row.length - 1 ? c : c.padEnd(colWidth)));
|
|
314
|
-
lines.push(" ".repeat(indent) + padded.join(" ".repeat(gutter)));
|
|
315
|
-
}
|
|
316
|
-
return lines.join("\n");
|
|
317
|
-
}
|
|
318
|
-
/** Abbreviate the home-directory prefix of an absolute path to `~` (POSIX `/` or Windows `\`). */
|
|
319
|
-
function abbrevHome(p) {
|
|
320
|
-
const home = homedir();
|
|
321
|
-
if (p === home)
|
|
322
|
-
return "~";
|
|
323
|
-
if (p.startsWith(home + "/") || p.startsWith(home + "\\"))
|
|
324
|
-
return "~" + p.slice(home.length);
|
|
325
|
-
return p;
|
|
326
|
-
}
|
|
327
|
-
/** Print a plugin's components, each expanded to its member names (verbose view). */
|
|
328
|
-
function printContents(contents, headerIndent) {
|
|
329
|
-
const entries = Object.entries(contents ?? {}).filter(([, names]) => names.length > 0);
|
|
330
|
-
for (const [type, names] of entries) {
|
|
331
|
-
const maxColWidth = Math.max(1, ...names.map((n) => n.length));
|
|
332
|
-
console.log(`${" ".repeat(headerIndent)}${ui.name(type)} ${ui.meta(`(${names.length}):`)}`);
|
|
333
|
-
console.log(formatColumns(names, { indent: headerIndent + 2, maxColWidth }));
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
272
|
async function runPlugins(rawVerb, rest) {
|
|
337
273
|
// `adg plugins` (no verb) or an explicit help request → the L1 overview.
|
|
338
274
|
if (rawVerb === undefined || rawVerb === "-h" || rawVerb === "--help" || rawVerb === "help") {
|
|
@@ -447,7 +383,8 @@ async function runPlugins(rawVerb, rest) {
|
|
|
447
383
|
for (const f of res.adapted)
|
|
448
384
|
console.log(ui.meta(` adapted: ${f}`));
|
|
449
385
|
}
|
|
450
|
-
|
|
386
|
+
for (const line of renderAgentReport(agents, "enabled"))
|
|
387
|
+
console.log(line);
|
|
451
388
|
return;
|
|
452
389
|
}
|
|
453
390
|
case "import-skills": {
|
|
@@ -494,7 +431,8 @@ async function runPlugins(rawVerb, rest) {
|
|
|
494
431
|
console.log(`${r.changed ? ui.ok("updated") : ui.meta("unchanged")} ${ui.name(`${r.name}@${r.version}`)}`);
|
|
495
432
|
for (const m of missing)
|
|
496
433
|
console.error(ui.warn(` ! missing directory for locked plugin: ${m}`));
|
|
497
|
-
|
|
434
|
+
for (const line of renderAgentReport(agents, "re-synced"))
|
|
435
|
+
console.log(line);
|
|
498
436
|
return;
|
|
499
437
|
}
|
|
500
438
|
case "remove": {
|
|
@@ -528,43 +466,8 @@ async function runPlugins(rawVerb, rest) {
|
|
|
528
466
|
const { values } = parseVerb(verb, cmd.flags, rest);
|
|
529
467
|
const pluginsDir = resolveScopeDir(values);
|
|
530
468
|
const plugins = listPlugins(pluginsDir);
|
|
531
|
-
|
|
532
|
-
console.log(
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
// Pre-compute each plugin's display row so the name/path columns can be
|
|
536
|
-
// aligned across rows (à la `adg skills list`). The `Agents:` column is
|
|
537
|
-
// derived from the exposed component types — which agents can adapt it.
|
|
538
|
-
const PATH_MAX = 44;
|
|
539
|
-
const rows = plugins.map((p) => {
|
|
540
|
-
const exposed = Object.entries(p.contents ?? {}).filter(([, names]) => names.length > 0);
|
|
541
|
-
const types = exposed.map(([type]) => type);
|
|
542
|
-
const agents = agentsForComponents(types).map((a) => a.displayName);
|
|
543
|
-
return {
|
|
544
|
-
p,
|
|
545
|
-
label: `${p.name}@${p.version}`,
|
|
546
|
-
path: abbrevHome(installedPluginDir(pluginsDir, p.name, p.origin)),
|
|
547
|
-
agents: agents.length > 0 ? agents.join(", ") : "—",
|
|
548
|
-
counts: exposed.map(([type, names]) => `${type}: ${names.length}`),
|
|
549
|
-
};
|
|
550
|
-
});
|
|
551
|
-
const nameW = Math.max(...rows.map((r) => r.label.length));
|
|
552
|
-
const pathW = Math.min(PATH_MAX, Math.max(...rows.map((r) => r.path.length)));
|
|
553
|
-
const ellip = (s, w) => (s.length > w ? "…" + s.slice(s.length - w + 1) : s);
|
|
554
|
-
// Color mirrors `adg skills list`: cyan name, dim path / dim "Agents:"
|
|
555
|
-
// label with the agent names left bright, and the provenance/counts line
|
|
556
|
-
// fully dimmed as secondary metadata. Widths are measured on the uncolored
|
|
557
|
-
// strings (above), so wrapping the padded text keeps columns aligned.
|
|
558
|
-
// picocolors auto-disables on non-TTY / NO_COLOR, so pipes stay plain.
|
|
559
|
-
for (const r of rows) {
|
|
560
|
-
const partial = r.p.selection ? " (partial)" : "";
|
|
561
|
-
const name = ui.name(r.label.padEnd(nameW));
|
|
562
|
-
const path = ui.meta(ellip(r.path, pathW).padEnd(pathW));
|
|
563
|
-
console.log(`${name} ${path} ${ui.meta("Agents:")} ${r.agents}`);
|
|
564
|
-
const provenance = `[${r.p.origin.type}] ${r.p.folderHash.slice(0, 19)}${partial}`;
|
|
565
|
-
console.log(ui.meta(` ${[provenance, ...r.counts].join(" ")}`));
|
|
566
|
-
if (values.verbose)
|
|
567
|
-
printContents(r.p.contents, 4);
|
|
469
|
+
for (const line of renderPluginList(plugins, pluginsDir, { verbose: values.verbose })) {
|
|
470
|
+
console.log(line);
|
|
568
471
|
}
|
|
569
472
|
return;
|
|
570
473
|
}
|
|
@@ -618,28 +521,10 @@ async function runMarketplace(args) {
|
|
|
618
521
|
const { values } = parseVerb("marketplace", ["verbose", ...SCOPE], rest);
|
|
619
522
|
const dir = resolveScopeDir(values);
|
|
620
523
|
const groups = marketplaceList({ pluginsDir: dir });
|
|
621
|
-
if (groups.length === 0) {
|
|
622
|
-
console.log(ui.meta("No plugins installed."));
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
524
|
// Verbose: drill each plugin down to its components (reuses `plugins list -v`).
|
|
626
525
|
const byName = values.verbose ? new Map(listPlugins(dir).map((p) => [p.name, p])) : undefined;
|
|
627
|
-
for (const
|
|
628
|
-
|
|
629
|
-
const n = g.installed.length;
|
|
630
|
-
const tag = g.remote ? "" : ui.warn(" (local — re-run add to update)");
|
|
631
|
-
console.log(`${ui.name(`${g.source}${ref}`)} ${ui.meta(`(${n} plugin${n !== 1 ? "s" : ""})`)}${tag}`);
|
|
632
|
-
if (byName) {
|
|
633
|
-
for (const name of g.installed) {
|
|
634
|
-
const p = byName.get(name);
|
|
635
|
-
console.log(` ${ui.name(name)}${p?.selection ? ui.meta(" (partial)") : ""}`);
|
|
636
|
-
printContents(p?.contents, 4);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
else {
|
|
640
|
-
console.log(formatColumns(g.installed));
|
|
641
|
-
}
|
|
642
|
-
}
|
|
526
|
+
for (const line of renderMarketplaceList(groups, byName))
|
|
527
|
+
console.log(line);
|
|
643
528
|
return;
|
|
644
529
|
}
|
|
645
530
|
case "upgrade": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveProjectedSkills } from "../skills.js";
|
|
3
3
|
import { isExposed } from "../components.js";
|
|
4
4
|
/**
|
|
5
5
|
* Generate a Claude (.claude-plugin/plugin.json) manifest from an ADG manifest.
|
|
@@ -31,24 +31,16 @@ export function toAnthropicManifest(pluginDir, manifest, selection) {
|
|
|
31
31
|
out.hooks = manifest.hooks;
|
|
32
32
|
if (manifest.mcp && isExposed(selection, "mcp"))
|
|
33
33
|
out.mcp = manifest.mcp;
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// Claude's array form is already `./skills/<id>` paths, so a strict array is
|
|
35
|
+
// passed through verbatim; an explicit id list (selection or strict:false) is
|
|
36
|
+
// mapped to paths and marks the manifest non-strict.
|
|
37
|
+
const projected = resolveProjectedSkills(pluginDir, manifest, selection, { passthroughArray: true });
|
|
38
|
+
if (projected.explicit) {
|
|
36
39
|
out.strict = false;
|
|
37
|
-
|
|
38
|
-
? selection.skills ?? resolveSkills(pluginDir, manifest)
|
|
39
|
-
: [];
|
|
40
|
-
out.skills = names.map((name) => `./skills/${name}`);
|
|
40
|
+
out.skills = projected.skills.map((name) => `./skills/${name}`);
|
|
41
41
|
}
|
|
42
42
|
else {
|
|
43
|
-
|
|
44
|
-
if (strict && manifest.skills !== undefined) {
|
|
45
|
-
out.skills = manifest.skills;
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
// skill-bundle form: explicit list, strict:false
|
|
49
|
-
out.strict = false;
|
|
50
|
-
out.skills = resolveSkills(pluginDir, manifest).map((name) => `./skills/${name}`);
|
|
51
|
-
}
|
|
43
|
+
out.skills = projected.skills;
|
|
52
44
|
}
|
|
53
45
|
return { defaultPath: join(".claude-plugin", "plugin.json"), manifest: out };
|
|
54
46
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import { isExposed } from "../components.js";
|
|
2
|
+
import { resolveProjectedSkills } from "../skills.js";
|
|
4
3
|
/**
|
|
5
4
|
* Generate a Codex (.codex-plugin/plugin.json) manifest from an ADG manifest.
|
|
6
5
|
*
|
|
@@ -14,18 +13,9 @@ import { isExposed } from "../components.js";
|
|
|
14
13
|
* rather than passed through. Codex only consumes skills.
|
|
15
14
|
*/
|
|
16
15
|
export function toCodexManifest(pluginDir, manifest, selection) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
? selection.skills ?? resolveSkills(pluginDir, manifest)
|
|
21
|
-
: [];
|
|
22
|
-
}
|
|
23
|
-
else {
|
|
24
|
-
const strict = manifest.strict !== false;
|
|
25
|
-
skills = strict && typeof manifest.skills === "string"
|
|
26
|
-
? manifest.skills
|
|
27
|
-
: resolveSkills(pluginDir, manifest);
|
|
28
|
-
}
|
|
16
|
+
// Codex's array form is bare ids, so a strict skills *array* is resolved to
|
|
17
|
+
// ids rather than passed through; only a declared root string passes through.
|
|
18
|
+
const { skills } = resolveProjectedSkills(pluginDir, manifest, selection, { passthroughArray: false });
|
|
29
19
|
const out = {
|
|
30
20
|
name: manifest.name,
|
|
31
21
|
version: manifest.version,
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { toAnthropicManifest } from "./anthropic.js";
|
|
2
|
-
import { toCodexManifest } from "./
|
|
2
|
+
import { toCodexManifest } from "./codex.js";
|
|
3
3
|
import { toAntigravityManifest } from "./antigravity.js";
|
|
4
|
+
// `anthropic` is kept as a synonym because Claude's plugin.json *is* the
|
|
5
|
+
// "anthropic" manifest shape. There is deliberately no `openai` key: the runtime
|
|
6
|
+
// is Codex, and an `openai` alias would imply OpenAI support that does not exist.
|
|
4
7
|
export const ADAPTERS = {
|
|
5
8
|
claude: toAnthropicManifest,
|
|
6
9
|
anthropic: toAnthropicManifest,
|
|
7
10
|
codex: toCodexManifest,
|
|
8
|
-
openai: toCodexManifest,
|
|
9
11
|
antigravity: toAntigravityManifest,
|
|
10
12
|
agy: toAntigravityManifest,
|
|
11
13
|
gemini: toAntigravityManifest,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ADG_SCHEMA_VERSION } from "../types.js";
|
|
2
2
|
import { validateManifest } from "../manifest.js";
|
|
3
|
+
import { toPosix } from "../fsutil.js";
|
|
3
4
|
/**
|
|
4
5
|
* Reverse-adapt a runtime-native manifest (.claude-plugin/plugin.json or
|
|
5
6
|
* .codex-plugin/plugin.json) into a canonical ADG manifest. This is the inverse
|
|
@@ -7,8 +8,14 @@ import { validateManifest } from "../manifest.js";
|
|
|
7
8
|
*
|
|
8
9
|
* Missing `version` falls back to 0.0.0 (callers may override with a git SHA);
|
|
9
10
|
* skills normalize to the manifest's array/string or the default ./skills/.
|
|
11
|
+
*
|
|
12
|
+
* `kind` disambiguates the two native skills-array conventions: Claude arrays are
|
|
13
|
+
* already `./skills/<id>` paths, while Codex arrays are bare ids. Both canonicalize
|
|
14
|
+
* to ADG's path-array contract so a later cross-adapt (e.g. codex → ADG → claude)
|
|
15
|
+
* emits valid `./skills/<id>` entries instead of leaking bare ids into a Claude
|
|
16
|
+
* manifest.
|
|
10
17
|
*/
|
|
11
|
-
export function fromNativeManifest(raw,
|
|
18
|
+
export function fromNativeManifest(raw, kind) {
|
|
12
19
|
if (typeof raw !== "object" || raw === null) {
|
|
13
20
|
throw new Error("native manifest must be a JSON object");
|
|
14
21
|
}
|
|
@@ -35,9 +42,16 @@ export function fromNativeManifest(raw, _kind) {
|
|
|
35
42
|
else if (typeof n.author === "string") {
|
|
36
43
|
manifest.author = { name: n.author };
|
|
37
44
|
}
|
|
38
|
-
if (typeof n.skills === "string"
|
|
45
|
+
if (typeof n.skills === "string") {
|
|
39
46
|
manifest.skills = n.skills;
|
|
40
47
|
}
|
|
48
|
+
else if (isStringArray(n.skills)) {
|
|
49
|
+
// Codex arrays are bare ids; map them to ADG's `./skills/<id>` path form.
|
|
50
|
+
// Claude arrays are already paths, but a Windows-authored manifest may use
|
|
51
|
+
// backslashes, so normalize separators to keep ADG manifests POSIX-pathed
|
|
52
|
+
// (downstream `resolveSkillEntries` splits on `/`).
|
|
53
|
+
manifest.skills = kind === "codex" ? n.skills.map(toSkillPath) : n.skills.map(toPosix);
|
|
54
|
+
}
|
|
41
55
|
else {
|
|
42
56
|
manifest.skills = "./skills/";
|
|
43
57
|
}
|
|
@@ -51,3 +65,12 @@ function copyIfString(src, dst, key) {
|
|
|
51
65
|
function isStringArray(v) {
|
|
52
66
|
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
53
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Canonicalize a skill reference (bare id or path) to ADG's `./skills/<id>` form.
|
|
70
|
+
* Accepts both `/` and `\` separators so a Windows-authored native manifest
|
|
71
|
+
* (e.g. `skills\\foo`) still yields a valid `./skills/foo` entry.
|
|
72
|
+
*/
|
|
73
|
+
function toSkillPath(ref) {
|
|
74
|
+
const name = ref.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || ref;
|
|
75
|
+
return `./skills/${name}`;
|
|
76
|
+
}
|
|
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, relative } from "node:path";
|
|
5
|
-
import { writeJson } from "../fsutil.js";
|
|
5
|
+
import { toPosix, writeJson } from "../fsutil.js";
|
|
6
6
|
import { readManifest } from "../manifest.js";
|
|
7
7
|
import { installedPluginDir, lockPath } from "../paths.js";
|
|
8
8
|
import { readLock } from "../lock.js";
|
|
@@ -15,9 +15,6 @@ import { readLock } from "../lock.js";
|
|
|
15
15
|
* versions) rather than hand-editing Claude's internal state.
|
|
16
16
|
*/
|
|
17
17
|
const MARKETPLACE = "adg";
|
|
18
|
-
function toPosix(p) {
|
|
19
|
-
return p.split("\\").join("/");
|
|
20
|
-
}
|
|
21
18
|
function claudeHome(env) {
|
|
22
19
|
return env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude");
|
|
23
20
|
}
|
|
@@ -4,7 +4,7 @@ import { basename, join, relative, resolve } from "node:path";
|
|
|
4
4
|
import { ADAPTER_TARGETS } from "../adapters/index.js";
|
|
5
5
|
import { fromNativeManifest } from "../adapters/reverse.js";
|
|
6
6
|
import { adaptPlugin } from "./adapt.js";
|
|
7
|
-
import { copyPluginDir, writeJson } from "../fsutil.js";
|
|
7
|
+
import { copyPluginDir, toPosix, writeJson } from "../fsutil.js";
|
|
8
8
|
import { folderHash } from "../hash.js";
|
|
9
9
|
import { packageFilter, PROJECTION_DIRS } from "../package.js";
|
|
10
10
|
import { lockPath, marketplacePath, marketplaceSourcePath, pluginDir } from "../paths.js";
|
|
@@ -19,9 +19,6 @@ import { skillDescriptionLoader } from "../skills.js";
|
|
|
19
19
|
import { resolveAgents } from "../agents/index.js";
|
|
20
20
|
// Generated runtime projections never count toward a plugin's content hash.
|
|
21
21
|
const HASH_IGNORE = PROJECTION_DIRS;
|
|
22
|
-
function toPosix(p) {
|
|
23
|
-
return p.split("\\").join("/");
|
|
24
|
-
}
|
|
25
22
|
/**
|
|
26
23
|
* Install a single local plugin directory into a plugins directory: copy the
|
|
27
24
|
* source, generate adapter manifests, compute the folder hash, and update both
|
package/dist/src/fsutil.js
CHANGED
|
@@ -3,6 +3,10 @@ import { dirname, relative } from "node:path";
|
|
|
3
3
|
export function ensureDir(dir) {
|
|
4
4
|
mkdirSync(dir, { recursive: true });
|
|
5
5
|
}
|
|
6
|
+
/** Normalize a path to forward slashes (stable across Windows and POSIX hosts). */
|
|
7
|
+
export function toPosix(p) {
|
|
8
|
+
return p.split("\\").join("/");
|
|
9
|
+
}
|
|
6
10
|
export function writeJson(file, value) {
|
|
7
11
|
ensureDir(dirname(file));
|
|
8
12
|
writeFileSync(file, JSON.stringify(value, null, 2) + "\n");
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ui, formatColumns, abbrevHome, ellipsizeStart } from "./ui.js";
|
|
2
|
+
import { installedPluginDir } from "../paths.js";
|
|
3
|
+
import { agentsForComponents, getAgent } from "../agents/index.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Presentation layer for `adg plugins`. Each function turns command-layer data
|
|
6
|
+
// into terminal-ready lines (returned as string[]), so bin/adg.ts only parses
|
|
7
|
+
// args, calls a command, prints the lines — and the formatting is unit-testable
|
|
8
|
+
// without spawning the CLI. Color mirrors `adg skills list` throughout.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
/** A plugin's components, each expanded to its member names (verbose view). */
|
|
11
|
+
export function renderContents(contents, headerIndent) {
|
|
12
|
+
const out = [];
|
|
13
|
+
const entries = Object.entries(contents ?? {}).filter(([, names]) => names.length > 0);
|
|
14
|
+
for (const [type, names] of entries) {
|
|
15
|
+
const maxColWidth = Math.max(1, ...names.map((n) => n.length));
|
|
16
|
+
out.push(`${" ".repeat(headerIndent)}${ui.name(type)} ${ui.meta(`(${names.length}):`)}`);
|
|
17
|
+
// formatColumns returns one string with embedded newlines; split so `out`
|
|
18
|
+
// stays a flat list of single lines.
|
|
19
|
+
out.push(...formatColumns(names, { indent: headerIndent + 2, maxColWidth }).split("\n"));
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
/** Per-agent sync outcomes (enabled/disabled/re-synced), printed generically. */
|
|
24
|
+
export function renderAgentReport(agents, verb) {
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const r of agents ?? []) {
|
|
27
|
+
const name = getAgent(r.agent)?.displayName ?? r.agent;
|
|
28
|
+
if (r.affected.length > 0)
|
|
29
|
+
out.push(`${ui.ok(verb)} in ${ui.name(name)}: ${r.affected.join(", ")}`);
|
|
30
|
+
else if (r.skipped)
|
|
31
|
+
out.push(ui.warn(`note: \`${r.agent}\` CLI not found — run \`adg plugins link --target ${r.agent}\` after installing it.`));
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
const PATH_MAX = 44;
|
|
36
|
+
/** `adg plugins list` — aligned name/path/agents rows, with optional verbose contents. */
|
|
37
|
+
export function renderPluginList(plugins, pluginsDir, opts = {}) {
|
|
38
|
+
if (plugins.length === 0)
|
|
39
|
+
return [ui.meta(`no plugins recorded in ${pluginsDir}`)];
|
|
40
|
+
// Pre-compute each plugin's display row so the name/path columns can be
|
|
41
|
+
// aligned across rows (à la `adg skills list`). The `Agents:` column is
|
|
42
|
+
// derived from the exposed component types — which agents can adapt it.
|
|
43
|
+
const rows = plugins.map((p) => {
|
|
44
|
+
const exposed = Object.entries(p.contents ?? {}).filter(([, names]) => names.length > 0);
|
|
45
|
+
const types = exposed.map(([type]) => type);
|
|
46
|
+
const agents = agentsForComponents(types).map((a) => a.displayName);
|
|
47
|
+
return {
|
|
48
|
+
p,
|
|
49
|
+
label: `${p.name}@${p.version}`,
|
|
50
|
+
path: abbrevHome(installedPluginDir(pluginsDir, p.name, p.origin)),
|
|
51
|
+
agents: agents.length > 0 ? agents.join(", ") : "—",
|
|
52
|
+
counts: exposed.map(([type, names]) => `${type}: ${names.length}`),
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
const nameW = Math.max(...rows.map((r) => r.label.length));
|
|
56
|
+
const pathW = Math.min(PATH_MAX, Math.max(...rows.map((r) => r.path.length)));
|
|
57
|
+
// Color mirrors `adg skills list`: cyan name, dim path / dim "Agents:" label
|
|
58
|
+
// with the agent names left bright, and the provenance/counts line fully
|
|
59
|
+
// dimmed as secondary metadata. Widths are measured on the uncolored strings
|
|
60
|
+
// (above), so wrapping the padded text keeps columns aligned.
|
|
61
|
+
const out = [];
|
|
62
|
+
for (const r of rows) {
|
|
63
|
+
const partial = r.p.selection ? " (partial)" : "";
|
|
64
|
+
const name = ui.name(r.label.padEnd(nameW));
|
|
65
|
+
const path = ui.meta(ellipsizeStart(r.path, pathW).padEnd(pathW));
|
|
66
|
+
out.push(`${name} ${path} ${ui.meta("Agents:")} ${r.agents}`);
|
|
67
|
+
const provenance = `[${r.p.origin.type}] ${(r.p.folderHash ?? "").slice(0, 19)}${partial}`;
|
|
68
|
+
out.push(ui.meta(` ${[provenance, ...r.counts].join(" ")}`));
|
|
69
|
+
if (opts.verbose)
|
|
70
|
+
out.push(...renderContents(r.p.contents, 4));
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* `adg plugins marketplace list` — installed plugins grouped by source.
|
|
76
|
+
* `details` (name → ListedPlugin) enables the verbose per-plugin component drill;
|
|
77
|
+
* pass it only when --verbose is set.
|
|
78
|
+
*/
|
|
79
|
+
export function renderMarketplaceList(groups, details) {
|
|
80
|
+
if (groups.length === 0)
|
|
81
|
+
return [ui.meta("No plugins installed.")];
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const g of groups) {
|
|
84
|
+
const ref = g.ref ? `@${g.ref}` : "";
|
|
85
|
+
const n = g.installed.length;
|
|
86
|
+
const tag = g.remote ? "" : ui.warn(" (local — re-run add to update)");
|
|
87
|
+
out.push(`${ui.name(`${g.source}${ref}`)} ${ui.meta(`(${n} plugin${n !== 1 ? "s" : ""})`)}${tag}`);
|
|
88
|
+
if (details) {
|
|
89
|
+
for (const name of g.installed) {
|
|
90
|
+
const p = details.get(name);
|
|
91
|
+
out.push(` ${ui.name(name)}${p?.selection ? ui.meta(" (partial)") : ""}`);
|
|
92
|
+
out.push(...renderContents(p?.contents, 4));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
out.push(...formatColumns(g.installed).split("\n"));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Semantic colors, mirroring `adg skills list` so output reads the same across
|
|
5
|
+
// commands: cyan = primary identifiers (plugins, agents, sources), dim =
|
|
6
|
+
// secondary metadata (paths, hashes, sub-details), green = success, yellow =
|
|
7
|
+
// notes/warnings, red = errors, bold = section titles. picocolors auto-disables
|
|
8
|
+
// on non-TTY / NO_COLOR, so piped output and tests stay plain.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export const ui = {
|
|
11
|
+
title: (s) => pc.bold(s),
|
|
12
|
+
name: (s) => pc.cyan(s),
|
|
13
|
+
meta: (s) => pc.dim(s),
|
|
14
|
+
ok: (s) => pc.green(s),
|
|
15
|
+
warn: (s) => pc.yellow(s),
|
|
16
|
+
err: (s) => pc.red(s),
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Lay items out in aligned columns sized to the terminal width (row-major).
|
|
20
|
+
* Items longer than `maxColWidth` are truncated with an ellipsis. Falls back to
|
|
21
|
+
* a single column on narrow terminals. Returns the block as a string.
|
|
22
|
+
*/
|
|
23
|
+
export function formatColumns(items, opts = {}) {
|
|
24
|
+
const indent = opts.indent ?? 2;
|
|
25
|
+
const gutter = opts.gutter ?? 2;
|
|
26
|
+
const maxColWidth = opts.maxColWidth ?? 24;
|
|
27
|
+
const termWidth = opts.width ?? process.stdout.columns ?? 80;
|
|
28
|
+
const cells = items.map((s) => (s.length > maxColWidth ? s.slice(0, maxColWidth - 1) + "…" : s));
|
|
29
|
+
const colWidth = Math.min(Math.max(1, ...cells.map((c) => c.length)), maxColWidth);
|
|
30
|
+
const cols = Math.max(1, Math.floor((termWidth - indent + gutter) / (colWidth + gutter)));
|
|
31
|
+
const lines = [];
|
|
32
|
+
for (let i = 0; i < cells.length; i += cols) {
|
|
33
|
+
const row = cells.slice(i, i + cols);
|
|
34
|
+
const padded = row.map((c, j) => (j === row.length - 1 ? c : c.padEnd(colWidth)));
|
|
35
|
+
lines.push(" ".repeat(indent) + padded.join(" ".repeat(gutter)));
|
|
36
|
+
}
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
/** Abbreviate the home-directory prefix of an absolute path to `~` (POSIX `/` or Windows `\`). */
|
|
40
|
+
export function abbrevHome(p) {
|
|
41
|
+
const home = homedir();
|
|
42
|
+
if (p === home)
|
|
43
|
+
return "~";
|
|
44
|
+
if (p.startsWith(home + "/") || p.startsWith(home + "\\"))
|
|
45
|
+
return "~" + p.slice(home.length);
|
|
46
|
+
return p;
|
|
47
|
+
}
|
|
48
|
+
/** Tail-truncate a string to width `w`, prefixing an ellipsis when it overflows. */
|
|
49
|
+
export function ellipsizeStart(s, w) {
|
|
50
|
+
return s.length > w ? "…" + s.slice(s.length - w + 1) : s;
|
|
51
|
+
}
|
package/dist/src/semver.js
CHANGED
|
@@ -19,6 +19,82 @@ export function compare(a, b) {
|
|
|
19
19
|
}
|
|
20
20
|
return 0;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Extract the dot-separated pre-release identifiers from a version string.
|
|
24
|
+
*
|
|
25
|
+
* Returns the segment after the first `-` (and before any `+` build metadata),
|
|
26
|
+
* with numeric-only identifiers coerced to `number`. A stable version (no
|
|
27
|
+
* pre-release) yields `[]`.
|
|
28
|
+
*/
|
|
29
|
+
export function parsePrerelease(v) {
|
|
30
|
+
// Drop build metadata first so a hyphen inside it (e.g. `1.2.3+build-1`) is
|
|
31
|
+
// not mistaken for the pre-release separator.
|
|
32
|
+
const withoutBuild = v.split("+")[0] ?? "";
|
|
33
|
+
const dashIndex = withoutBuild.indexOf("-");
|
|
34
|
+
if (dashIndex === -1)
|
|
35
|
+
return [];
|
|
36
|
+
const pre = withoutBuild.slice(dashIndex + 1);
|
|
37
|
+
if (pre === "")
|
|
38
|
+
return [];
|
|
39
|
+
return pre.split(".").map((id) => (/^\d+$/.test(id) ? Number(id) : id));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Compare two pre-release identifier lists per SemVer §11.
|
|
43
|
+
*
|
|
44
|
+
* A stable version (empty list) outranks any pre-release. Otherwise identifiers
|
|
45
|
+
* are compared left to right: numeric identifiers rank below alphanumeric ones,
|
|
46
|
+
* numerics compare numerically, strings compare by ASCII, and when every shared
|
|
47
|
+
* field is equal the longer list wins.
|
|
48
|
+
*/
|
|
49
|
+
export function comparePrerelease(a, b) {
|
|
50
|
+
if (a.length === 0 && b.length === 0)
|
|
51
|
+
return 0;
|
|
52
|
+
if (a.length === 0)
|
|
53
|
+
return 1; // stable > pre-release
|
|
54
|
+
if (b.length === 0)
|
|
55
|
+
return -1;
|
|
56
|
+
const len = Math.min(a.length, b.length);
|
|
57
|
+
for (let i = 0; i < len; i++) {
|
|
58
|
+
const x = a[i];
|
|
59
|
+
const y = b[i];
|
|
60
|
+
if (x === y)
|
|
61
|
+
continue;
|
|
62
|
+
const xNum = typeof x === "number";
|
|
63
|
+
const yNum = typeof y === "number";
|
|
64
|
+
if (xNum && yNum)
|
|
65
|
+
return x < y ? -1 : 1;
|
|
66
|
+
if (xNum !== yNum)
|
|
67
|
+
return xNum ? -1 : 1; // numeric < alphanumeric
|
|
68
|
+
return x < y ? -1 : 1;
|
|
69
|
+
}
|
|
70
|
+
// All shared fields equal: the list with more fields has higher precedence.
|
|
71
|
+
return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Full version comparison including pre-release precedence.
|
|
75
|
+
*
|
|
76
|
+
* Unlike {@link compare}, this honors the pre-release suffix so that, e.g.,
|
|
77
|
+
* `0.3.0-beta.2 < 0.3.0-beta.3 < 0.3.0`. Used by the update check; range
|
|
78
|
+
* matching ({@link satisfies}) deliberately ignores pre-release.
|
|
79
|
+
*/
|
|
80
|
+
export function compareVersions(a, b) {
|
|
81
|
+
const core = compare(parseVersion(a), parseVersion(b));
|
|
82
|
+
if (core !== 0)
|
|
83
|
+
return core;
|
|
84
|
+
return comparePrerelease(parsePrerelease(a), parsePrerelease(b));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Return the pre-release channel of a version — its first non-numeric
|
|
88
|
+
* pre-release identifier (e.g. `0.3.0-beta.2` -> `"beta"`), or `null` for a
|
|
89
|
+
* stable version. Used to pick the matching npm dist-tag.
|
|
90
|
+
*/
|
|
91
|
+
export function prereleaseChannel(v) {
|
|
92
|
+
for (const id of parsePrerelease(v)) {
|
|
93
|
+
if (typeof id === "string")
|
|
94
|
+
return id;
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
22
98
|
export function satisfies(version, range) {
|
|
23
99
|
const v = parseVersion(version);
|
|
24
100
|
const r = range.trim();
|
package/dist/src/skills.js
CHANGED
|
@@ -42,6 +42,37 @@ export function resolveSkillEntries(pluginDir, manifest) {
|
|
|
42
42
|
export function resolveSkills(pluginDir, manifest) {
|
|
43
43
|
return resolveSkillEntries(pluginDir, manifest).map((e) => e.name);
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Decide the skills a forward adapter should project from an ADG manifest, the
|
|
47
|
+
* one strict/selection decision both adapters share (previously copy-pasted, so
|
|
48
|
+
* it could drift — see the Codex/Claude strict regression that motivated it).
|
|
49
|
+
*
|
|
50
|
+
* In the default strict case the declared skills *root* string is passed through
|
|
51
|
+
* (the runtime discovers skills from the directory); an omitted `skills` is
|
|
52
|
+
* treated as the default `./skills/` root so the strict default stays
|
|
53
|
+
* directory-discovery rather than an explicit enumeration. A `selection` or
|
|
54
|
+
* `strict: false` always yields an explicit resolved id list. A declared `skills`
|
|
55
|
+
* *array* is runtime-dependent: `passthroughArray` keeps it verbatim (Claude,
|
|
56
|
+
* whose entries are already `./skills/<id>` paths), otherwise it resolves to ids
|
|
57
|
+
* (Codex, whose array form is bare ids).
|
|
58
|
+
*/
|
|
59
|
+
export function resolveProjectedSkills(pluginDir, manifest, selection, opts) {
|
|
60
|
+
if (selection) {
|
|
61
|
+
const skills = selection.components.includes("skills")
|
|
62
|
+
? selection.skills ?? resolveSkills(pluginDir, manifest)
|
|
63
|
+
: [];
|
|
64
|
+
return { skills, explicit: true };
|
|
65
|
+
}
|
|
66
|
+
const strict = manifest.strict !== false;
|
|
67
|
+
// An omitted `skills` defaults to the `./skills/` root, so a strict manifest
|
|
68
|
+
// without a declaration keeps directory discovery instead of being forced to
|
|
69
|
+
// an explicit `strict: false` enumeration.
|
|
70
|
+
const declared = manifest.skills ?? "./skills/";
|
|
71
|
+
const passthrough = strict && (typeof declared === "string" || (opts.passthroughArray && Array.isArray(declared)));
|
|
72
|
+
if (passthrough)
|
|
73
|
+
return { skills: declared, explicit: false };
|
|
74
|
+
return { skills: resolveSkills(pluginDir, manifest), explicit: true };
|
|
75
|
+
}
|
|
45
76
|
/** Read a SKILL.md's `description` from its YAML frontmatter (undefined if absent). */
|
|
46
77
|
export function readSkillDescription(skillMd) {
|
|
47
78
|
try {
|
package/dist/src/sources.js
CHANGED
|
@@ -105,11 +105,11 @@ function walkNative(current, out) {
|
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
107
|
if (existsSync(claude)) {
|
|
108
|
-
out.push({ dir: current, kind: "
|
|
108
|
+
out.push({ dir: current, kind: "claude", manifestFile: claude });
|
|
109
109
|
return;
|
|
110
110
|
}
|
|
111
111
|
if (existsSync(codex)) {
|
|
112
|
-
out.push({ dir: current, kind: "
|
|
112
|
+
out.push({ dir: current, kind: "codex", manifestFile: codex });
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
115
115
|
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
package/dist/src/update-check.js
CHANGED
|
@@ -10,12 +10,20 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
10
10
|
import https from "node:https";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import {
|
|
13
|
+
import { compareVersions, prereleaseChannel } from "./semver.js";
|
|
14
14
|
const PACKAGE_NAME = "@rbbtsn0w/adg";
|
|
15
15
|
// URL-encode the slash in the scoped package name for the npm registry API.
|
|
16
|
-
|
|
16
|
+
// The abbreviated packument (vnd.npm.install-v1+json) is small and exposes
|
|
17
|
+
// `dist-tags`, which we need to follow the caller's release channel (e.g. the
|
|
18
|
+
// `beta` dist-tag for pre-release users, not just `latest`).
|
|
19
|
+
const REGISTRY_URL = `https://registry.npmjs.org/@rbbtsn0w%2Fadg`;
|
|
20
|
+
const REGISTRY_ACCEPT = "application/vnd.npm.install-v1+json";
|
|
17
21
|
const CACHE_FILENAME = "update-check.json";
|
|
18
22
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
23
|
+
// Cap the accumulated response body. The abbreviated packument is small, but a
|
|
24
|
+
// registry that ignores the abbreviated Accept header (or returns an unexpected
|
|
25
|
+
// payload) could stream a much larger body; abort rather than grow unbounded.
|
|
26
|
+
const MAX_RESPONSE_BYTES = 1024 * 1024; // 1 MiB
|
|
19
27
|
/** Resolve the directory that holds the update-check cache file. */
|
|
20
28
|
export function updateCacheDir(env = process.env) {
|
|
21
29
|
const stateHome = env.XDG_STATE_HOME ?? join(homedir(), ".local", "state");
|
|
@@ -46,6 +54,29 @@ export function writeUpdateCache(cache, env = process.env) {
|
|
|
46
54
|
// Ignore write errors (read-only FS, permissions, etc.)
|
|
47
55
|
}
|
|
48
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Pick the newest version relevant to the caller's release channel from the
|
|
59
|
+
* registry's `dist-tags`.
|
|
60
|
+
*
|
|
61
|
+
* Always considers `latest` (stable). When `currentVersion` is a pre-release
|
|
62
|
+
* (e.g. `0.3.0-beta.2`) it also considers the matching channel tag (e.g.
|
|
63
|
+
* `beta`), so pre-release users are notified of newer pre-releases as well as a
|
|
64
|
+
* newer stable. Returns the max candidate by pre-release-aware comparison, or
|
|
65
|
+
* `undefined` when no usable tag is present.
|
|
66
|
+
*/
|
|
67
|
+
export function resolveLatestForChannel(currentVersion, distTags) {
|
|
68
|
+
if (!distTags)
|
|
69
|
+
return undefined;
|
|
70
|
+
const candidates = [];
|
|
71
|
+
if (typeof distTags.latest === "string")
|
|
72
|
+
candidates.push(distTags.latest);
|
|
73
|
+
const channel = prereleaseChannel(currentVersion);
|
|
74
|
+
if (channel && typeof distTags[channel] === "string")
|
|
75
|
+
candidates.push(distTags[channel]);
|
|
76
|
+
if (candidates.length === 0)
|
|
77
|
+
return undefined;
|
|
78
|
+
return candidates.reduce((best, v) => (compareVersions(v, best) > 0 ? v : best));
|
|
79
|
+
}
|
|
49
80
|
/**
|
|
50
81
|
* Fire-and-forget background fetch of the latest version from the npm registry.
|
|
51
82
|
* The socket is unreffed so Node can exit naturally without waiting for the
|
|
@@ -53,14 +84,26 @@ export function writeUpdateCache(cache, env = process.env) {
|
|
|
53
84
|
*/
|
|
54
85
|
export function scheduleUpdateCacheRefresh(currentVersion, env = process.env) {
|
|
55
86
|
try {
|
|
56
|
-
const req = https.get(REGISTRY_URL, { headers: { "User-Agent": `adg/${currentVersion}`, Accept:
|
|
87
|
+
const req = https.get(REGISTRY_URL, { headers: { "User-Agent": `adg/${currentVersion}`, Accept: REGISTRY_ACCEPT } }, (res) => {
|
|
57
88
|
let body = "";
|
|
58
|
-
|
|
89
|
+
let byteCount = 0;
|
|
90
|
+
res.on("data", (chunk) => {
|
|
91
|
+
byteCount += Buffer.byteLength(chunk);
|
|
92
|
+
if (byteCount > MAX_RESPONSE_BYTES) {
|
|
93
|
+
// Oversized payload: stop reading and abort so we neither buffer
|
|
94
|
+
// unbounded memory nor parse a partial body.
|
|
95
|
+
body = "";
|
|
96
|
+
req.destroy();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
body += String(chunk);
|
|
100
|
+
});
|
|
59
101
|
res.on("end", () => {
|
|
60
102
|
try {
|
|
61
103
|
const data = JSON.parse(body);
|
|
62
|
-
|
|
63
|
-
|
|
104
|
+
const latestVersion = resolveLatestForChannel(currentVersion, data["dist-tags"]);
|
|
105
|
+
if (latestVersion !== undefined) {
|
|
106
|
+
writeUpdateCache({ latestVersion, checkedAt: new Date().toISOString() }, env);
|
|
64
107
|
}
|
|
65
108
|
}
|
|
66
109
|
catch {
|
|
@@ -103,9 +146,7 @@ export function checkForUpdate(currentVersion, env = process.env, refresh = sche
|
|
|
103
146
|
if (!cache)
|
|
104
147
|
return undefined;
|
|
105
148
|
try {
|
|
106
|
-
|
|
107
|
-
const latest = parseVersion(cache.latestVersion);
|
|
108
|
-
return compare(latest, current) > 0 ? cache.latestVersion : undefined;
|
|
149
|
+
return compareVersions(cache.latestVersion, currentVersion) > 0 ? cache.latestVersion : undefined;
|
|
109
150
|
}
|
|
110
151
|
catch {
|
|
111
152
|
return undefined;
|
|
@@ -113,6 +154,11 @@ export function checkForUpdate(currentVersion, env = process.env, refresh = sche
|
|
|
113
154
|
}
|
|
114
155
|
/** Format an update notice for display on stderr. */
|
|
115
156
|
export function formatUpdateNotice(currentVersion, latestVersion) {
|
|
157
|
+
// A pre-release suggestion lives on its channel dist-tag (e.g. `beta`), not
|
|
158
|
+
// `latest`; installing `@latest` would pull the stable release instead of the
|
|
159
|
+
// advertised version. Pin to the exact version so the right artifact installs.
|
|
160
|
+
const channel = prereleaseChannel(latestVersion);
|
|
161
|
+
const installTarget = channel ? latestVersion : "latest";
|
|
116
162
|
return (`\n Update available: ${currentVersion} → ${latestVersion}\n` +
|
|
117
|
-
` Run: npm install -g ${PACKAGE_NAME}
|
|
163
|
+
` Run: npm install -g ${PACKAGE_NAME}@${installTarget}\n`);
|
|
118
164
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rbbtsn0w/adg",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.4",
|
|
4
4
|
"description": "Agent Directory Group (ADG) toolkit — two domains: plugins and skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"adg": "node ./bin/adg.ts",
|
|
26
26
|
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json --noCheck && node -e \"require('fs').chmodSync('dist/bin/adg.js',0o755)\"",
|
|
27
27
|
"test": "node --test 'test/**/*.test.ts'",
|
|
28
|
+
"check:vendor-deps": "node scripts/check-vendor-deps.mjs",
|
|
28
29
|
"typecheck": "tsc --noEmit",
|
|
29
30
|
"prepare": "npm run typecheck",
|
|
30
31
|
"prepack": "npm run build"
|
|
@@ -35,12 +36,12 @@
|
|
|
35
36
|
"vendor",
|
|
36
37
|
"docs"
|
|
37
38
|
],
|
|
38
|
-
"comment:dependencies": "Runtime deps below are required only by the vendored skills CLI (vendor/skills)
|
|
39
|
+
"comment:dependencies": "Runtime deps below are required only by the vendored skills CLI (vendor/skills); ADG's own plugins code has zero runtime deps. Root is the single source of truth: each range MUST be >= the floor declared in vendor/skills/package.json, since that is the version the vendored source was authored against. `npm run check:vendor-deps` enforces this and fails on drift.",
|
|
39
40
|
"dependencies": {
|
|
40
|
-
"@clack/prompts": "^0.
|
|
41
|
+
"@clack/prompts": "^0.11.0",
|
|
41
42
|
"@vercel/detect-agent": "^1.2.3",
|
|
42
|
-
"picocolors": "^1.
|
|
43
|
-
"simple-git": "^3.
|
|
43
|
+
"picocolors": "^1.1.1",
|
|
44
|
+
"simple-git": "^3.36.0",
|
|
44
45
|
"xdg-basedir": "^5.1.0",
|
|
45
46
|
"yaml": "^2.8.3"
|
|
46
47
|
},
|