@rbbtsn0w/adg 0.2.1 → 0.3.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -361,6 +361,12 @@ Debugging tips:
361
361
  - GitHub clone/sparse logic is injectable (`gitRunner`) and covered offline by
362
362
  the test suite; live network clones are exercised by `import owner/repo`.
363
363
 
364
+ ## Contributing
365
+
366
+ All feature/fix pull requests target the **`beta`** integration branch; `main`
367
+ is reserved for stable releases. See [CONTRIBUTING.md](CONTRIBUTING.md) and
368
+ [docs/branching-and-release.md](docs/branching-and-release.md).
369
+
364
370
  ## License
365
371
 
366
372
  MIT
package/dist/bin/adg.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from "node:util";
3
+ import pc from "picocolors";
3
4
  import { spawnSync } from "node:child_process";
4
5
  import { readFileSync, realpathSync } from "node:fs";
6
+ import { homedir } from "node:os";
5
7
  import { dirname, join, resolve } from "node:path";
6
8
  import { fileURLToPath } from "node:url";
7
9
  import { checkForUpdate, formatUpdateNotice } from "../src/update-check.js";
@@ -21,9 +23,24 @@ import { selectTargetsInteractive } from "../src/commands/select-agents.js";
21
23
  import { selectPluginsInteractive } from "../src/commands/select-plugins.js";
22
24
  import { selectScopeInteractive } from "../src/commands/select-scope.js";
23
25
  import { confirmFullInstall, selectComponentsInteractive } from "../src/commands/select-components.js";
24
- import { globalPluginsDir, projectPluginsDir } from "../src/paths.js";
26
+ import { globalPluginsDir, installedPluginDir, projectPluginsDir } from "../src/paths.js";
25
27
  import { COMPONENT_TYPES } from "../src/types.js";
26
- import { getAgent } from "../src/agents/index.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
+ };
27
44
  const FLAGS = {
28
45
  // Short flags are first-letter aliases. Where several long flags share a
29
46
  // first letter, the highest-frequency one wins the short and the rest stay
@@ -32,7 +49,7 @@ const FLAGS = {
32
49
  dir: { type: "string", short: "d", hint: "<dir>", help: "install into an explicit directory" },
33
50
  global: { type: "boolean", short: "g", help: "use ~/.agents/plugins (across all projects)" },
34
51
  project: { type: "boolean", help: "use <repo>/.agents/plugins (default)" },
35
- target: { type: "string", short: "t", hint: "claude|codex|all", help: "runtime(s) to adapt for" },
52
+ target: { type: "string", short: "t", hint: "claude|codex|antigravity|all", help: "runtime(s) to adapt for" },
36
53
  all: { type: "boolean", short: "a", help: "select all available plugins" },
37
54
  plugin: { type: "string", short: "p", multiple: true, hint: "<name>", help: "select a specific plugin (repeatable)" },
38
55
  "no-deps": { type: "boolean", short: "n", help: "don't install dependencies" },
@@ -120,7 +137,7 @@ const PLUGIN_COMMANDS = {
120
137
  },
121
138
  link: {
122
139
  summary: "link installed plugins into a runtime",
123
- synopsis: "adg plugins link --target claude|codex",
140
+ synopsis: "adg plugins link --target claude|codex|antigravity",
124
141
  flags: ["target", ...SCOPE],
125
142
  },
126
143
  migrate: {
@@ -208,7 +225,7 @@ function wantsHelp(args) {
208
225
  return args.includes("-h") || args.includes("--help");
209
226
  }
210
227
  function fail(msg) {
211
- console.error(`error: ${msg}\n`);
228
+ console.error(`${ui.err("error:")} ${msg}\n`);
212
229
  console.error(TOP_USAGE);
213
230
  process.exit(1);
214
231
  }
@@ -231,17 +248,25 @@ function reportAgents(agents, verb) {
231
248
  for (const r of agents ?? []) {
232
249
  const name = getAgent(r.agent)?.displayName ?? r.agent;
233
250
  if (r.affected.length > 0)
234
- console.log(`${verb} in ${name}: ${r.affected.join(", ")}`);
251
+ console.log(`${ui.ok(verb)} in ${ui.name(name)}: ${r.affected.join(", ")}`);
235
252
  else if (r.skipped)
236
- console.log(`note: \`${r.agent}\` CLI not found — run \`adg plugins link --target ${r.agent}\` after installing it.`);
253
+ console.log(ui.warn(`note: \`${r.agent}\` CLI not found — run \`adg plugins link --target ${r.agent}\` after installing it.`));
237
254
  }
238
255
  }
256
+ /** Friendly `--target` aliases mapped onto canonical adapter target ids. */
257
+ const TARGET_ALIASES = {
258
+ anthropic: "claude",
259
+ openai: "codex",
260
+ agy: "antigravity",
261
+ gemini: "antigravity",
262
+ };
239
263
  function resolveTargets(target) {
240
264
  if (!target || target === "all")
241
265
  return [...ADAPTER_TARGETS];
242
- if (target === "claude" || target === "codex")
243
- return [target];
244
- fail(`invalid --target "${String(target)}" (expected claude|codex|all)`);
266
+ const t = TARGET_ALIASES[target] ?? target;
267
+ if (ADAPTER_TARGETS.includes(t))
268
+ return [t];
269
+ fail(`invalid --target "${target}" (expected ${[...ADAPTER_TARGETS, "all"].join("|")})`);
245
270
  }
246
271
  /** Parse a `--only skills,commands` list into validated component types. */
247
272
  function resolveComponents(only) {
@@ -264,7 +289,7 @@ function parseVerb(name, flags, rest) {
264
289
  return { values: values, positionals };
265
290
  }
266
291
  catch (err) {
267
- console.error(`error: ${err instanceof Error ? err.message : String(err)}\n`);
292
+ console.error(`${ui.err("error:")} ${err instanceof Error ? err.message : String(err)}\n`);
268
293
  console.error(renderVerbHelp(name));
269
294
  process.exit(1);
270
295
  }
@@ -290,12 +315,21 @@ function formatColumns(items, opts = {}) {
290
315
  }
291
316
  return lines.join("\n");
292
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
+ }
293
327
  /** Print a plugin's components, each expanded to its member names (verbose view). */
294
328
  function printContents(contents, headerIndent) {
295
329
  const entries = Object.entries(contents ?? {}).filter(([, names]) => names.length > 0);
296
330
  for (const [type, names] of entries) {
297
331
  const maxColWidth = Math.max(1, ...names.map((n) => n.length));
298
- console.log(`${" ".repeat(headerIndent)}${type} (${names.length}):`);
332
+ console.log(`${" ".repeat(headerIndent)}${ui.name(type)} ${ui.meta(`(${names.length}):`)}`);
299
333
  console.log(formatColumns(names, { indent: headerIndent + 2, maxColWidth }));
300
334
  }
301
335
  }
@@ -308,7 +342,7 @@ async function runPlugins(rawVerb, rest) {
308
342
  const verb = PLUGIN_ALIASES[rawVerb] ?? rawVerb;
309
343
  const cmd = PLUGIN_COMMANDS[verb];
310
344
  if (!cmd) {
311
- console.error(`error: unknown plugins subcommand: ${rawVerb}\n`);
345
+ console.error(`${ui.err("error:")} unknown plugins subcommand: ${rawVerb}\n`);
312
346
  console.error(renderPluginsHelp());
313
347
  process.exit(1);
314
348
  }
@@ -330,16 +364,16 @@ async function runPlugins(rawVerb, rest) {
330
364
  fail(`invalid --type "${values.type}" (expected plugin|marketplace|all)`);
331
365
  }
332
366
  const res = initScaffold({ name, dir, type, description: values.description, author: values.author, skill: values.skill?.[0] });
333
- console.log(`created ${type} at ${res.pluginDir}`);
367
+ console.log(`${ui.ok(`created ${type}`)} at ${ui.name(res.pluginDir)}`);
334
368
  for (const f of res.created)
335
- console.log(` + ${f}`);
369
+ console.log(ui.meta(` + ${f}`));
336
370
  return;
337
371
  }
338
372
  case "adapt": {
339
373
  const { values, positionals } = parseVerb(verb, cmd.flags, rest);
340
374
  const pluginDir = resolve(positionals[0] ?? process.cwd());
341
375
  for (const r of adaptPlugin(pluginDir, resolveTargets(values.target))) {
342
- console.log(`adapted ${r.target} -> ${r.file}`);
376
+ console.log(`${ui.ok("adapted")} ${ui.name(r.target)} ${ui.meta(`-> ${r.file}`)}`);
343
377
  }
344
378
  return;
345
379
  }
@@ -348,12 +382,12 @@ async function runPlugins(rawVerb, rest) {
348
382
  const pluginDir = resolve(positionals[0] ?? process.cwd());
349
383
  const res = validatePlugin(pluginDir);
350
384
  if (res.ok) {
351
- console.log(`ok: ${pluginDir} is a valid ADG plugin`);
385
+ console.log(`${ui.ok("ok:")} ${ui.name(pluginDir)} is a valid ADG plugin`);
352
386
  }
353
387
  else {
354
- console.error(`invalid: ${pluginDir}`);
388
+ console.error(`${ui.err("invalid:")} ${ui.name(pluginDir)}`);
355
389
  for (const i of res.issues)
356
- console.error(` - ${i}`);
390
+ console.error(ui.warn(` - ${i}`));
357
391
  process.exit(1);
358
392
  }
359
393
  return;
@@ -404,14 +438,14 @@ async function runPlugins(rawVerb, rest) {
404
438
  scope: global ? "user" : "project",
405
439
  });
406
440
  for (const name of converted)
407
- console.log(`converted native manifest -> .agents/.plugin.json: ${name}`);
441
+ console.log(ui.meta(`converted native manifest -> .agents/.plugin.json: ${name}`));
408
442
  if (order.length > 1)
409
- console.log(`install order: ${order.join(" -> ")}`);
443
+ console.log(ui.meta(`install order: ${order.join(" -> ")}`));
410
444
  for (const res of installed) {
411
- console.log(`added ${res.name} -> ${res.installedTo}`);
412
- console.log(` folderHash: ${res.folderHash}`);
445
+ console.log(`${ui.ok("added")} ${ui.name(res.name)} ${ui.meta(`-> ${res.installedTo}`)}`);
446
+ console.log(ui.meta(` folderHash: ${res.folderHash}`));
413
447
  for (const f of res.adapted)
414
- console.log(` adapted: ${f}`);
448
+ console.log(ui.meta(` adapted: ${f}`));
415
449
  }
416
450
  reportAgents(agents, "enabled");
417
451
  return;
@@ -430,22 +464,23 @@ async function runPlugins(rawVerb, rest) {
430
464
  pluginsDir: resolveScopeDir(values),
431
465
  description: values.description,
432
466
  });
433
- console.log(`imported skills into ${res.name} -> ${res.installedTo}`);
467
+ console.log(`${ui.ok("imported skills into")} ${ui.name(res.name)} ${ui.meta(`-> ${res.installedTo}`)}`);
434
468
  return;
435
469
  }
436
470
  case "link": {
437
471
  const { values } = parseVerb(verb, cmd.flags, rest);
438
- const target = values.target;
439
- if (target !== "claude" && target !== "codex")
440
- fail("plugins link requires --target claude|codex");
472
+ if (values.target === undefined || values.target === "all") {
473
+ fail(`plugins link requires a single --target (${ADAPTER_TARGETS.join("|")})`);
474
+ }
475
+ const target = resolveTargets(values.target)[0]; // validates + maps aliases (agy → antigravity); always non-empty
441
476
  const res = linkPlugins({ pluginsDir: resolveScopeDir(values), target, global: Boolean(values.global) });
442
477
  for (const a of res.actions) {
443
- console.log(`linked ${a.name} [${res.target}]${a.linkedTo ? ` -> ${a.linkedTo}` : ""}`);
478
+ console.log(`${ui.ok("linked")} ${ui.name(a.name)} ${ui.meta(`[${res.target}]`)}${a.linkedTo ? ui.meta(` -> ${a.linkedTo}`) : ""}`);
444
479
  for (const f of a.adapted)
445
- console.log(` adapted: ${f}`);
480
+ console.log(ui.meta(` adapted: ${f}`));
446
481
  }
447
482
  if (res.cliSkipped) {
448
- console.log(`note: \`${target}\` CLI not found — manifests were generated, but nothing was enabled in ${target}.`);
483
+ console.log(ui.warn(`note: \`${target}\` CLI not found — manifests were generated, but nothing was enabled in ${target}.`));
449
484
  }
450
485
  return;
451
486
  }
@@ -456,9 +491,9 @@ async function runPlugins(rawVerb, rest) {
456
491
  scope: scopeOf(values),
457
492
  });
458
493
  for (const r of results)
459
- console.log(`${r.changed ? "updated" : "unchanged"} ${r.name}@${r.version}`);
494
+ console.log(`${r.changed ? ui.ok("updated") : ui.meta("unchanged")} ${ui.name(`${r.name}@${r.version}`)}`);
460
495
  for (const m of missing)
461
- console.error(` ! missing directory for locked plugin: ${m}`);
496
+ console.error(ui.warn(` ! missing directory for locked plugin: ${m}`));
462
497
  reportAgents(agents, "re-synced");
463
498
  return;
464
499
  }
@@ -475,17 +510,17 @@ async function runPlugins(rawVerb, rest) {
475
510
  scope: scopeOf(values),
476
511
  });
477
512
  if (res.removedDir)
478
- console.log(`removed ${res.name} -> ${res.removedDir}`);
513
+ console.log(`${ui.ok("removed")} ${ui.name(res.name)} ${ui.meta(`-> ${res.removedDir}`)}`);
479
514
  else
480
- console.log(`removed ${res.name} (no directory on disk)`);
515
+ console.log(`${ui.ok("removed")} ${ui.name(res.name)} ${ui.meta("(no directory on disk)")}`);
481
516
  for (const link of res.unlinked)
482
- console.log(` unlinked: ${link}`);
517
+ console.log(ui.meta(` unlinked: ${link}`));
483
518
  for (const r of res.agents ?? []) {
484
519
  if (r.affected.length > 0)
485
- console.log(` disabled in ${getAgent(r.agent)?.displayName ?? r.agent}`);
520
+ console.log(ui.meta(` disabled in ${getAgent(r.agent)?.displayName ?? r.agent}`));
486
521
  }
487
522
  if (!res.removedFromLock && !res.removedDir) {
488
- console.log(` ${res.name} was not recorded in the lock`);
523
+ console.log(ui.warn(` ${res.name} was not recorded in the lock`));
489
524
  }
490
525
  return;
491
526
  }
@@ -494,21 +529,42 @@ async function runPlugins(rawVerb, rest) {
494
529
  const pluginsDir = resolveScopeDir(values);
495
530
  const plugins = listPlugins(pluginsDir);
496
531
  if (plugins.length === 0) {
497
- console.log(`no plugins recorded in ${pluginsDir}`);
532
+ console.log(ui.meta(`no plugins recorded in ${pluginsDir}`));
498
533
  return;
499
534
  }
500
- for (const p of plugins) {
501
- const partial = p.selection ? " (partial)" : "";
502
- console.log(`${p.name}@${p.version} [${p.origin.type}] ${p.folderHash.slice(0, 19)}${partial}`);
503
- const entries = Object.entries(p.contents ?? {}).filter(([, names]) => names.length > 0);
504
- if (entries.length === 0)
505
- continue;
506
- if (values.verbose) {
507
- printContents(p.contents, 2);
508
- }
509
- else {
510
- console.log(` ${entries.map(([type, names]) => `${type}: ${names.length}`).join(" ")}`);
511
- }
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);
512
568
  }
513
569
  return;
514
570
  }
@@ -516,11 +572,11 @@ async function runPlugins(rawVerb, rest) {
516
572
  const { values } = parseVerb(verb, cmd.flags, rest);
517
573
  const res = migrateLayout(resolveScopeDir(values));
518
574
  for (const m of res.moved)
519
- console.log(`moved ${m.name}: ${m.from} -> ${m.to}`);
575
+ console.log(`${ui.ok("moved")} ${ui.name(m.name)}: ${ui.meta(`${m.from} -> ${m.to}`)}`);
520
576
  for (const m of res.missing)
521
- console.error(` ! missing directory for locked plugin: ${m}`);
577
+ console.error(ui.warn(` ! missing directory for locked plugin: ${m}`));
522
578
  if (res.moved.length === 0)
523
- console.log(`nothing to migrate (${res.unchanged.length} already in place)`);
579
+ console.log(ui.meta(`nothing to migrate (${res.unchanged.length} already in place)`));
524
580
  return;
525
581
  }
526
582
  case "marketplace":
@@ -537,7 +593,7 @@ Commands:
537
593
  adg plugins marketplace list [--verbose] [--global | --project | --dir <dir>]
538
594
  Group installed plugins by source. --verbose expands each plugin to its
539
595
  components (skills, agents, commands, …).
540
- adg plugins marketplace upgrade [<source>] [--all] [--target claude|codex|all] [--global | --project | --dir <dir>]
596
+ adg plugins marketplace upgrade [<source>] [--all] [--target claude|codex|antigravity|all] [--global | --project | --dir <dir>]
541
597
  Re-fetch a source and update its installed plugins (--all also installs
542
598
  anything new it now offers). No <source> upgrades every remote source.
543
599
  adg plugins marketplace remove <source> [--force] [--global | --project | --dir <dir>]
@@ -563,7 +619,7 @@ async function runMarketplace(args) {
563
619
  const dir = resolveScopeDir(values);
564
620
  const groups = marketplaceList({ pluginsDir: dir });
565
621
  if (groups.length === 0) {
566
- console.log("No plugins installed.");
622
+ console.log(ui.meta("No plugins installed."));
567
623
  return;
568
624
  }
569
625
  // Verbose: drill each plugin down to its components (reuses `plugins list -v`).
@@ -571,12 +627,12 @@ async function runMarketplace(args) {
571
627
  for (const g of groups) {
572
628
  const ref = g.ref ? `@${g.ref}` : "";
573
629
  const n = g.installed.length;
574
- const tag = g.remote ? "" : " (local — re-run add to update)";
575
- console.log(`${g.source}${ref} (${n} plugin${n !== 1 ? "s" : ""})${tag}`);
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}`);
576
632
  if (byName) {
577
633
  for (const name of g.installed) {
578
634
  const p = byName.get(name);
579
- console.log(` ${name}${p?.selection ? " (partial)" : ""}`);
635
+ console.log(` ${ui.name(name)}${p?.selection ? ui.meta(" (partial)") : ""}`);
580
636
  printContents(p?.contents, 4);
581
637
  }
582
638
  }
@@ -599,11 +655,11 @@ async function runMarketplace(args) {
599
655
  });
600
656
  for (const r of results) {
601
657
  const conv = r.converted.length ? ` (${r.converted.length} converted from native)` : "";
602
- console.log(`upgraded ${r.source}: ${r.updated.length} plugin(s)${conv}`);
658
+ console.log(`${ui.ok("upgraded")} ${ui.name(r.source)}: ${r.updated.length} plugin(s)${ui.meta(conv)}`);
603
659
  for (const p of r.updated)
604
- console.log(` ${p.name} -> ${p.installedTo}`);
660
+ console.log(` ${ui.name(p.name)} ${ui.meta(`-> ${p.installedTo}`)}`);
605
661
  if (r.available.length > 0) {
606
- console.log(` ${r.available.length} more available (use --all): ${r.available.join(", ")}`);
662
+ console.log(ui.meta(` ${r.available.length} more available (use --all): ${r.available.join(", ")}`));
607
663
  }
608
664
  }
609
665
  return;
@@ -622,11 +678,11 @@ async function runMarketplace(args) {
622
678
  force: values.force,
623
679
  deactivate: true,
624
680
  });
625
- console.log(`removed ${res.removed.length} plugin(s) from ${res.source}: ${res.removed.join(", ")}`);
681
+ console.log(`${ui.ok("removed")} ${res.removed.length} plugin(s) from ${ui.name(res.source)}: ${res.removed.join(", ")}`);
626
682
  return;
627
683
  }
628
684
  default: {
629
- console.error(`error: unknown marketplace subcommand: ${sub}\n`);
685
+ console.error(`${ui.err("error:")} unknown marketplace subcommand: ${sub}\n`);
630
686
  console.error(MARKETPLACE_USAGE);
631
687
  process.exit(1);
632
688
  }
@@ -729,7 +785,7 @@ function isInvokedDirectly() {
729
785
  }
730
786
  if (isInvokedDirectly()) {
731
787
  main(process.argv.slice(2)).catch((err) => {
732
- console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
788
+ console.error(`${ui.err("error:")} ${err instanceof Error ? err.message : String(err)}`);
733
789
  process.exit(1);
734
790
  });
735
791
  }
@@ -0,0 +1,18 @@
1
+ import { join } from "node:path";
2
+ /** Projection subdirectory that holds the self-contained agy plugin root. */
3
+ export const ANTIGRAVITY_PROJECTION_DIR = ".antigravity-plugin";
4
+ /**
5
+ * Generate an Antigravity (`agy`) plugin.json from an ADG manifest.
6
+ *
7
+ * Antigravity's manifest is minimal: it reads only `name` from a `plugin.json`
8
+ * and discovers components by convention (sibling `skills/`, `agents/`,
9
+ * `commands/`, `hooks/` directories plus a `mcp_config.json`) — all resolved
10
+ * relative to the directory handed to `agy plugin install`, with no manifest
11
+ * path indirection. We therefore project a self-contained agy plugin root under
12
+ * `.antigravity-plugin/`: this pure transform emits its `plugin.json`, while the
13
+ * agent materializes the rest (mcp_config.json + symlinked component dirs), so a
14
+ * partial-install `selection` is not expressible for this target.
15
+ */
16
+ export function toAntigravityManifest(_pluginDir, manifest, _selection) {
17
+ return { defaultPath: join(ANTIGRAVITY_PROJECTION_DIR, "plugin.json"), manifest: { name: manifest.name } };
18
+ }
@@ -1,10 +1,28 @@
1
1
  import { toAnthropicManifest } from "./anthropic.js";
2
2
  import { toCodexManifest } from "./openai.js";
3
+ import { toAntigravityManifest } from "./antigravity.js";
3
4
  export const ADAPTERS = {
4
5
  claude: toAnthropicManifest,
5
6
  anthropic: toAnthropicManifest,
6
7
  codex: toCodexManifest,
7
8
  openai: toCodexManifest,
9
+ antigravity: toAntigravityManifest,
10
+ agy: toAntigravityManifest,
11
+ gemini: toAntigravityManifest,
8
12
  };
9
- export const ADAPTER_TARGETS = ["claude", "codex"];
10
- export { toAnthropicManifest, toCodexManifest };
13
+ export const ADAPTER_TARGETS = ["claude", "codex", "antigravity"];
14
+ /**
15
+ * Component categories each adapter target can actually express, mirroring what
16
+ * the adapters emit: the Claude manifest carries skills/agents/commands/hooks/mcp
17
+ * (`toAnthropicManifest`), while Codex only consumes skills (`toCodexManifest`).
18
+ * Antigravity (`agy`) discovers the same superset as Claude via convention
19
+ * (skills/agents/commands/hooks dirs + mcp_config.json). `apps` is emitted by
20
+ * none, so it maps to no target. Used to derive which agents a plugin is
21
+ * adaptable to from its exposed component types.
22
+ */
23
+ export const ADAPTER_COMPONENTS = {
24
+ claude: ["skills", "agents", "commands", "hooks", "mcp"],
25
+ codex: ["skills"],
26
+ antigravity: ["skills", "agents", "commands", "hooks", "mcp"],
27
+ };
28
+ export { toAnthropicManifest, toCodexManifest, toAntigravityManifest };
@@ -0,0 +1,185 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { cpSync, existsSync, rmSync, statSync, symlinkSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join, relative } from "node:path";
5
+ import { ensureDir, writeJson } from "../fsutil.js";
6
+ import { readManifest } from "../manifest.js";
7
+ import { resolveSkillEntries } from "../skills.js";
8
+ import { isExposed } from "../components.js";
9
+ import { installedPluginDir, lockPath } from "../paths.js";
10
+ import { readLock } from "../lock.js";
11
+ import { ANTIGRAVITY_PROJECTION_DIR } from "../adapters/antigravity.js";
12
+ /**
13
+ * Antigravity (`agy`) agent.
14
+ *
15
+ * Antigravity discovers a plugin by convention relative to the directory handed
16
+ * to `agy plugin install` — `plugin.json` plus sibling `skills/`, `agents/`,
17
+ * `commands/`, `hooks/` dirs and a `mcp_config.json`, with no manifest path
18
+ * indirection. We therefore project a self-contained agy plugin root under
19
+ * `<store>/.antigravity-plugin/`: generated `plugin.json` + `mcp_config.json`
20
+ * (the ADG `mcp/.mcp.json` shape is exactly agy's, so it passes through) and
21
+ * *symlinks* to the real component dirs one level up, so nothing is duplicated
22
+ * on disk. agy follows these symlinks; where the platform forbids them (e.g.
23
+ * Windows without privilege) we fall back to a copy. We then drive
24
+ * `agy plugin install/uninstall` (which owns `~/.gemini/antigravity-cli`).
25
+ */
26
+ const ID = "antigravity";
27
+ /**
28
+ * Single-directory component fields: agy reads each as a sibling dir named by
29
+ * convention. `skills` is handled separately because it can be a path-list and
30
+ * supports per-skill subsetting.
31
+ */
32
+ const DIR_FIELDS = ["agents", "commands", "hooks"];
33
+ function geminiHome(env) {
34
+ return env.GEMINI_HOME?.trim() || join(homedir(), ".gemini");
35
+ }
36
+ /**
37
+ * agy's config/store home: `<GEMINI_HOME>/antigravity-cli` (defaulting to
38
+ * `~/.gemini/antigravity-cli`). Exported so the resolver itself is testable
39
+ * without depending on the host filesystem state.
40
+ */
41
+ export function antigravityHome(env = process.env) {
42
+ return join(geminiHome(env), "antigravity-cli");
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
+ }
57
+ /** Resolve a plugin's on-disk store directory and selection from the lock's provenance. */
58
+ function pluginStore(pluginsDir, name) {
59
+ const entry = readLock(lockPath(pluginsDir)).plugins[name];
60
+ if (!entry)
61
+ return undefined;
62
+ const dir = installedPluginDir(pluginsDir, name, entry.origin);
63
+ return existsSync(dir) ? { dir, selection: entry.selection } : undefined;
64
+ }
65
+ /** First on-disk top segment of a declared component path (e.g. "./agents/" -> "agents"). */
66
+ function componentSegment(value) {
67
+ const first = Array.isArray(value) ? value[0] : value;
68
+ if (typeof first !== "string")
69
+ return undefined;
70
+ const seg = first.replace(/^\.?[/\\]/, "").split(/[/\\]/)[0];
71
+ return seg || undefined;
72
+ }
73
+ /**
74
+ * Symlink `linkPath` at `absTarget` (target stored relative so the projection
75
+ * survives the whole plugin dir being moved), copying instead where symlinks are
76
+ * unavailable (e.g. Windows without privilege). Idempotent.
77
+ */
78
+ function linkOrCopy(linkPath, absTarget) {
79
+ rmSync(linkPath, { recursive: true, force: true });
80
+ ensureDir(dirname(linkPath));
81
+ try {
82
+ symlinkSync(relative(dirname(linkPath), absTarget), linkPath, "dir");
83
+ }
84
+ catch {
85
+ cpSync(absTarget, linkPath, { recursive: true });
86
+ }
87
+ }
88
+ /**
89
+ * Build the agy-native projection under `<dir>/.antigravity-plugin/`: a
90
+ * `plugin.json` (name only), a `mcp_config.json` copied verbatim from the
91
+ * manifest's mcp file when present, and relative symlinks (copy fallback) to the
92
+ * declared component dirs named for agy's convention.
93
+ *
94
+ * An optional `selection` (the plugin's partial install) narrows what is
95
+ * projected: component categories outside it are dropped, and `skills` is pinned
96
+ * to the selected subset. `skills` is also projected per-skill into a real
97
+ * `skills/` dir, so a path-list spanning multiple roots is fully honored rather
98
+ * than collapsing to its first root. Idempotent; safe to re-run.
99
+ */
100
+ export function writeAntigravityProjection(dir, selection) {
101
+ const manifest = readManifest(dir);
102
+ const stage = join(dir, ANTIGRAVITY_PROJECTION_DIR);
103
+ ensureDir(stage);
104
+ writeJson(join(stage, "plugin.json"), { name: manifest.name });
105
+ const mcpConfig = join(stage, "mcp_config.json");
106
+ rmSync(mcpConfig, { force: true });
107
+ if (manifest.mcp && isExposed(selection, "mcp")) {
108
+ const mcpFile = join(dir, manifest.mcp);
109
+ // The ADG mcp file shape is exactly agy's `mcp_config.json`, so copy it
110
+ // verbatim — preserving formatting and avoiding a parse/re-serialize round-trip.
111
+ if (existsSync(mcpFile))
112
+ cpSync(mcpFile, mcpConfig);
113
+ }
114
+ const skillsDir = join(stage, "skills");
115
+ rmSync(skillsDir, { recursive: true, force: true });
116
+ if (manifest.skills !== undefined && isExposed(selection, "skills")) {
117
+ const pick = selection?.skills;
118
+ for (const e of resolveSkillEntries(dir, manifest)) {
119
+ if (pick && !pick.includes(e.name))
120
+ continue;
121
+ if (!e.skillMd)
122
+ continue;
123
+ const srcSkillDir = dirname(e.skillMd);
124
+ if (existsSync(srcSkillDir) && statSync(srcSkillDir).isDirectory()) {
125
+ linkOrCopy(join(skillsDir, e.name), srcSkillDir);
126
+ }
127
+ }
128
+ }
129
+ for (const field of DIR_FIELDS) {
130
+ const link = join(stage, field);
131
+ rmSync(link, { recursive: true, force: true });
132
+ if (!isExposed(selection, field))
133
+ continue;
134
+ const seg = componentSegment(manifest[field]);
135
+ if (!seg)
136
+ continue;
137
+ const srcDir = join(dir, seg);
138
+ // agy reads the component by its convention name (`field`); point that at
139
+ // the real source dir so a non-conventional source name still resolves.
140
+ if (existsSync(srcDir))
141
+ linkOrCopy(link, srcDir);
142
+ }
143
+ }
144
+ export const antigravityAgent = {
145
+ id: ID,
146
+ displayName: "Antigravity",
147
+ adaptTarget: "antigravity",
148
+ detect: (env = process.env) => existsSync(antigravityHome(env)),
149
+ available,
150
+ activate(ctx) {
151
+ if (!available())
152
+ return { agent: ID, affected: [], skipped: true };
153
+ const affected = [];
154
+ for (const p of ctx.plugins) {
155
+ // Isolate each plugin: a malformed manifest or a filesystem error must not
156
+ // abort activation of the remaining (valid) plugins.
157
+ try {
158
+ const store = pluginStore(ctx.pluginsDir, p);
159
+ if (!store)
160
+ continue;
161
+ writeAntigravityProjection(store.dir, store.selection);
162
+ if (run(["plugin", "install", join(store.dir, ANTIGRAVITY_PROJECTION_DIR)]).ok)
163
+ affected.push(p);
164
+ }
165
+ catch (err) {
166
+ console.error(`failed to enable "${p}" in Antigravity:`, err);
167
+ }
168
+ }
169
+ return { agent: ID, affected, skipped: false };
170
+ },
171
+ deactivate(ctx) {
172
+ if (!available())
173
+ return { agent: ID, affected: [], skipped: true };
174
+ const affected = [];
175
+ for (const p of ctx.plugins) {
176
+ if (run(["plugin", "uninstall", p]).ok)
177
+ affected.push(p);
178
+ }
179
+ return { agent: ID, affected, skipped: false };
180
+ },
181
+ // `agy plugin install` re-imports the source dir, so re-running it is the refresh.
182
+ refresh(ctx) {
183
+ return antigravityAgent.activate(ctx);
184
+ },
185
+ };
@@ -1,10 +1,12 @@
1
1
  import { registerAgent } from "./registry.js";
2
2
  import { claudeAgent } from "./claude.js";
3
3
  import { codexAgent } from "./codex.js";
4
+ import { antigravityAgent } from "./antigravity.js";
4
5
  // Built-in agents register on import. Third-party agents can `registerAgent()`
5
6
  // their own implementation (stage 2: discover from config without core edits).
6
7
  registerAgent(claudeAgent);
7
8
  registerAgent(codexAgent);
9
+ registerAgent(antigravityAgent);
8
10
  export * from "./types.js";
9
- export { registerAgent, getAgent, allAgents, detectedAgents, resolveAgents } from "./registry.js";
11
+ export { registerAgent, getAgent, allAgents, detectedAgents, resolveAgents, agentsForComponents } from "./registry.js";
10
12
  export { writeClaudeCatalog } from "./claude.js";
@@ -1,3 +1,4 @@
1
+ import { ADAPTER_COMPONENTS } from "../adapters/index.js";
1
2
  /**
2
3
  * The agent registry — the "factory": construction/lookup only. Orchestration
3
4
  * (looping, dependency order, store writes) stays in the command layer.
@@ -16,6 +17,17 @@ export function allAgents() {
16
17
  export function detectedAgents(env) {
17
18
  return allAgents().filter((a) => a.detect(env));
18
19
  }
20
+ /**
21
+ * Registered agents whose adapter can express at least one of the given exposed
22
+ * component types — i.e. the agents a plugin is adaptable to. An empty `types`
23
+ * (no manifest / nothing exposed) can't be proven incompatible, so all agents
24
+ * are returned.
25
+ */
26
+ export function agentsForComponents(types) {
27
+ if (types.length === 0)
28
+ return allAgents();
29
+ return allAgents().filter((a) => (ADAPTER_COMPONENTS[a.adaptTarget] ?? []).some((c) => types.includes(c)));
30
+ }
19
31
  /** Agents matching the given ids, or every registered agent when none are given. */
20
32
  export function resolveAgents(targets) {
21
33
  if (!targets)
@@ -6,7 +6,7 @@ import { fromNativeManifest } from "../adapters/reverse.js";
6
6
  import { adaptPlugin } from "./adapt.js";
7
7
  import { copyPluginDir, writeJson } from "../fsutil.js";
8
8
  import { folderHash } from "../hash.js";
9
- import { packageFilter } from "../package.js";
9
+ import { packageFilter, PROJECTION_DIRS } from "../package.js";
10
10
  import { lockPath, marketplacePath, marketplaceSourcePath, pluginDir } from "../paths.js";
11
11
  import { readLock, upsertEntry, writeLock } from "../lock.js";
12
12
  import { ADG_MANIFEST_PATH, readManifest } from "../manifest.js";
@@ -17,7 +17,8 @@ import { sameSource, COMPONENT_TYPES } from "../types.js";
17
17
  import { pluginContents, presentComponents } from "../components.js";
18
18
  import { skillDescriptionLoader } from "../skills.js";
19
19
  import { resolveAgents } from "../agents/index.js";
20
- const HASH_IGNORE = [".claude-plugin", ".codex-plugin"];
20
+ // Generated runtime projections never count toward a plugin's content hash.
21
+ const HASH_IGNORE = PROJECTION_DIRS;
21
22
  function toPosix(p) {
22
23
  return p.split("\\").join("/");
23
24
  }
@@ -1,10 +1,11 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { folderHash } from "../hash.js";
3
- import { packageFilter } from "../package.js";
3
+ import { packageFilter, PROJECTION_DIRS } from "../package.js";
4
4
  import { lockPath, installedPluginDir } from "../paths.js";
5
5
  import { readLock, writeLock } from "../lock.js";
6
6
  import { readManifest } from "../manifest.js";
7
7
  import { adaptPlugin } from "./adapt.js";
8
+ import { ADAPTER_TARGETS } from "../adapters/index.js";
8
9
  import { resolveAgents } from "../agents/index.js";
9
10
  /**
10
11
  * Re-scan installed plugins in a plugins directory, refreshing each lock
@@ -29,14 +30,16 @@ export function updateLock(pluginsDir, now = new Date().toISOString(), opts = {}
29
30
  continue;
30
31
  }
31
32
  const manifest = readManifest(dir);
32
- const hash = folderHash(dir, [".claude-plugin", ".codex-plugin"], packageFilter(manifest, { includeProjections: false }));
33
+ const hash = folderHash(dir, PROJECTION_DIRS, packageFilter(manifest, { includeProjections: false }));
33
34
  const changed = hash !== entry.folderHash || manifest.version !== entry.version;
34
35
  if (changed) {
35
36
  entry.folderHash = hash;
36
37
  entry.version = manifest.version;
37
38
  entry.updatedAt = now;
38
39
  // Regenerate runtime manifests from the updated source, honoring selection.
39
- adaptPlugin(dir, ["claude", "codex"], entry.selection);
40
+ // Covers every registered adapter target (claude/codex/antigravity) so a
41
+ // projection can't go stale after an update.
42
+ adaptPlugin(dir, [...ADAPTER_TARGETS], entry.selection);
40
43
  changedNames.push(name);
41
44
  }
42
45
  results.push({ name, changed, version: manifest.version, folderHash: hash });
@@ -11,7 +11,7 @@
11
11
  /** Top-level metadata files always shipped with a plugin, matched case-insensitively. */
12
12
  const META_RE = /^(README|LICEN[CS]E|CHANGELOG|NOTICE)(\..+)?$/i;
13
13
  /** Generated runtime projections — shipped, but excluded from the content hash. */
14
- export const PROJECTION_DIRS = [".claude-plugin", ".codex-plugin"];
14
+ export const PROJECTION_DIRS = [".claude-plugin", ".codex-plugin", ".antigravity-plugin"];
15
15
  /** Extract the first path segment of a manifest component value (e.g. "./skills/" -> "skills"). */
16
16
  function topSegment(p) {
17
17
  return p.replace(/^\.?[/\\]/, "").split(/[/\\]/)[0] ?? "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rbbtsn0w/adg",
3
- "version": "0.2.1",
3
+ "version": "0.3.0-beta.2",
4
4
  "description": "Agent Directory Group (ADG) toolkit — two domains: plugins and skills.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "scripts": {
25
25
  "adg": "node ./bin/adg.ts",
26
- "build": "rm -rf dist && tsc -p tsconfig.build.json --noCheck",
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
28
  "typecheck": "tsc --noEmit",
29
29
  "prepare": "npm run typecheck",
@@ -52,6 +52,7 @@
52
52
  "@semantic-release/npm": "^13.1.5",
53
53
  "@semantic-release/release-notes-generator": "^14.1.1",
54
54
  "@types/node": "^22.0.0",
55
+ "conventional-changelog-conventionalcommits": "^8.0.0",
55
56
  "semantic-release": "^25.0.5",
56
57
  "typescript": "^5.6.0"
57
58
  }