@rbbtsn0w/adg 0.3.0-beta.3 → 0.3.0-beta.5

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 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, installedPluginDir, projectPluginsDir } from "../src/paths.js";
24
+ import { globalPluginsDir, projectPluginsDir } from "../src/paths.js";
27
25
  import { COMPONENT_TYPES } from "../src/types.js";
28
- import { agentsForComponents, getAgent } from "../src/agents/index.js";
29
- // ---------------------------------------------------------------------------
30
- // Semantic colors, mirroring `adg skills list` so output reads the same across
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
- reportAgents(agents, "enabled");
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
- reportAgents(agents, "re-synced");
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
- if (plugins.length === 0) {
532
- console.log(ui.meta(`no plugins recorded in ${pluginsDir}`));
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 g of groups) {
628
- const ref = g.ref ? `@${g.ref}` : "";
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": {
@@ -31,6 +31,8 @@ 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
+ if (manifest.apps && isExposed(selection, "apps"))
35
+ out.apps = manifest.apps;
34
36
  // Claude's array form is already `./skills/<id>` paths, so a strict array is
35
37
  // passed through verbatim; an explicit id list (selection or strict:false) is
36
38
  // mapped to paths and marks the manifest non-strict.
@@ -16,14 +16,14 @@ export const ADAPTER_TARGETS = ["claude", "codex", "antigravity"];
16
16
  /**
17
17
  * Component categories each adapter target can actually express, mirroring what
18
18
  * the adapters emit: the Claude manifest carries skills/agents/commands/hooks/mcp
19
- * (`toAnthropicManifest`), while Codex only consumes skills (`toCodexManifest`).
20
- * Antigravity (`agy`) discovers the same superset as Claude via convention
21
- * (skills/agents/commands/hooks dirs + mcp_config.json). `apps` is emitted by
22
- * none, so it maps to no target. Used to derive which agents a plugin is
23
- * adaptable to from its exposed component types.
19
+ * plus apps (`toAnthropicManifest`), while Codex only consumes skills
20
+ * (`toCodexManifest`). Antigravity (`agy`) discovers components by convention
21
+ * (skills/agents/commands/hooks dirs + mcp_config.json) and does not surface
22
+ * apps, so apps maps only to the Claude target. Used to derive which agents a
23
+ * plugin is adaptable to from its exposed component types.
24
24
  */
25
25
  export const ADAPTER_COMPONENTS = {
26
- claude: ["skills", "agents", "commands", "hooks", "mcp"],
26
+ claude: ["skills", "agents", "commands", "hooks", "mcp", "apps"],
27
27
  codex: ["skills"],
28
28
  antigravity: ["skills", "agents", "commands", "hooks", "mcp"],
29
29
  };
@@ -36,6 +36,7 @@ export function fromNativeManifest(raw, kind) {
36
36
  copyIfString(n, out, "agents");
37
37
  copyIfString(n, out, "hooks");
38
38
  copyIfString(n, out, "mcp");
39
+ copyIfString(n, out, "apps");
39
40
  if (typeof n.author === "object" && n.author !== null) {
40
41
  manifest.author = n.author;
41
42
  }
@@ -1,4 +1,3 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { cpSync, existsSync, rmSync, statSync, symlinkSync } from "node:fs";
3
2
  import { homedir } from "node:os";
4
3
  import { dirname, join, relative } from "node:path";
@@ -9,6 +8,7 @@ import { isExposed } from "../components.js";
9
8
  import { installedPluginDir, lockPath } from "../paths.js";
10
9
  import { readLock } from "../lock.js";
11
10
  import { ANTIGRAVITY_PROJECTION_DIR } from "../adapters/antigravity.js";
11
+ import { makeCli } from "./base.js";
12
12
  /**
13
13
  * Antigravity (`agy`) agent.
14
14
  *
@@ -41,19 +41,10 @@ function geminiHome(env) {
41
41
  export function antigravityHome(env = process.env) {
42
42
  return join(geminiHome(env), "antigravity-cli");
43
43
  }
44
- function available() {
45
- // `--help` is rejected by `install` (it parses it as a target), so probe the
46
- // plugin command group with its own `help` subcommand instead.
47
- return spawnSync("agy", ["plugin", "help"], { stdio: "ignore" }).status === 0;
48
- }
49
- function run(args) {
50
- const r = spawnSync("agy", args, { encoding: "utf8" });
51
- const ok = r.status === 0;
52
- // Surface the CLI's own diagnostics on failure instead of swallowing them.
53
- if (!ok && r.stderr)
54
- console.error(r.stderr.trim());
55
- return { ok, out: `${r.stdout ?? ""}${r.stderr ?? ""}` };
56
- }
44
+ // `--help` is rejected by `agy plugin install` (it parses it as a target), so
45
+ // probe the plugin command group with its own `help` subcommand instead.
46
+ // `echoStderr` surfaces the CLI's own diagnostics on failure.
47
+ const { available, run } = makeCli("agy", { probeArgs: ["plugin", "help"], echoStderr: true });
57
48
  /** Resolve a plugin's on-disk store directory and selection from the lock's provenance. */
58
49
  function pluginStore(pluginsDir, name) {
59
50
  const entry = readLock(lockPath(pluginsDir)).plugins[name];
@@ -0,0 +1,26 @@
1
+ import { spawnSync } from "node:child_process";
2
+ /**
3
+ * Build the shared `available()` / `run()` pair for an agent that drives an
4
+ * external plugin CLI. Centralizing the `spawnSync` here keeps error handling,
5
+ * output capture, and (future) timeout/env-forwarding changes in one place
6
+ * rather than copied across every agent.
7
+ */
8
+ export function makeCli(bin, opts) {
9
+ return {
10
+ available: () => spawnSync(bin, opts.probeArgs, { stdio: "ignore" }).status === 0,
11
+ run: (args) => {
12
+ const r = spawnSync(bin, args, { encoding: "utf8" });
13
+ // A launch failure (e.g. ENOENT for a missing binary, EACCES) leaves
14
+ // `status` null and `stderr` empty, exposing the cause only via `error`;
15
+ // treat that as a failure and keep its message instead of swallowing it.
16
+ const ok = r.status === 0 && !r.error;
17
+ if (!ok && opts.echoStderr) {
18
+ if (r.error)
19
+ console.error(r.error.message);
20
+ else if (r.stderr)
21
+ console.error(r.stderr.trim());
22
+ }
23
+ return { ok, out: `${r.stdout ?? ""}${r.stderr ?? ""}${r.error ? r.error.message : ""}` };
24
+ },
25
+ };
26
+ }
@@ -1,4 +1,3 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { existsSync } from "node:fs";
3
2
  import { homedir } from "node:os";
4
3
  import { join, relative } from "node:path";
@@ -6,6 +5,7 @@ import { toPosix, writeJson } from "../fsutil.js";
6
5
  import { readManifest } from "../manifest.js";
7
6
  import { installedPluginDir, lockPath } from "../paths.js";
8
7
  import { readLock } from "../lock.js";
8
+ import { makeCli } from "./base.js";
9
9
  /**
10
10
  * Claude Code agent.
11
11
  *
@@ -18,13 +18,7 @@ const MARKETPLACE = "adg";
18
18
  function claudeHome(env) {
19
19
  return env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude");
20
20
  }
21
- function available() {
22
- return spawnSync("claude", ["plugin", "--help"], { stdio: "ignore" }).status === 0;
23
- }
24
- function run(args) {
25
- const r = spawnSync("claude", args, { encoding: "utf8" });
26
- return { ok: r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}` };
27
- }
21
+ const { available, run } = makeCli("claude", { probeArgs: ["plugin", "--help"] });
28
22
  /**
29
23
  * Write a Claude marketplace catalog listing every installed plugin, each
30
24
  * `source` pointing at its on-disk directory (relative to the catalog).
@@ -1,9 +1,9 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { existsSync } from "node:fs";
3
2
  import { homedir } from "node:os";
4
3
  import { join } from "node:path";
5
4
  import { marketplacePath } from "../paths.js";
6
5
  import { readMarketplace } from "../marketplace.js";
6
+ import { makeCli } from "./base.js";
7
7
  /**
8
8
  * Codex agent.
9
9
  *
@@ -14,13 +14,7 @@ import { readMarketplace } from "../marketplace.js";
14
14
  function codexHome(env) {
15
15
  return env.CODEX_HOME?.trim() || join(homedir(), ".codex");
16
16
  }
17
- function available() {
18
- return spawnSync("codex", ["plugin", "--help"], { stdio: "ignore" }).status === 0;
19
- }
20
- function run(args) {
21
- const r = spawnSync("codex", args, { encoding: "utf8" });
22
- return { ok: r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}` };
23
- }
17
+ const { available, run } = makeCli("codex", { probeArgs: ["plugin", "--help"] });
24
18
  /** The marketplace name Codex sees, read from the generated marketplace.json. */
25
19
  function marketplaceName(pluginsDir) {
26
20
  return readMarketplace(marketplacePath(pluginsDir), "").name;
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rbbtsn0w/adg",
3
- "version": "0.3.0-beta.3",
3
+ "version": "0.3.0-beta.5",
4
4
  "description": "Agent Directory Group (ADG) toolkit — two domains: plugins and skills.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,9 +25,11 @@
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",
29
+ "audit:prod": "npm audit --omit=dev",
28
30
  "typecheck": "tsc --noEmit",
29
31
  "prepare": "npm run typecheck",
30
- "prepack": "npm run build"
32
+ "prepack": "npm run typecheck && npm run build"
31
33
  },
32
34
  "files": [
33
35
  "dist",
@@ -35,12 +37,12 @@
35
37
  "vendor",
36
38
  "docs"
37
39
  ],
38
- "comment:dependencies": "Runtime deps below are required only by the vendored skills CLI (vendor/skills). ADG's own plugins code has zero runtime deps.",
40
+ "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
41
  "dependencies": {
40
- "@clack/prompts": "^0.7.0",
42
+ "@clack/prompts": "^0.11.0",
41
43
  "@vercel/detect-agent": "^1.2.3",
42
- "picocolors": "^1.0.0",
43
- "simple-git": "^3.0.0",
44
+ "picocolors": "^1.1.1",
45
+ "simple-git": "^3.36.0",
44
46
  "xdg-basedir": "^5.1.0",
45
47
  "yaml": "^2.8.3"
46
48
  },