quiver-cli 0.8.0 → 1.0.0

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.
Files changed (200) hide show
  1. package/README.md +110 -45
  2. package/bin/quiver-cli.mjs +3 -1
  3. package/dist/cli.js +546 -458
  4. package/package.json +2 -2
  5. package/template/.agents/AGENTS.md +4 -3
  6. package/template/.agents/config.json +7 -0
  7. package/template/.agents/plugins/opencode/rtk.ts +34 -0
  8. package/template/.agents/skills/agent-browser/SKILL.md +1 -0
  9. package/template/.agents/skills/apps/skybridge/SKILL.md +4 -0
  10. package/template/.agents/skills/design/impeccable/SKILL.md +36 -118
  11. package/template/.agents/skills/design/impeccable/reference/adapt.md +1 -0
  12. package/template/.agents/skills/design/impeccable/reference/adapt.native.md +58 -0
  13. package/template/.agents/skills/design/impeccable/reference/android.md +40 -0
  14. package/template/.agents/skills/design/impeccable/reference/animate.md +73 -188
  15. package/template/.agents/skills/design/impeccable/reference/audit.md +12 -9
  16. package/template/.agents/skills/design/impeccable/reference/audit.native.md +139 -0
  17. package/template/.agents/skills/design/impeccable/reference/bolder.md +19 -101
  18. package/template/.agents/skills/design/impeccable/reference/clarify.md +59 -253
  19. package/template/.agents/skills/design/impeccable/reference/colorize.md +51 -222
  20. package/template/.agents/skills/design/impeccable/reference/craft-floor.md +42 -0
  21. package/template/.agents/skills/design/impeccable/reference/craft.md +3 -121
  22. package/template/.agents/skills/design/impeccable/reference/critique.md +44 -23
  23. package/template/.agents/skills/design/impeccable/reference/degraded/asset-producer.md +90 -0
  24. package/template/.agents/skills/design/impeccable/reference/degraded/documenter.md +24 -0
  25. package/template/.agents/skills/design/impeccable/reference/degraded/finish-reviewer.md +37 -0
  26. package/template/.agents/skills/design/impeccable/reference/degraded/manual-edit-applier.md +92 -0
  27. package/template/.agents/skills/design/impeccable/reference/delight.md +47 -279
  28. package/template/.agents/skills/design/impeccable/reference/distill.md +2 -2
  29. package/template/.agents/skills/design/impeccable/reference/doctor.md +53 -0
  30. package/template/.agents/skills/design/impeccable/reference/document.md +60 -73
  31. package/template/.agents/skills/design/impeccable/reference/harden.md +1 -12
  32. package/template/.agents/skills/design/impeccable/reference/hooks.md +20 -5
  33. package/template/.agents/skills/design/impeccable/reference/init.md +72 -119
  34. package/template/.agents/skills/design/impeccable/reference/ios.md +45 -0
  35. package/template/.agents/skills/design/impeccable/reference/layout.md +54 -131
  36. package/template/.agents/skills/design/impeccable/reference/live-setup.md +102 -0
  37. package/template/.agents/skills/design/impeccable/reference/live.md +116 -511
  38. package/template/.agents/skills/design/impeccable/reference/new-work.md +105 -0
  39. package/template/.agents/skills/design/impeccable/reference/{product.md → operate.md} +6 -5
  40. package/template/.agents/skills/design/impeccable/reference/optimize.md +4 -4
  41. package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -4
  42. package/template/.agents/skills/design/impeccable/reference/polish.md +68 -212
  43. package/template/.agents/skills/design/impeccable/reference/quieter.md +3 -3
  44. package/template/.agents/skills/design/impeccable/reference/routing.md +18 -0
  45. package/template/.agents/skills/design/impeccable/reference/shape.md +38 -144
  46. package/template/.agents/skills/design/impeccable/reference/typeset.md +51 -250
  47. package/template/.agents/skills/design/impeccable/reference/visualize.md +47 -0
  48. package/template/.agents/skills/design/impeccable/scripts/command-metadata.json +1 -1
  49. package/template/.agents/skills/design/impeccable/scripts/concept-seed.mjs +558 -0
  50. package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +119 -10
  51. package/template/.agents/skills/design/impeccable/scripts/context.mjs +534 -45
  52. package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +18 -47
  53. package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +96 -10
  54. package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +174 -26
  55. package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +233 -0
  56. package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +3348 -203
  57. package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +102 -7
  58. package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +297 -97
  59. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +187 -16
  60. package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +41 -11
  61. package/template/.agents/skills/design/impeccable/scripts/detector/findings.mjs +7 -1
  62. package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +16 -2
  63. package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +207 -38
  64. package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +3082 -173
  65. package/template/.agents/skills/design/impeccable/scripts/detector/shared/constants.mjs +11 -0
  66. package/template/.agents/skills/design/impeccable/scripts/detector/shared/fonts.mjs +30 -0
  67. package/template/.agents/skills/design/impeccable/scripts/doctor.mjs +336 -0
  68. package/template/.agents/skills/design/impeccable/scripts/embed-prompt.mjs +133 -0
  69. package/template/.agents/skills/design/impeccable/scripts/generate-image.mjs +240 -0
  70. package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +98 -18
  71. package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +46 -6
  72. package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +518 -50
  73. package/template/.agents/skills/design/impeccable/scripts/hook.mjs +25 -8
  74. package/template/.agents/skills/design/impeccable/scripts/lib/artifact-schema.mjs +93 -0
  75. package/template/.agents/skills/design/impeccable/scripts/lib/composition-catalog.mjs +200 -0
  76. package/template/.agents/skills/design/impeccable/scripts/lib/concept-catalog.mjs +357 -0
  77. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +27 -7
  78. package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +17 -8
  79. package/template/.agents/skills/design/impeccable/scripts/lib/provider.mjs +5 -0
  80. package/template/.agents/skills/design/impeccable/scripts/lib/roll-selection.mjs +362 -0
  81. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-deep.mjs +457 -0
  82. package/template/.agents/skills/design/impeccable/scripts/lib/staleness-notice.mjs +169 -0
  83. package/template/.agents/skills/design/impeccable/scripts/lib/staleness.mjs +457 -0
  84. package/template/.agents/skills/design/impeccable/scripts/lib/surface-briefs.mjs +151 -0
  85. package/template/.agents/skills/design/impeccable/scripts/lib/target-slug.mjs +33 -0
  86. package/template/.agents/skills/design/impeccable/scripts/lib/template-extensions.mjs +146 -0
  87. package/template/.agents/skills/design/impeccable/scripts/live/accept-css.mjs +617 -0
  88. package/template/.agents/skills/design/impeccable/scripts/live/accept-verify.mjs +60 -0
  89. package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +7 -1
  90. package/template/.agents/skills/design/impeccable/scripts/live/completion.mjs +10 -1
  91. package/template/.agents/skills/design/impeccable/scripts/live/event-validation.mjs +67 -5
  92. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/astro.mjs +47 -0
  93. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/detect-utils.mjs +73 -0
  94. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/index.mjs +143 -0
  95. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/journal.mjs +197 -0
  96. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nextjs.mjs +49 -0
  97. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/nuxt.mjs +161 -0
  98. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/script-src.mjs +17 -0
  99. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/static-html.mjs +26 -0
  100. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/sveltekit.mjs +71 -0
  101. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tag-strategy.mjs +247 -0
  102. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/tanstack-start.mjs +70 -0
  103. package/template/.agents/skills/design/impeccable/scripts/live/frameworks/vite-generic.mjs +42 -0
  104. package/template/.agents/skills/design/impeccable/scripts/live/generation-preflight.mjs +149 -0
  105. package/template/.agents/skills/design/impeccable/scripts/live/instructions.mjs +142 -0
  106. package/template/.agents/skills/design/impeccable/scripts/live/poll-lanes.mjs +14 -0
  107. package/template/.agents/skills/design/impeccable/scripts/live/roots.mjs +508 -0
  108. package/template/.agents/skills/design/impeccable/scripts/live/session-store.mjs +324 -50
  109. package/template/.agents/skills/design/impeccable/scripts/live/source-lock.mjs +105 -0
  110. package/template/.agents/skills/design/impeccable/scripts/live/source-search.mjs +105 -0
  111. package/template/.agents/skills/design/impeccable/scripts/live/svelte-ast.mjs +961 -0
  112. package/template/.agents/skills/design/impeccable/scripts/live/svelte-component.mjs +588 -72
  113. package/template/.agents/skills/design/impeccable/scripts/live/sveltekit-adapter.mjs +59 -17
  114. package/template/.agents/skills/design/impeccable/scripts/live/tanstack-adapter.mjs +280 -0
  115. package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +135 -0
  116. package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +210 -68
  117. package/template/.agents/skills/design/impeccable/scripts/live-browser.js +1562 -223
  118. package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +3 -0
  119. package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +33 -1
  120. package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +175 -255
  121. package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +26 -6
  122. package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +6 -1
  123. package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +61 -16
  124. package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +39 -10
  125. package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +573 -47
  126. package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +17 -7
  127. package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +124 -91
  128. package/template/.agents/skills/design/impeccable/scripts/live.mjs +88 -26
  129. package/template/.agents/skills/design/impeccable/scripts/palette.mjs +76 -81
  130. package/template/.agents/skills/design/impeccable/scripts/pin.mjs +18 -11
  131. package/template/.agents/skills/design/impeccable/scripts/serve-question.mjs +932 -0
  132. package/template/.agents/skills/design/impeccable/scripts/surface-brief.mjs +74 -0
  133. package/template/.agents/skills/design/shadcn/SKILL.md +46 -11
  134. package/template/.agents/skills/design/shadcn/cli.md +49 -16
  135. package/template/.agents/skills/design/shadcn/customization.md +14 -7
  136. package/template/.agents/skills/design/shadcn/evals/evals.json +30 -0
  137. package/template/.agents/skills/design/shadcn/mcp.md +27 -16
  138. package/template/.agents/skills/design/shadcn/registry.md +277 -0
  139. package/template/.agents/skills/design/shadcn/rules/chat.md +224 -0
  140. package/template/.agents/skills/design/shadcn/rules/composition.md +20 -2
  141. package/template/.agents/skills/design/shadcn/rules/styling.md +23 -0
  142. package/template/.agents/skills/find-skills/SKILL.md +2 -3
  143. package/template/.agents/skills/hono/SKILL.md +579 -0
  144. package/template/.agents/skills/integrations/langfuse/SKILL.md +10 -6
  145. package/template/.agents/skills/integrations/langfuse/references/ci-cd.md +41 -0
  146. package/template/.agents/skills/integrations/langfuse/references/cli.md +8 -0
  147. package/template/.agents/skills/integrations/langfuse/references/error-analysis.md +15 -27
  148. package/template/.agents/skills/integrations/langfuse/references/instrumentation.md +39 -52
  149. package/template/.agents/skills/integrations/langfuse/references/judge-calibration.md +5 -2
  150. package/template/.agents/skills/integrations/langfuse/references/prompt-engineering.md +35 -0
  151. package/template/.agents/skills/integrations/langfuse/references/prompt-migration.md +41 -196
  152. package/template/.agents/skills/integrations/langfuse/references/skill-feedback.md +3 -0
  153. package/template/.agents/skills/integrations/langfuse/references/trace-evaluator-upgrade.md +76 -0
  154. package/template/.agents/skills/integrations/langfuse/references/user-feedback.md +4 -0
  155. package/template/.agents/skills/integrations/langfuse/references/v4-project-migration.md +73 -0
  156. package/template/.agents/skills/supabase/CHANGELOG.md +71 -0
  157. package/template/.agents/skills/supabase/SKILL.md +145 -0
  158. package/template/.agents/skills/supabase/assets/feedback-issue-template.md +17 -0
  159. package/template/.agents/skills/supabase/references/skill-feedback.md +17 -0
  160. package/template/.agents/skills/supabase-postgres-best-practices/CHANGELOG.md +73 -0
  161. package/template/.agents/skills/supabase-postgres-best-practices/SKILL.md +64 -0
  162. package/template/.agents/skills/supabase-postgres-best-practices/references/_contributing.md +170 -0
  163. package/template/.agents/skills/supabase-postgres-best-practices/references/_sections.md +39 -0
  164. package/template/.agents/skills/supabase-postgres-best-practices/references/_template.md +34 -0
  165. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-full-text-search.md +55 -0
  166. package/template/.agents/skills/supabase-postgres-best-practices/references/advanced-jsonb-indexing.md +49 -0
  167. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-idle-timeout.md +46 -0
  168. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-limits.md +44 -0
  169. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-pooling.md +41 -0
  170. package/template/.agents/skills/supabase-postgres-best-practices/references/conn-prepared-statements.md +46 -0
  171. package/template/.agents/skills/supabase-postgres-best-practices/references/data-batch-inserts.md +54 -0
  172. package/template/.agents/skills/supabase-postgres-best-practices/references/data-n-plus-one.md +53 -0
  173. package/template/.agents/skills/supabase-postgres-best-practices/references/data-pagination.md +50 -0
  174. package/template/.agents/skills/supabase-postgres-best-practices/references/data-upsert.md +50 -0
  175. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-advisory.md +56 -0
  176. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-deadlock-prevention.md +68 -0
  177. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-short-transactions.md +50 -0
  178. package/template/.agents/skills/supabase-postgres-best-practices/references/lock-skip-locked.md +54 -0
  179. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-explain-analyze.md +45 -0
  180. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-pg-stat-statements.md +55 -0
  181. package/template/.agents/skills/supabase-postgres-best-practices/references/monitor-vacuum-analyze.md +55 -0
  182. package/template/.agents/skills/supabase-postgres-best-practices/references/query-composite-indexes.md +44 -0
  183. package/template/.agents/skills/supabase-postgres-best-practices/references/query-covering-indexes.md +40 -0
  184. package/template/.agents/skills/supabase-postgres-best-practices/references/query-index-types.md +48 -0
  185. package/template/.agents/skills/supabase-postgres-best-practices/references/query-missing-indexes.md +43 -0
  186. package/template/.agents/skills/supabase-postgres-best-practices/references/query-partial-indexes.md +45 -0
  187. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-constraints.md +80 -0
  188. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-data-types.md +46 -0
  189. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-foreign-key-indexes.md +59 -0
  190. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-lowercase-identifiers.md +55 -0
  191. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-partitioning.md +55 -0
  192. package/template/.agents/skills/supabase-postgres-best-practices/references/schema-primary-keys.md +61 -0
  193. package/template/.agents/skills/supabase-postgres-best-practices/references/security-privileges.md +54 -0
  194. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-basics.md +50 -0
  195. package/template/.agents/skills/supabase-postgres-best-practices/references/security-rls-performance.md +63 -0
  196. package/template/.agents/upstreams.json +34 -12
  197. package/template/.agents/skills/design/impeccable/reference/brand.md +0 -108
  198. package/template/.agents/skills/design/impeccable/reference/codex.md +0 -105
  199. package/template/.agents/skills/design/impeccable/reference/interaction-design.md +0 -189
  200. package/template/.agents/skills/integrations/langfuse/references/sdk-upgrade.md +0 -175
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ var __export = (target, all) => {
9
9
  };
10
10
 
11
11
  // src/ui/prompts.ts
12
- var clackPromise, warnedClackUnavailable, loadClack, QUIVER_ART, TAGLINE, colorEnabled, palette, block, banner, outro, step, info, success, warn, error, password, selectGrouped, selectGroupedText;
12
+ var clackPromise, warnedClackUnavailable, loadClack, QUIVER_ART, TAGLINE, colorEnabled, palette, block, banner, outro, step, info, success, warn, error, selectGrouped, selectGroupedText;
13
13
  var init_prompts = __esm({
14
14
  "src/ui/prompts.ts"() {
15
15
  "use strict";
@@ -96,23 +96,6 @@ ${message}
96
96
  if (clack) clack.log.error(message);
97
97
  else console.error(message);
98
98
  };
99
- password = async (message) => {
100
- const clack = await loadClack();
101
- if (clack) {
102
- const value = await clack.password({ message });
103
- if (clack.isCancel(value)) return null;
104
- return value;
105
- }
106
- const { createInterface } = await import("readline");
107
- const rl = createInterface({ input: process.stdin, output: process.stdout });
108
- const answer = await new Promise((res) => {
109
- rl.question(`${message}: `, (a) => {
110
- rl.close();
111
- res(a);
112
- });
113
- });
114
- return answer.trim() || null;
115
- };
116
99
  selectGrouped = async ({
117
100
  message,
118
101
  groups,
@@ -326,7 +309,7 @@ var init_frontmatter = __esm({
326
309
  // src/catalog/discover.ts
327
310
  import { existsSync, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
328
311
  import { relative as relative2, resolve as resolve2 } from "path";
329
- var readConfig, discoverSkills, discoverCommands, discoverMcp, loadCatalog;
312
+ var readConfig, discoverSkills, discoverCommands, discoverMcp, discoverPlugins, loadCatalog;
330
313
  var init_discover = __esm({
331
314
  "src/catalog/discover.ts"() {
332
315
  "use strict";
@@ -400,6 +383,20 @@ var init_discover = __esm({
400
383
  configDigest: jsonDigest(servers[name])
401
384
  }));
402
385
  };
386
+ discoverPlugins = (root, config) => Object.entries(config.plugins ?? {}).sort(([a], [b]) => a.localeCompare(b)).map(([name, plugin]) => {
387
+ const absPath = resolve2(root, plugin.sourcePath);
388
+ if (!existsSync(absPath)) {
389
+ throw new Error(`Plugin "${name}" source not found: ${absPath}`);
390
+ }
391
+ return {
392
+ name,
393
+ provider: plugin.provider,
394
+ sourcePath: plugin.sourcePath,
395
+ absPath,
396
+ digest: jsonDigest({ config: plugin, content: fileDigest(absPath) }),
397
+ requires: plugin.requires ?? []
398
+ };
399
+ });
403
400
  loadCatalog = (catalog) => {
404
401
  const { config, path } = readConfig(catalog.root);
405
402
  return {
@@ -407,14 +404,15 @@ var init_discover = __esm({
407
404
  configPath: path,
408
405
  skills: discoverSkills(catalog.root),
409
406
  commands: discoverCommands(catalog.root),
410
- mcp: discoverMcp(config)
407
+ mcp: discoverMcp(config),
408
+ plugins: discoverPlugins(catalog.root, config)
411
409
  };
412
410
  };
413
411
  }
414
412
  });
415
413
 
416
414
  // src/catalog/entries.ts
417
- var skillToEntry, commandToEntry, mcpToEntry;
415
+ var skillToEntry, commandToEntry, mcpToEntry, pluginToEntry;
418
416
  var init_entries = __esm({
419
417
  "src/catalog/entries.ts"() {
420
418
  "use strict";
@@ -437,6 +435,13 @@ var init_entries = __esm({
437
435
  tools: null,
438
436
  toolsFetchedAt: null
439
437
  });
438
+ pluginToEntry = (plugin) => ({
439
+ type: "plugin",
440
+ provider: plugin.provider,
441
+ sourcePath: plugin.sourcePath,
442
+ digest: plugin.digest,
443
+ requires: plugin.requires
444
+ });
440
445
  }
441
446
  });
442
447
 
@@ -526,6 +531,12 @@ var init_materialize = __esm({
526
531
  mkdirSync(dirname(dest), { recursive: true });
527
532
  cpSync(command.absPath, dest, { force: true });
528
533
  }
534
+ for (const plugin of catalog.plugins) {
535
+ if (!selection.plugins.includes(plugin.name)) continue;
536
+ const dest = resolve4(destRoot, plugin.sourcePath);
537
+ mkdirSync(dirname(dest), { recursive: true });
538
+ cpSync(plugin.absPath, dest, { force: true });
539
+ }
529
540
  const filteredConfig = {};
530
541
  if (catalog.config.shared) filteredConfig["shared"] = catalog.config.shared;
531
542
  const mcpServers = {};
@@ -533,6 +544,14 @@ var init_materialize = __esm({
533
544
  if (selection.mcp.includes(mcp.name)) mcpServers[mcp.name] = mcp.server;
534
545
  }
535
546
  if (Object.keys(mcpServers).length) filteredConfig["mcpServers"] = mcpServers;
547
+ const plugins = {};
548
+ for (const plugin of catalog.plugins) {
549
+ if (!selection.plugins.includes(plugin.name)) continue;
550
+ plugins[plugin.name] = catalog.config.plugins?.[plugin.name];
551
+ }
552
+ if (Object.keys(plugins).length) filteredConfig["plugins"] = plugins;
553
+ if (catalog.config.opencode) filteredConfig["opencode"] = catalog.config.opencode;
554
+ if (catalog.config.tui) filteredConfig["tui"] = catalog.config.tui;
536
555
  if (catalog.config.claude) filteredConfig["claude"] = catalog.config.claude;
537
556
  writeFileSync(
538
557
  resolve4(destRoot, "config.json"),
@@ -568,53 +587,10 @@ var init_materialize = __esm({
568
587
 
569
588
  // src/github/auth.ts
570
589
  import { execFileSync } from "child_process";
571
- import {
572
- chmodSync,
573
- existsSync as existsSync4,
574
- mkdirSync as mkdirSync2,
575
- readFileSync as readFileSync4,
576
- rmSync as rmSync2,
577
- writeFileSync as writeFileSync2
578
- } from "fs";
579
- import { homedir } from "os";
580
- import { resolve as resolve5 } from "path";
581
- var configDir, authFilePath, readAuthFile, readStoredToken, writeStoredToken, deleteStoredToken, cachedToken, ghToken, resolveGithubToken, resetTokenCache, validateToken;
590
+ var cachedToken, ghToken, resolveGithubToken;
582
591
  var init_auth = __esm({
583
592
  "src/github/auth.ts"() {
584
593
  "use strict";
585
- configDir = () => {
586
- const base = process.env["XDG_CONFIG_HOME"] || resolve5(homedir(), ".config");
587
- return resolve5(base, "quiver");
588
- };
589
- authFilePath = () => resolve5(configDir(), "auth.json");
590
- readAuthFile = () => {
591
- const path = authFilePath();
592
- if (!existsSync4(path)) return null;
593
- try {
594
- return JSON.parse(readFileSync4(path, "utf8"));
595
- } catch {
596
- return null;
597
- }
598
- };
599
- readStoredToken = () => readAuthFile()?.github?.token ?? null;
600
- writeStoredToken = (token, login2) => {
601
- mkdirSync2(configDir(), { recursive: true });
602
- const file = {
603
- github: { token, login: login2, createdAt: (/* @__PURE__ */ new Date()).toISOString() }
604
- };
605
- writeFileSync2(authFilePath(), JSON.stringify(file, null, 2) + "\n", {
606
- mode: 384
607
- });
608
- chmodSync(authFilePath(), 384);
609
- resetTokenCache();
610
- };
611
- deleteStoredToken = () => {
612
- const path = authFilePath();
613
- if (!existsSync4(path)) return false;
614
- rmSync2(path);
615
- resetTokenCache();
616
- return true;
617
- };
618
594
  ghToken = () => {
619
595
  try {
620
596
  const out = execFileSync("gh", ["auth", "token"], {
@@ -629,38 +605,14 @@ var init_auth = __esm({
629
605
  };
630
606
  resolveGithubToken = () => {
631
607
  if (cachedToken !== void 0) return cachedToken;
632
- cachedToken = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"] ?? readStoredToken() ?? ghToken();
608
+ cachedToken = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"] ?? ghToken();
633
609
  return cachedToken;
634
610
  };
635
- resetTokenCache = () => {
636
- cachedToken = void 0;
637
- };
638
- validateToken = async (token) => {
639
- let res;
640
- try {
641
- res = await fetch("https://api.github.com/user", {
642
- headers: {
643
- Accept: "application/vnd.github+json",
644
- "User-Agent": "quiver-cli",
645
- Authorization: `Bearer ${token}`
646
- }
647
- });
648
- } catch (err) {
649
- return {
650
- ok: false,
651
- reason: err instanceof Error ? err.message : "fetch failed"
652
- };
653
- }
654
- if (res.status === 401) return { ok: false, reason: "token is invalid or expired" };
655
- if (!res.ok) return { ok: false, reason: `HTTP ${res.status}` };
656
- const body = await res.json();
657
- return { ok: true, login: body.login ?? "unknown" };
658
- };
659
611
  }
660
612
  });
661
613
 
662
614
  // src/github/api.ts
663
- import { mkdirSync as mkdirSync3 } from "fs";
615
+ import { mkdirSync as mkdirSync2 } from "fs";
664
616
  import { Readable } from "stream";
665
617
  import { pipeline } from "stream/promises";
666
618
  import * as tar from "tar";
@@ -678,13 +630,14 @@ var init_api = __esm({
678
630
  if (token) h["Authorization"] = `Bearer ${token}`;
679
631
  return h;
680
632
  };
681
- failureReason = (status2) => {
682
- if (status2 === 401) return "authentication failed - run `quiver-cli login` with a valid token";
683
- if (status2 === 403 || status2 === 429)
684
- return "rate-limited or access denied (run `quiver-cli login` or set GITHUB_TOKEN)";
685
- if (status2 === 404)
686
- return "repo or ref not found (private repo? run `quiver-cli login`)";
687
- return `HTTP ${status2}`;
633
+ failureReason = (status) => {
634
+ if (status === 401)
635
+ return "authentication failed - set GITHUB_TOKEN or log in with the gh CLI";
636
+ if (status === 403 || status === 429)
637
+ return "rate-limited or access denied (set GITHUB_TOKEN or log in with the gh CLI)";
638
+ if (status === 404)
639
+ return "repo or ref not found (private repo? set GITHUB_TOKEN or log in with the gh CLI)";
640
+ return `HTTP ${status}`;
688
641
  };
689
642
  apiFetch = async (url) => {
690
643
  let res;
@@ -721,7 +674,7 @@ var init_api = __esm({
721
674
  );
722
675
  if (!res.ok) return res;
723
676
  if (!res.value.body) return { ok: false, reason: "empty tarball response" };
724
- mkdirSync3(destDir, { recursive: true });
677
+ mkdirSync2(destDir, { recursive: true });
725
678
  try {
726
679
  await pipeline(
727
680
  Readable.fromWeb(res.value.body),
@@ -746,9 +699,9 @@ __export(remote_exports, {
746
699
  fetchRemoteCatalog: () => fetchRemoteCatalog,
747
700
  parseGithubSource: () => parseGithubSource
748
701
  });
749
- import { existsSync as existsSync5, mkdtempSync, renameSync, rmSync as rmSync3, mkdirSync as mkdirSync4 } from "fs";
750
- import { homedir as homedir2 } from "os";
751
- import { dirname as dirname2, resolve as resolve6 } from "path";
702
+ import { existsSync as existsSync4, mkdtempSync, renameSync, rmSync as rmSync2, mkdirSync as mkdirSync3 } from "fs";
703
+ import { homedir } from "os";
704
+ import { dirname as dirname2, resolve as resolve5 } from "path";
752
705
  var parseGithubSource, catalogCacheRoot, catalogCacheDir, fetchRemoteCatalog;
753
706
  var init_remote = __esm({
754
707
  "src/catalog/remote.ts"() {
@@ -771,10 +724,10 @@ var init_remote = __esm({
771
724
  return { repo: `${owner}/${repo}`, path: pathParts.join("/"), ref };
772
725
  };
773
726
  catalogCacheRoot = () => {
774
- const base = process.env["XDG_CACHE_HOME"] || resolve6(homedir2(), ".cache");
775
- return resolve6(base, "quiver", "catalogs");
727
+ const base = process.env["XDG_CACHE_HOME"] || resolve5(homedir(), ".cache");
728
+ return resolve5(base, "quiver", "catalogs");
776
729
  };
777
- catalogCacheDir = (repo, sha) => resolve6(catalogCacheRoot(), `${repo.replace("/", "-")}-${sha.slice(0, 12)}`);
730
+ catalogCacheDir = (repo, sha) => resolve5(catalogCacheRoot(), `${repo.replace("/", "-")}-${sha.slice(0, 12)}`);
778
731
  fetchRemoteCatalog = async (source, options = {}) => {
779
732
  const spec = parseGithubSource(source);
780
733
  let ref = spec.ref;
@@ -794,22 +747,22 @@ var init_remote = __esm({
794
747
  sha = commit.value;
795
748
  }
796
749
  const cacheDir = catalogCacheDir(spec.repo, sha);
797
- if (!existsSync5(cacheDir)) {
798
- mkdirSync4(dirname2(cacheDir), { recursive: true });
750
+ if (!existsSync4(cacheDir)) {
751
+ mkdirSync3(dirname2(cacheDir), { recursive: true });
799
752
  const tmp = mkdtempSync(`${cacheDir}.tmp-`);
800
753
  const result = await downloadTarball(spec.repo, sha, tmp);
801
754
  if (!result.ok) {
802
- rmSync3(tmp, { recursive: true, force: true });
755
+ rmSync2(tmp, { recursive: true, force: true });
803
756
  throw new Error(`Cannot download ${spec.repo}@${sha.slice(0, 12)}: ${result.reason}`);
804
757
  }
805
758
  try {
806
759
  renameSync(tmp, cacheDir);
807
760
  } catch {
808
- rmSync3(tmp, { recursive: true, force: true });
761
+ rmSync2(tmp, { recursive: true, force: true });
809
762
  }
810
763
  }
811
- const root = spec.path ? resolve6(cacheDir, spec.path) : cacheDir;
812
- if (!existsSync5(root)) {
764
+ const root = spec.path ? resolve5(cacheDir, spec.path) : cacheDir;
765
+ if (!existsSync4(root)) {
813
766
  throw new Error(
814
767
  `Catalog path "${spec.path}" not found in ${spec.repo}@${sha.slice(0, 12)}.`
815
768
  );
@@ -820,21 +773,21 @@ var init_remote = __esm({
820
773
  });
821
774
 
822
775
  // src/catalog/resolve.ts
823
- import { accessSync, constants, existsSync as existsSync6 } from "fs";
824
- import { dirname as dirname3, relative as relative3, resolve as resolve7, sep } from "path";
776
+ import { accessSync, constants, existsSync as existsSync5 } from "fs";
777
+ import { dirname as dirname3, relative as relative3, resolve as resolve6, sep } from "path";
825
778
  import { fileURLToPath } from "url";
826
779
  var packageRoot, DEFAULT_CATALOG_SOURCE, resolveCatalog, isInstalledPackage, isBundledCatalog, isCatalogWritable;
827
780
  var init_resolve = __esm({
828
781
  "src/catalog/resolve.ts"() {
829
782
  "use strict";
830
- packageRoot = resolve7(dirname3(fileURLToPath(import.meta.url)), "..");
783
+ packageRoot = resolve6(dirname3(fileURLToPath(import.meta.url)), "..");
831
784
  DEFAULT_CATALOG_SOURCE = "local:template/.agents";
832
785
  resolveCatalog = async (source = DEFAULT_CATALOG_SOURCE, options = {}) => {
833
786
  const [scheme, ...rest] = source.split(":");
834
787
  const spec = rest.join(":");
835
788
  if (scheme === "local") {
836
- const root = resolve7(packageRoot, spec);
837
- if (!existsSync6(root)) {
789
+ const root = resolve6(packageRoot, spec);
790
+ if (!existsSync5(root)) {
838
791
  throw new Error(`Catalog not found at ${root} (source: ${source}).`);
839
792
  }
840
793
  return { source, root };
@@ -879,7 +832,9 @@ var init_schema = __esm({
879
832
  if (idx === -1) return null;
880
833
  const type = id.slice(0, idx);
881
834
  const name = id.slice(idx + 1);
882
- if (type !== "skill" && type !== "command" && type !== "mcp") return null;
835
+ if (type !== "skill" && type !== "command" && type !== "mcp" && type !== "plugin") {
836
+ return null;
837
+ }
883
838
  if (!name) return null;
884
839
  return { type, name };
885
840
  };
@@ -887,19 +842,19 @@ var init_schema = __esm({
887
842
  });
888
843
 
889
844
  // src/lockfile/io.ts
890
- import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
891
- import { resolve as resolve8 } from "path";
845
+ import { existsSync as existsSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
846
+ import { resolve as resolve7 } from "path";
892
847
  var lockfilePath, lockfileExists, readLockfile, writeLockfile, emptyLockfile;
893
848
  var init_io = __esm({
894
849
  "src/lockfile/io.ts"() {
895
850
  "use strict";
896
851
  init_schema();
897
- lockfilePath = (targetRoot) => resolve8(targetRoot, LOCKFILE_NAME);
898
- lockfileExists = (targetRoot) => existsSync7(lockfilePath(targetRoot));
852
+ lockfilePath = (targetRoot) => resolve7(targetRoot, LOCKFILE_NAME);
853
+ lockfileExists = (targetRoot) => existsSync6(lockfilePath(targetRoot));
899
854
  readLockfile = (targetRoot) => {
900
855
  const path = lockfilePath(targetRoot);
901
- if (!existsSync7(path)) return null;
902
- const parsed = JSON.parse(readFileSync5(path, "utf8"));
856
+ if (!existsSync6(path)) return null;
857
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
903
858
  if (parsed.version !== LOCKFILE_VERSION) {
904
859
  throw new Error(
905
860
  `Unsupported ${LOCKFILE_NAME} version ${parsed.version}; expected ${LOCKFILE_VERSION}.`
@@ -918,7 +873,7 @@ var init_io = __esm({
918
873
  ...lock.providers !== void 0 ? { providers: lock.providers } : {},
919
874
  entries: sortedEntries
920
875
  };
921
- writeFileSync3(lockfilePath(targetRoot), JSON.stringify(ordered, null, 2) + "\n");
876
+ writeFileSync2(lockfilePath(targetRoot), JSON.stringify(ordered, null, 2) + "\n");
922
877
  };
923
878
  emptyLockfile = (catalogSource, remote = {}) => ({
924
879
  version: LOCKFILE_VERSION,
@@ -990,7 +945,7 @@ var init_resolve2 = __esm({
990
945
  });
991
946
 
992
947
  // src/providers/claude.ts
993
- import { resolve as resolve9 } from "path";
948
+ import { resolve as resolve8 } from "path";
994
949
  var formatMcpJson, planClaude;
995
950
  var init_claude = __esm({
996
951
  "src/providers/claude.ts"() {
@@ -1005,33 +960,33 @@ var init_claude = __esm({
1005
960
  const removeFiles = [];
1006
961
  if (claudeSettings) {
1007
962
  files.push({
1008
- path: resolve9(targetRoot, ".claude/settings.json"),
963
+ path: resolve8(targetRoot, ".claude/settings.json"),
1009
964
  content: JSON.stringify(claudeSettings, null, 2) + "\n"
1010
965
  });
1011
966
  }
1012
967
  const mcp = formatMcpJson(mcpServers);
1013
- const mcpPath = resolve9(targetRoot, ".mcp.json");
968
+ const mcpPath = resolve8(targetRoot, ".mcp.json");
1014
969
  if (mcp) files.push({ path: mcpPath, content: mcp });
1015
970
  else removeFiles.push(mcpPath);
1016
971
  const skillNames = selected.skills.map((s) => s.name);
1017
972
  const commandFiles = selected.commands.map((c) => `${c.name}.md`);
1018
973
  const symlinks = [
1019
974
  ...selected.skills.map((s) => ({
1020
- path: resolve9(targetRoot, ".claude/skills", s.name),
975
+ path: resolve8(targetRoot, ".claude/skills", s.name),
1021
976
  target: s.absDir
1022
977
  })),
1023
978
  ...selected.commands.map((c) => ({
1024
- path: resolve9(targetRoot, ".claude/commands", `${c.name}.md`),
979
+ path: resolve8(targetRoot, ".claude/commands", `${c.name}.md`),
1025
980
  target: c.absPath
1026
981
  }))
1027
982
  ];
1028
983
  const managedDirs = [
1029
984
  {
1030
- path: resolve9(targetRoot, ".claude/skills"),
985
+ path: resolve8(targetRoot, ".claude/skills"),
1031
986
  expected: new Set(skillNames)
1032
987
  },
1033
988
  {
1034
- path: resolve9(targetRoot, ".claude/commands"),
989
+ path: resolve8(targetRoot, ".claude/commands"),
1035
990
  expected: new Set(commandFiles)
1036
991
  }
1037
992
  ];
@@ -1041,7 +996,7 @@ var init_claude = __esm({
1041
996
  });
1042
997
 
1043
998
  // src/providers/codex.ts
1044
- import { resolve as resolve10 } from "path";
999
+ import { resolve as resolve9 } from "path";
1045
1000
  import TOML from "@iarna/toml";
1046
1001
  var buildConfig, planCodex;
1047
1002
  var init_codex = __esm({
@@ -1085,7 +1040,7 @@ var init_codex = __esm({
1085
1040
  const files = [];
1086
1041
  const removeFiles = [];
1087
1042
  const config = buildConfig(rawMcpServers);
1088
- const path = resolve10(targetRoot, ".codex/config.toml");
1043
+ const path = resolve9(targetRoot, ".codex/config.toml");
1089
1044
  if (config) {
1090
1045
  files.push({
1091
1046
  path,
@@ -1101,19 +1056,19 @@ var init_codex = __esm({
1101
1056
 
1102
1057
  // src/providers/fsops.ts
1103
1058
  import {
1104
- existsSync as existsSync8,
1059
+ existsSync as existsSync7,
1105
1060
  lstatSync,
1106
- mkdirSync as mkdirSync5,
1061
+ mkdirSync as mkdirSync4,
1107
1062
  readdirSync as readdirSync3,
1108
- readFileSync as readFileSync6,
1063
+ readFileSync as readFileSync5,
1109
1064
  readlinkSync,
1110
1065
  rmdirSync,
1111
- rmSync as rmSync4,
1066
+ rmSync as rmSync3,
1112
1067
  symlinkSync,
1113
1068
  unlinkSync,
1114
- writeFileSync as writeFileSync4
1069
+ writeFileSync as writeFileSync3
1115
1070
  } from "fs";
1116
- import { dirname as dirname4, relative as relative4, resolve as resolve11 } from "path";
1071
+ import { dirname as dirname4, relative as relative4, resolve as resolve10 } from "path";
1117
1072
  var isENOENT, isMatchingSymlink, removePath, applyOutputs, checkOutputs;
1118
1073
  var init_fsops = __esm({
1119
1074
  "src/providers/fsops.ts"() {
@@ -1122,7 +1077,7 @@ var init_fsops = __esm({
1122
1077
  isMatchingSymlink = (path, target) => {
1123
1078
  const stats = lstatSync(path);
1124
1079
  if (!stats.isSymbolicLink()) return false;
1125
- return resolve11(dirname4(path), readlinkSync(path)) === target;
1080
+ return resolve10(dirname4(path), readlinkSync(path)) === target;
1126
1081
  };
1127
1082
  removePath = (path) => {
1128
1083
  let stats;
@@ -1133,7 +1088,7 @@ var init_fsops = __esm({
1133
1088
  throw e;
1134
1089
  }
1135
1090
  if (stats.isSymbolicLink()) unlinkSync(path);
1136
- else rmSync4(path, { force: true, recursive: true });
1091
+ else rmSync3(path, { force: true, recursive: true });
1137
1092
  };
1138
1093
  applyOutputs = ({
1139
1094
  files,
@@ -1146,22 +1101,22 @@ var init_fsops = __esm({
1146
1101
  for (const out of files) {
1147
1102
  let current = null;
1148
1103
  try {
1149
- current = readFileSync6(out.path, "utf8");
1104
+ current = readFileSync5(out.path, "utf8");
1150
1105
  } catch (e) {
1151
1106
  if (!isENOENT(e)) throw e;
1152
1107
  }
1153
1108
  if (current === out.content) continue;
1154
- mkdirSync5(dirname4(out.path), { recursive: true });
1155
- writeFileSync4(out.path, out.content);
1109
+ mkdirSync4(dirname4(out.path), { recursive: true });
1110
+ writeFileSync3(out.path, out.content);
1156
1111
  result.generated.push(out.path);
1157
1112
  }
1158
1113
  for (const path of removeFiles) {
1159
- if (!existsSync8(path)) continue;
1160
- rmSync4(path, { force: true });
1114
+ if (!existsSync7(path)) continue;
1115
+ rmSync3(path, { force: true });
1161
1116
  result.removed.push(path);
1162
1117
  }
1163
1118
  for (const path of removeDirs) {
1164
- if (!existsSync8(path)) continue;
1119
+ if (!existsSync7(path)) continue;
1165
1120
  removePath(path);
1166
1121
  result.removed.push(path);
1167
1122
  }
@@ -1172,7 +1127,7 @@ var init_fsops = __esm({
1172
1127
  }
1173
1128
  }
1174
1129
  for (const link of symlinks) {
1175
- mkdirSync5(dirname4(link.path), { recursive: true });
1130
+ mkdirSync4(dirname4(link.path), { recursive: true });
1176
1131
  try {
1177
1132
  if (isMatchingSymlink(link.path, link.target)) continue;
1178
1133
  } catch (e) {
@@ -1187,10 +1142,10 @@ var init_fsops = __esm({
1187
1142
  result.linked.push(link.path);
1188
1143
  }
1189
1144
  for (const dir of managedDirs) {
1190
- if (!existsSync8(dir.path)) continue;
1145
+ if (!existsSync7(dir.path)) continue;
1191
1146
  for (const child of readdirSync3(dir.path)) {
1192
1147
  if (dir.expected.has(child)) continue;
1193
- const childPath = resolve11(dir.path, child);
1148
+ const childPath = resolve10(dir.path, child);
1194
1149
  removePath(childPath);
1195
1150
  result.removed.push(childPath);
1196
1151
  }
@@ -1207,7 +1162,7 @@ var init_fsops = __esm({
1207
1162
  const problems = [];
1208
1163
  for (const out of files) {
1209
1164
  try {
1210
- if (readFileSync6(out.path, "utf8") !== out.content) {
1165
+ if (readFileSync5(out.path, "utf8") !== out.content) {
1211
1166
  problems.push(`out of sync: ${out.path}`);
1212
1167
  }
1213
1168
  } catch (e) {
@@ -1216,10 +1171,10 @@ var init_fsops = __esm({
1216
1171
  }
1217
1172
  }
1218
1173
  for (const path of removeFiles) {
1219
- if (existsSync8(path)) problems.push(`stale (should be removed): ${path}`);
1174
+ if (existsSync7(path)) problems.push(`stale (should be removed): ${path}`);
1220
1175
  }
1221
1176
  for (const path of removeDirs) {
1222
- if (existsSync8(path)) problems.push(`stale (should be removed): ${path}`);
1177
+ if (existsSync7(path)) problems.push(`stale (should be removed): ${path}`);
1223
1178
  }
1224
1179
  for (const link of symlinks) {
1225
1180
  try {
@@ -1232,10 +1187,10 @@ var init_fsops = __esm({
1232
1187
  }
1233
1188
  }
1234
1189
  for (const dir of managedDirs) {
1235
- if (!existsSync8(dir.path)) continue;
1190
+ if (!existsSync7(dir.path)) continue;
1236
1191
  for (const child of readdirSync3(dir.path)) {
1237
1192
  if (!dir.expected.has(child)) {
1238
- problems.push(`unexpected shim: ${resolve11(dir.path, child)}`);
1193
+ problems.push(`unexpected shim: ${resolve10(dir.path, child)}`);
1239
1194
  }
1240
1195
  }
1241
1196
  }
@@ -1245,61 +1200,111 @@ var init_fsops = __esm({
1245
1200
  });
1246
1201
 
1247
1202
  // src/providers/opencode.ts
1248
- import { resolve as resolve12 } from "path";
1249
- var formatOpenCodeJson, planOpenCode;
1203
+ import { existsSync as existsSync8 } from "fs";
1204
+ import { resolve as resolve11 } from "path";
1205
+ var formatOpenCodeJson, preserveEnvReference, preserveEnvReferences, planOpenCode;
1250
1206
  var init_opencode = __esm({
1251
1207
  "src/providers/opencode.ts"() {
1252
1208
  "use strict";
1253
- formatOpenCodeJson = (servers) => {
1254
- if (Object.keys(servers).length === 0) return null;
1209
+ formatOpenCodeJson = (servers, overlay) => {
1210
+ if (Object.keys(servers).length === 0 && !overlay) return null;
1255
1211
  const mcp = {};
1256
1212
  for (const [name, server] of Object.entries(servers)) {
1257
1213
  if (server.transport === "http") {
1258
1214
  mcp[name] = {
1259
1215
  type: "remote",
1260
- url: server.url,
1261
- ...server.headers ? { headers: server.headers } : {}
1216
+ url: preserveEnvReference(server.url),
1217
+ ...server.headers ? { headers: preserveEnvReferences(server.headers) } : {}
1262
1218
  };
1263
1219
  } else {
1264
1220
  mcp[name] = {
1265
1221
  type: "local",
1266
- command: [server.command, ...server.args ?? []],
1267
- ...server.env ? { environment: server.env } : {}
1222
+ command: [server.command, ...server.args ?? []].map(
1223
+ preserveEnvReference
1224
+ ),
1225
+ ...server.env ? { environment: preserveEnvReferences(server.env) } : {}
1268
1226
  };
1269
1227
  }
1270
1228
  }
1271
- return JSON.stringify(
1272
- { $schema: "https://opencode.ai/config.json", mcp },
1273
- null,
1274
- 2
1275
- ) + "\n";
1276
- };
1229
+ const overlayMcp = overlay?.["mcp"] && typeof overlay["mcp"] === "object" ? overlay["mcp"] : {};
1230
+ const mergedMcp = { ...overlayMcp, ...mcp };
1231
+ const config = {
1232
+ ...overlay,
1233
+ $schema: "https://opencode.ai/config.json",
1234
+ ...Object.keys(mergedMcp).length ? { mcp: mergedMcp } : {}
1235
+ };
1236
+ return JSON.stringify(config, null, 2) + "\n";
1237
+ };
1238
+ preserveEnvReference = (value) => value.replace(/\$\{([^}]+)\}/g, "{env:$1}");
1239
+ preserveEnvReferences = (values) => Object.fromEntries(
1240
+ Object.entries(values).map(([key, value]) => [
1241
+ key,
1242
+ preserveEnvReference(value)
1243
+ ])
1244
+ );
1277
1245
  planOpenCode = (inputs) => {
1278
- const { targetRoot, selected, mcpServers } = inputs;
1246
+ const {
1247
+ targetRoot,
1248
+ selected,
1249
+ rawMcpServers,
1250
+ opencodeConfig,
1251
+ tuiConfig
1252
+ } = inputs;
1279
1253
  const files = [];
1280
1254
  const removeFiles = [];
1281
- const json = formatOpenCodeJson(mcpServers);
1282
- const jsonPath = resolve12(targetRoot, "opencode.json");
1255
+ const json = formatOpenCodeJson(rawMcpServers, opencodeConfig);
1256
+ const jsonPath = resolve11(targetRoot, "opencode.json");
1283
1257
  if (json) files.push({ path: jsonPath, content: json });
1284
1258
  else removeFiles.push(jsonPath);
1259
+ const tuiPath = resolve11(targetRoot, ".opencode/tui.json");
1260
+ const tuiMarkerPath = resolve11(targetRoot, ".opencode/.quiver-tui");
1261
+ if (tuiConfig) {
1262
+ files.push({
1263
+ path: tuiPath,
1264
+ content: JSON.stringify(
1265
+ { ...tuiConfig, $schema: "https://opencode.ai/tui.json" },
1266
+ null,
1267
+ 2
1268
+ ) + "\n"
1269
+ });
1270
+ files.push({ path: tuiMarkerPath, content: "managed by quiver\n" });
1271
+ } else if (existsSync8(tuiMarkerPath)) {
1272
+ removeFiles.push(tuiPath, tuiMarkerPath);
1273
+ }
1285
1274
  const symlinks = [
1286
1275
  ...selected.skills.map((s) => ({
1287
- path: resolve12(targetRoot, ".opencode/skills", s.name),
1276
+ path: resolve11(targetRoot, ".opencode/skills", s.name),
1288
1277
  target: s.absDir
1289
1278
  })),
1290
1279
  ...selected.commands.map((c) => ({
1291
- path: resolve12(targetRoot, ".opencode/commands", `${c.name}.md`),
1280
+ path: resolve11(targetRoot, ".opencode/commands", `${c.name}.md`),
1292
1281
  target: c.absPath
1282
+ })),
1283
+ ...selected.plugins.map((plugin) => ({
1284
+ path: resolve11(
1285
+ targetRoot,
1286
+ ".opencode/plugins",
1287
+ `${plugin.name}${plugin.absPath.endsWith(".js") ? ".js" : ".ts"}`
1288
+ ),
1289
+ target: plugin.absPath
1293
1290
  }))
1294
1291
  ];
1295
1292
  const managedDirs = [
1296
1293
  {
1297
- path: resolve12(targetRoot, ".opencode/skills"),
1294
+ path: resolve11(targetRoot, ".opencode/skills"),
1298
1295
  expected: new Set(selected.skills.map((s) => s.name))
1299
1296
  },
1300
1297
  {
1301
- path: resolve12(targetRoot, ".opencode/commands"),
1298
+ path: resolve11(targetRoot, ".opencode/commands"),
1302
1299
  expected: new Set(selected.commands.map((c) => `${c.name}.md`))
1300
+ },
1301
+ {
1302
+ path: resolve11(targetRoot, ".opencode/plugins"),
1303
+ expected: new Set(
1304
+ selected.plugins.map(
1305
+ (plugin) => `${plugin.name}${plugin.absPath.endsWith(".js") ? ".js" : ".ts"}`
1306
+ )
1307
+ )
1303
1308
  }
1304
1309
  ];
1305
1310
  return { files, removeFiles, symlinks, managedDirs };
@@ -1317,17 +1322,20 @@ var init_selection = __esm({
1317
1322
  const skillNames = /* @__PURE__ */ new Set();
1318
1323
  const commandNames = /* @__PURE__ */ new Set();
1319
1324
  const mcpNames = /* @__PURE__ */ new Set();
1325
+ const pluginNames = /* @__PURE__ */ new Set();
1320
1326
  for (const id of Object.keys(lock.entries)) {
1321
1327
  const parsed = parseEntryId(id);
1322
1328
  if (!parsed) continue;
1323
1329
  if (parsed.type === "skill") skillNames.add(parsed.name);
1324
1330
  else if (parsed.type === "command") commandNames.add(parsed.name);
1325
1331
  else if (parsed.type === "mcp") mcpNames.add(parsed.name);
1332
+ else if (parsed.type === "plugin") pluginNames.add(parsed.name);
1326
1333
  }
1327
1334
  return {
1328
1335
  skills: catalog.skills.filter((s) => skillNames.has(s.name)),
1329
1336
  commands: catalog.commands.filter((c) => commandNames.has(c.name)),
1330
- mcp: catalog.mcp.filter((m) => mcpNames.has(m.name))
1337
+ mcp: catalog.mcp.filter((m) => mcpNames.has(m.name)),
1338
+ plugins: catalog.plugins.filter((p) => pluginNames.has(p.name))
1331
1339
  };
1332
1340
  };
1333
1341
  }
@@ -1335,7 +1343,7 @@ var init_selection = __esm({
1335
1343
 
1336
1344
  // src/providers/write.ts
1337
1345
  import { existsSync as existsSync9, lstatSync as lstatSync2 } from "fs";
1338
- import { basename, relative as relative5, resolve as resolve13 } from "path";
1346
+ import { basename, relative as relative5, resolve as resolve12 } from "path";
1339
1347
  var isLinkable, buildPlan, writeProviders, formatWriteResult, checkProviders;
1340
1348
  var init_write = __esm({
1341
1349
  "src/providers/write.ts"() {
@@ -1360,14 +1368,16 @@ var init_write = __esm({
1360
1368
  const rawMcpServers = {};
1361
1369
  for (const mcp of selected.mcp) rawMcpServers[mcp.name] = mcp.server;
1362
1370
  const mcpServers = interpolateEnvVars(rawMcpServers, onMissingEnv);
1363
- const agentsRoot = resolve13(targetRoot, ".agents");
1371
+ const agentsRoot = resolve12(targetRoot, ".agents");
1364
1372
  const inputs = {
1365
1373
  targetRoot,
1366
1374
  agentsRoot,
1367
1375
  selected,
1368
1376
  mcpServers,
1369
1377
  rawMcpServers,
1370
- claudeSettings: catalog.config.claude?.settings ?? null
1378
+ claudeSettings: catalog.config.claude?.settings ?? null,
1379
+ opencodeConfig: catalog.config.opencode ?? null,
1380
+ tuiConfig: catalog.config.tui ?? null
1371
1381
  };
1372
1382
  const active = lock.providers?.length ? lock.providers : [...PROVIDERS];
1373
1383
  const planByProvider = {
@@ -1391,12 +1401,12 @@ var init_write = __esm({
1391
1401
  }
1392
1402
  }
1393
1403
  const rootSymlinks = [];
1394
- const agentsMd = resolve13(agentsRoot, "AGENTS.md");
1404
+ const agentsMd = resolve12(agentsRoot, "AGENTS.md");
1395
1405
  if (existsSync9(agentsMd)) {
1396
- const rootAgents = resolve13(targetRoot, "AGENTS.md");
1406
+ const rootAgents = resolve12(targetRoot, "AGENTS.md");
1397
1407
  for (const link of [
1398
1408
  { path: rootAgents, target: agentsMd },
1399
- { path: resolve13(targetRoot, "CLAUDE.md"), target: rootAgents }
1409
+ { path: resolve12(targetRoot, "CLAUDE.md"), target: rootAgents }
1400
1410
  ]) {
1401
1411
  if (isLinkable(link.path)) rootSymlinks.push(link);
1402
1412
  else onSkippedRootFile?.(basename(link.path));
@@ -1458,8 +1468,8 @@ var init_write = __esm({
1458
1468
 
1459
1469
  // src/commands/gitignore.ts
1460
1470
  import { execFileSync as execFileSync2 } from "child_process";
1461
- import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
1462
- import { resolve as resolve14 } from "path";
1471
+ import { existsSync as existsSync10, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1472
+ import { resolve as resolve13 } from "path";
1463
1473
  var HEADER, SECRETS_COMMENT, PROVIDER_ENTRIES, SHARED_ENTRIES, SECRET_ENTRIES, patchGitignore, ignoredSourcePaths;
1464
1474
  var init_gitignore = __esm({
1465
1475
  "src/commands/gitignore.ts"() {
@@ -1475,8 +1485,8 @@ var init_gitignore = __esm({
1475
1485
  SHARED_ENTRIES = ["AGENTS.md", "CLAUDE.md"];
1476
1486
  SECRET_ENTRIES = [".env.local"];
1477
1487
  patchGitignore = (targetRoot, providers2) => {
1478
- const path = resolve14(targetRoot, ".gitignore");
1479
- const current = existsSync10(path) ? readFileSync7(path, "utf8") : "";
1488
+ const path = resolve13(targetRoot, ".gitignore");
1489
+ const current = existsSync10(path) ? readFileSync6(path, "utf8") : "";
1480
1490
  const lines = new Set(current.split("\n").map((l) => l.trim()));
1481
1491
  const active = providers2?.length ? providers2 : [...PROVIDERS];
1482
1492
  const shimEntries = [
@@ -1490,7 +1500,7 @@ var init_gitignore = __esm({
1490
1500
  if (missingShims.length) block2.push(HEADER, ...missingShims);
1491
1501
  if (missingSecrets.length) block2.push(SECRETS_COMMENT, ...missingSecrets);
1492
1502
  const prefix = current && !current.endsWith("\n") ? "\n" : "";
1493
- writeFileSync5(
1503
+ writeFileSync4(
1494
1504
  path,
1495
1505
  current + prefix + (current ? "\n" : "") + block2.join("\n") + "\n"
1496
1506
  );
@@ -1535,12 +1545,24 @@ var init_select = __esm({
1535
1545
  const s = entry.server;
1536
1546
  return s.transport === "http" ? s.url : [s.command, ...s.args ?? []].filter(Boolean).join(" ");
1537
1547
  };
1538
- selectFromCatalog = async (catalog, { interactive }) => {
1548
+ selectFromCatalog = async (catalog, {
1549
+ interactive,
1550
+ providers: providers2
1551
+ }) => {
1539
1552
  const allSkills = catalog.skills.map((s) => s.name);
1540
1553
  const allCommands = catalog.commands.map((c) => c.name);
1541
1554
  const allMcp = catalog.mcp.map((m) => m.name);
1555
+ const availablePlugins = catalog.plugins.filter(
1556
+ (plugin) => providers2.includes(plugin.provider)
1557
+ );
1558
+ const allPlugins = availablePlugins.map((p) => p.name);
1542
1559
  if (!interactive || !process.stdin.isTTY) {
1543
- return { skills: allSkills, commands: allCommands, mcp: allMcp };
1560
+ return {
1561
+ skills: allSkills,
1562
+ commands: allCommands,
1563
+ mcp: allMcp,
1564
+ plugins: allPlugins
1565
+ };
1544
1566
  }
1545
1567
  const groupNames = [...new Set(catalog.skills.map((s) => s.group))].sort(
1546
1568
  (a, b) => a === "general" ? -1 : b === "general" ? 1 : a.localeCompare(b)
@@ -1595,7 +1617,21 @@ var init_select = __esm({
1595
1617
  groups: mcpGroups,
1596
1618
  initialValues: []
1597
1619
  }) : [];
1598
- return { skills, commands, mcp };
1620
+ const plugins = availablePlugins.length ? await selectGrouped({
1621
+ message: "Select plugins (space toggles, a all, enter confirms)",
1622
+ groups: [
1623
+ {
1624
+ name: "opencode",
1625
+ items: availablePlugins.map((plugin) => ({
1626
+ value: plugin.name,
1627
+ label: plugin.name,
1628
+ hint: plugin.requires.length ? `requires: ${plugin.requires.join(", ")}` : void 0
1629
+ }))
1630
+ }
1631
+ ],
1632
+ initialValues: []
1633
+ }) : [];
1634
+ return { skills, commands, mcp, plugins };
1599
1635
  };
1600
1636
  }
1601
1637
  });
@@ -1637,11 +1673,12 @@ var init_init = __esm({
1637
1673
  );
1638
1674
  }
1639
1675
  const sourceCatalog = loadCatalog(source);
1640
- const selection = await selectFromCatalog(sourceCatalog, {
1641
- interactive: !options.all
1642
- });
1643
1676
  const providers2 = await resolveProviders(options);
1644
1677
  if (providers2 === null) return;
1678
+ const selection = await selectFromCatalog(sourceCatalog, {
1679
+ interactive: !options.all,
1680
+ providers: providers2
1681
+ });
1645
1682
  const resolved = materializeCatalog(
1646
1683
  options.targetRoot,
1647
1684
  source,
@@ -1669,9 +1706,14 @@ var init_init = __esm({
1669
1706
  lock.entries[entryId("mcp", mcp.name)] = mcpToEntry(mcp);
1670
1707
  }
1671
1708
  }
1709
+ for (const plugin of catalog.plugins) {
1710
+ if (selection.plugins.includes(plugin.name)) {
1711
+ lock.entries[entryId("plugin", plugin.name)] = pluginToEntry(plugin);
1712
+ }
1713
+ }
1672
1714
  writeLockfile(options.targetRoot, lock);
1673
1715
  await step(
1674
- `Wrote quiver.lock (${selection.skills.length} skills, ${selection.commands.length} commands, ${selection.mcp.length} MCP servers)`
1716
+ `Wrote quiver.lock (${selection.skills.length} skills, ${selection.commands.length} commands, ${selection.mcp.length} MCP servers, ${selection.plugins.length} plugins)`
1675
1717
  );
1676
1718
  const result = writeProviders(options.targetRoot, catalog, lock);
1677
1719
  if (result.generated.length || result.linked.length) {
@@ -1706,13 +1748,13 @@ var init_init = __esm({
1706
1748
 
1707
1749
  // src/catalog/repo.ts
1708
1750
  import { existsSync as existsSync11 } from "fs";
1709
- import { resolve as resolve15 } from "path";
1751
+ import { resolve as resolve14 } from "path";
1710
1752
  var repoCatalogRoot, repoCatalogExists, loadRepoCatalog;
1711
1753
  var init_repo = __esm({
1712
1754
  "src/catalog/repo.ts"() {
1713
1755
  "use strict";
1714
1756
  init_discover();
1715
- repoCatalogRoot = (targetRoot) => resolve15(targetRoot, ".agents");
1757
+ repoCatalogRoot = (targetRoot) => resolve14(targetRoot, ".agents");
1716
1758
  repoCatalogExists = (targetRoot) => existsSync11(repoCatalogRoot(targetRoot));
1717
1759
  loadRepoCatalog = (targetRoot, source) => {
1718
1760
  const resolved = {
@@ -1745,13 +1787,17 @@ var init_add = __esm({
1745
1787
  add = async (options) => {
1746
1788
  const id = options.positionals[0];
1747
1789
  if (!id) {
1748
- await error("Usage: quiver-cli add <skill:name|command:name|mcp:name>");
1790
+ await error(
1791
+ "Usage: quiver-cli add <skill:name|command:name|mcp:name|plugin:name>"
1792
+ );
1749
1793
  process.exitCode = 1;
1750
1794
  return;
1751
1795
  }
1752
1796
  const parsed = parseEntryId(id);
1753
1797
  if (!parsed) {
1754
- await error(`Invalid id "${id}". Expected skill:<name>, command:<name> or mcp:<name>.`);
1798
+ await error(
1799
+ `Invalid id "${id}". Expected skill:<name>, command:<name>, mcp:<name> or plugin:<name>.`
1800
+ );
1755
1801
  process.exitCode = 1;
1756
1802
  return;
1757
1803
  }
@@ -1772,16 +1818,25 @@ var init_add = __esm({
1772
1818
  const skill = sourceCatalog.skills.find((s) => s.name === parsed.name);
1773
1819
  const command = sourceCatalog.commands.find((c) => c.name === parsed.name);
1774
1820
  const mcp = sourceCatalog.mcp.find((m) => m.name === parsed.name);
1775
- const entry = parsed.type === "skill" && skill ? skillToEntry(skill) : parsed.type === "command" && command ? commandToEntry(command) : parsed.type === "mcp" && mcp ? mcpToEntry(mcp) : null;
1821
+ const plugin = sourceCatalog.plugins.find((p) => p.name === parsed.name);
1822
+ const entry = parsed.type === "skill" && skill ? skillToEntry(skill) : parsed.type === "command" && command ? commandToEntry(command) : parsed.type === "mcp" && mcp ? mcpToEntry(mcp) : parsed.type === "plugin" && plugin ? pluginToEntry(plugin) : null;
1776
1823
  if (!entry) {
1777
1824
  await error(`${id} not found in catalog.`);
1778
1825
  process.exitCode = 1;
1779
1826
  return;
1780
1827
  }
1828
+ if (entry.type === "plugin" && lock.providers?.length && !lock.providers.includes(entry.provider)) {
1829
+ await error(
1830
+ `${id} requires the ${entry.provider} provider. Enable it with \`quiver-cli providers ${entry.provider}\` first.`
1831
+ );
1832
+ process.exitCode = 1;
1833
+ return;
1834
+ }
1781
1835
  const selection = selectionFromLock(lock);
1782
1836
  if (parsed.type === "skill") selection.skills.push(parsed.name);
1783
1837
  if (parsed.type === "command") selection.commands.push(parsed.name);
1784
1838
  if (parsed.type === "mcp") selection.mcp.push(parsed.name);
1839
+ if (parsed.type === "plugin") selection.plugins.push(parsed.name);
1785
1840
  materializeCatalog(options.targetRoot, source, sourceCatalog, selection);
1786
1841
  lock.entries[id] = entry;
1787
1842
  writeLockfile(options.targetRoot, lock);
@@ -1790,13 +1845,19 @@ var init_add = __esm({
1790
1845
  await success(`Added ${id}.`);
1791
1846
  };
1792
1847
  selectionFromLock = (lock) => {
1793
- const selection = { skills: [], commands: [], mcp: [] };
1848
+ const selection = {
1849
+ skills: [],
1850
+ commands: [],
1851
+ mcp: [],
1852
+ plugins: []
1853
+ };
1794
1854
  for (const lid of Object.keys(lock.entries)) {
1795
1855
  const p = parseEntryId(lid);
1796
1856
  if (!p) continue;
1797
1857
  if (p.type === "skill") selection.skills.push(p.name);
1798
1858
  else if (p.type === "command") selection.commands.push(p.name);
1799
1859
  else if (p.type === "mcp") selection.mcp.push(p.name);
1860
+ else if (p.type === "plugin") selection.plugins.push(p.name);
1800
1861
  }
1801
1862
  return selection;
1802
1863
  };
@@ -1808,9 +1869,9 @@ var remove_exports = {};
1808
1869
  __export(remove_exports, {
1809
1870
  remove: () => remove
1810
1871
  });
1811
- import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
1812
- import { resolve as resolve16 } from "path";
1813
- var remove, cleanupEmptyGroups, rewriteRepoMcp;
1872
+ import { existsSync as existsSync12, readdirSync as readdirSync4, readFileSync as readFileSync7, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
1873
+ import { resolve as resolve15 } from "path";
1874
+ var remove, rewriteRepoPlugins, cleanupEmptyGroups, rewriteRepoMcp;
1814
1875
  var init_remove = __esm({
1815
1876
  "src/commands/remove.ts"() {
1816
1877
  "use strict";
@@ -1823,7 +1884,9 @@ var init_remove = __esm({
1823
1884
  remove = async (options) => {
1824
1885
  const id = options.positionals[0];
1825
1886
  if (!id) {
1826
- await error("Usage: quiver remove <skill:name|command:name|mcp:name>");
1887
+ await error(
1888
+ "Usage: quiver remove <skill:name|command:name|mcp:name|plugin:name>"
1889
+ );
1827
1890
  process.exitCode = 1;
1828
1891
  return;
1829
1892
  }
@@ -1844,31 +1907,46 @@ var init_remove = __esm({
1844
1907
  await info(`${id} is not installed.`);
1845
1908
  return;
1846
1909
  }
1847
- if (entry.type === "skill" || entry.type === "command") {
1910
+ if (entry.type === "skill" || entry.type === "command" || entry.type === "plugin") {
1848
1911
  removeArtifact(options.targetRoot, entry.sourcePath);
1849
1912
  cleanupEmptyGroups(options.targetRoot);
1850
1913
  }
1851
1914
  delete lock.entries[id];
1852
1915
  writeLockfile(options.targetRoot, lock);
1853
1916
  rewriteRepoMcp(options.targetRoot, lock);
1917
+ rewriteRepoPlugins(options.targetRoot, lock);
1854
1918
  const { catalog } = loadRepoCatalog(options.targetRoot, lock.catalog.source);
1855
1919
  writeProviders(options.targetRoot, catalog, lock);
1856
1920
  await success(`Removed ${id}.`);
1857
1921
  };
1922
+ rewriteRepoPlugins = (targetRoot, lock) => {
1923
+ const configPath = resolve15(targetRoot, ".agents/config.json");
1924
+ if (!existsSync12(configPath)) return;
1925
+ const config = JSON.parse(readFileSync7(configPath, "utf8"));
1926
+ if (!config.plugins) return;
1927
+ const keep = new Set(
1928
+ Object.keys(lock.entries).map(parseEntryId).filter((p) => p?.type === "plugin").map((p) => p.name)
1929
+ );
1930
+ config.plugins = Object.fromEntries(
1931
+ Object.entries(config.plugins).filter(([name]) => keep.has(name))
1932
+ );
1933
+ if (!Object.keys(config.plugins).length) delete config.plugins;
1934
+ writeFileSync5(configPath, JSON.stringify(config, null, 2) + "\n");
1935
+ };
1858
1936
  cleanupEmptyGroups = (targetRoot) => {
1859
- const skillsRoot = resolve16(targetRoot, ".agents/skills");
1937
+ const skillsRoot = resolve15(targetRoot, ".agents/skills");
1860
1938
  if (!existsSync12(skillsRoot)) return;
1861
1939
  for (const entry of readdirSync4(skillsRoot, { withFileTypes: true })) {
1862
1940
  if (!entry.isDirectory()) continue;
1863
- const dir = resolve16(skillsRoot, entry.name);
1864
- if (existsSync12(resolve16(dir, "SKILL.md"))) continue;
1865
- if (readdirSync4(dir).length === 0) rmSync5(dir, { recursive: true, force: true });
1941
+ const dir = resolve15(skillsRoot, entry.name);
1942
+ if (existsSync12(resolve15(dir, "SKILL.md"))) continue;
1943
+ if (readdirSync4(dir).length === 0) rmSync4(dir, { recursive: true, force: true });
1866
1944
  }
1867
1945
  };
1868
1946
  rewriteRepoMcp = (targetRoot, lock) => {
1869
- const configPath = resolve16(targetRoot, ".agents/config.json");
1947
+ const configPath = resolve15(targetRoot, ".agents/config.json");
1870
1948
  if (!existsSync12(configPath)) return;
1871
- const config = JSON.parse(readFileSync8(configPath, "utf8"));
1949
+ const config = JSON.parse(readFileSync7(configPath, "utf8"));
1872
1950
  if (!config.mcpServers) return;
1873
1951
  const keep = new Set(
1874
1952
  Object.keys(lock.entries).map(parseEntryId).filter((p) => p?.type === "mcp").map((p) => p.name)
@@ -1879,7 +1957,7 @@ var init_remove = __esm({
1879
1957
  }
1880
1958
  if (Object.keys(kept).length) config.mcpServers = kept;
1881
1959
  else delete config.mcpServers;
1882
- writeFileSync6(configPath, JSON.stringify(config, null, 2) + "\n");
1960
+ writeFileSync5(configPath, JSON.stringify(config, null, 2) + "\n");
1883
1961
  };
1884
1962
  }
1885
1963
  });
@@ -1894,6 +1972,7 @@ var init_locksync = __esm({
1894
1972
  const skillByName = new Map(catalog.skills.map((s) => [s.name, s]));
1895
1973
  const commandByName = new Map(catalog.commands.map((c) => [c.name, c]));
1896
1974
  const mcpByName = new Map(catalog.mcp.map((m) => [m.name, m]));
1975
+ const pluginByName = new Map(catalog.plugins.map((p) => [p.name, p]));
1897
1976
  for (const [id, entry] of Object.entries(lock.entries)) {
1898
1977
  if (entry.type === "skill") {
1899
1978
  const cat = skillByName.get(id.slice("skill:".length));
@@ -1918,6 +1997,15 @@ var init_locksync = __esm({
1918
1997
  entry.configDigest = cat.configDigest;
1919
1998
  entry.transport = cat.server.transport;
1920
1999
  }
2000
+ } else if (entry.type === "plugin") {
2001
+ const cat = pluginByName.get(id.slice("plugin:".length));
2002
+ if (!cat) continue;
2003
+ if (cat.digest !== entry.digest) {
2004
+ drift.push(`${id}: plugin content changed`);
2005
+ entry.digest = cat.digest;
2006
+ entry.sourcePath = cat.sourcePath;
2007
+ entry.requires = cat.requires;
2008
+ }
1921
2009
  }
1922
2010
  }
1923
2011
  return drift;
@@ -1975,11 +2063,12 @@ var init_sync = __esm({
1975
2063
  const haveSkills = new Set(catalog.skills.map((s) => s.name));
1976
2064
  const haveCommands = new Set(catalog.commands.map((c) => c.name));
1977
2065
  const haveMcp = new Set(catalog.mcp.map((m) => m.name));
2066
+ const havePlugins = new Set(catalog.plugins.map((p) => p.name));
1978
2067
  const orphans = [];
1979
2068
  for (const id of Object.keys(lock.entries)) {
1980
2069
  const p = parseEntryId(id);
1981
2070
  if (!p) continue;
1982
- const present = p.type === "skill" && haveSkills.has(p.name) || p.type === "command" && haveCommands.has(p.name) || p.type === "mcp" && haveMcp.has(p.name);
2071
+ const present = p.type === "skill" && haveSkills.has(p.name) || p.type === "command" && haveCommands.has(p.name) || p.type === "mcp" && haveMcp.has(p.name) || p.type === "plugin" && havePlugins.has(p.name);
1983
2072
  if (!present) orphans.push(id);
1984
2073
  }
1985
2074
  if (orphans.length) {
@@ -2085,8 +2174,8 @@ var update_exports = {};
2085
2174
  __export(update_exports, {
2086
2175
  update: () => update
2087
2176
  });
2088
- import { cpSync as cpSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync9, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
2089
- import { dirname as dirname5, resolve as resolve17 } from "path";
2177
+ import { cpSync as cpSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
2178
+ import { dirname as dirname5, resolve as resolve16 } from "path";
2090
2179
  var update, applyUpdate, report;
2091
2180
  var init_update = __esm({
2092
2181
  "src/commands/update.ts"() {
@@ -2142,21 +2231,27 @@ var init_update = __esm({
2142
2231
  sourceCatalog,
2143
2232
  repoCatalog,
2144
2233
  lock,
2145
- options.force
2234
+ options.force,
2235
+ options.dryRun
2146
2236
  );
2147
2237
  if (report4.status === "updated") changed = true;
2148
2238
  reports.push(report4);
2149
2239
  }
2150
- if (changed || catalogMoved) {
2151
- writeLockfile(options.targetRoot, lock);
2152
- }
2153
- if (changed) {
2154
- const { catalog } = loadRepoCatalog(options.targetRoot, lock.catalog.source);
2155
- writeProviders(options.targetRoot, catalog, lock);
2240
+ if (!options.dryRun) {
2241
+ if (changed || catalogMoved) {
2242
+ writeLockfile(options.targetRoot, lock);
2243
+ }
2244
+ if (changed) {
2245
+ const { catalog } = loadRepoCatalog(
2246
+ options.targetRoot,
2247
+ lock.catalog.source
2248
+ );
2249
+ writeProviders(options.targetRoot, catalog, lock);
2250
+ }
2156
2251
  }
2157
2252
  report(reports, options);
2158
2253
  };
2159
- applyUpdate = (targetRoot, parsed, entry, sourceCatalog, repoCatalog, lock, force) => {
2254
+ applyUpdate = (targetRoot, parsed, entry, sourceCatalog, repoCatalog, lock, force, dryRun) => {
2160
2255
  const id = `${parsed.type}:${parsed.name}`;
2161
2256
  if (entry.type === "skill") {
2162
2257
  const src2 = sourceCatalog.skills.find((s) => s.name === parsed.name);
@@ -2166,9 +2261,10 @@ var init_update = __esm({
2166
2261
  if (repo2 && repo2.digest !== entry.digest && !force) {
2167
2262
  return { id, status: "local-changes" };
2168
2263
  }
2169
- const dest = resolve17(targetRoot, ".agents", src2.sourcePath);
2170
- rmSync6(dest, { recursive: true, force: true });
2171
- mkdirSync6(dirname5(dest), { recursive: true });
2264
+ if (dryRun) return { id, status: "updated" };
2265
+ const dest = resolve16(targetRoot, ".agents", src2.sourcePath);
2266
+ rmSync5(dest, { recursive: true, force: true });
2267
+ mkdirSync5(dirname5(dest), { recursive: true });
2172
2268
  cpSync2(src2.absDir, dest, { recursive: true });
2173
2269
  entry.digest = src2.digest;
2174
2270
  entry.frontmatter = src2.frontmatter;
@@ -2183,13 +2279,38 @@ var init_update = __esm({
2183
2279
  if (repo2 && repo2.digest !== entry.digest && !force) {
2184
2280
  return { id, status: "local-changes" };
2185
2281
  }
2186
- const dest = resolve17(targetRoot, ".agents", src2.sourcePath);
2187
- mkdirSync6(dirname5(dest), { recursive: true });
2282
+ if (dryRun) return { id, status: "updated" };
2283
+ const dest = resolve16(targetRoot, ".agents", src2.sourcePath);
2284
+ mkdirSync5(dirname5(dest), { recursive: true });
2188
2285
  cpSync2(src2.absPath, dest, { force: true });
2189
2286
  entry.digest = src2.digest;
2190
2287
  entry.sourcePath = src2.sourcePath;
2191
2288
  return { id, status: "updated" };
2192
2289
  }
2290
+ if (entry.type === "plugin") {
2291
+ const src2 = sourceCatalog.plugins.find((p) => p.name === parsed.name);
2292
+ if (!src2) return { id, status: "not-in-catalog" };
2293
+ if (src2.digest === entry.digest) return { id, status: "up-to-date" };
2294
+ const repo2 = repoCatalog.plugins.find((p) => p.name === parsed.name);
2295
+ if (repo2 && repo2.digest !== entry.digest && !force) {
2296
+ return { id, status: "local-changes" };
2297
+ }
2298
+ if (dryRun) return { id, status: "updated" };
2299
+ const dest = resolve16(targetRoot, ".agents", src2.sourcePath);
2300
+ mkdirSync5(dirname5(dest), { recursive: true });
2301
+ cpSync2(src2.absPath, dest, { force: true });
2302
+ const configPath2 = resolve16(targetRoot, ".agents/config.json");
2303
+ const config2 = JSON.parse(readFileSync8(configPath2, "utf8"));
2304
+ config2.plugins = {
2305
+ ...config2.plugins,
2306
+ [parsed.name]: sourceCatalog.config.plugins?.[parsed.name]
2307
+ };
2308
+ writeFileSync6(configPath2, JSON.stringify(config2, null, 2) + "\n");
2309
+ entry.digest = src2.digest;
2310
+ entry.sourcePath = src2.sourcePath;
2311
+ entry.requires = src2.requires;
2312
+ return { id, status: "updated" };
2313
+ }
2193
2314
  const src = sourceCatalog.mcp.find((m) => m.name === parsed.name);
2194
2315
  if (!src) return { id, status: "not-in-catalog" };
2195
2316
  if (src.configDigest === entry.configDigest) return { id, status: "up-to-date" };
@@ -2197,10 +2318,11 @@ var init_update = __esm({
2197
2318
  if (repo && repo.configDigest !== entry.configDigest && !force) {
2198
2319
  return { id, status: "local-changes" };
2199
2320
  }
2200
- const configPath = resolve17(targetRoot, ".agents", "config.json");
2201
- const config = JSON.parse(readFileSync9(configPath, "utf8"));
2321
+ if (dryRun) return { id, status: "updated" };
2322
+ const configPath = resolve16(targetRoot, ".agents", "config.json");
2323
+ const config = JSON.parse(readFileSync8(configPath, "utf8"));
2202
2324
  config.mcpServers = { ...config.mcpServers, [parsed.name]: src.server };
2203
- writeFileSync7(configPath, JSON.stringify(config, null, 2) + "\n");
2325
+ writeFileSync6(configPath, JSON.stringify(config, null, 2) + "\n");
2204
2326
  entry.configDigest = src.configDigest;
2205
2327
  entry.transport = src.server.transport;
2206
2328
  entry.tools = null;
@@ -2214,6 +2336,7 @@ var init_update = __esm({
2214
2336
  JSON.stringify(
2215
2337
  {
2216
2338
  ok: true,
2339
+ dryRun: options.dryRun,
2217
2340
  updated: by("updated").map((r) => r.id),
2218
2341
  upToDate: by("up-to-date").map((r) => r.id),
2219
2342
  localChanges: by("local-changes").map((r) => r.id),
@@ -2227,7 +2350,8 @@ var init_update = __esm({
2227
2350
  }
2228
2351
  const c = palette();
2229
2352
  const lines = [""];
2230
- for (const r of by("updated")) lines.push(` ${c.cyan("\u2191")} ${r.id} updated`);
2353
+ const verb = options.dryRun ? "would update" : "updated";
2354
+ for (const r of by("updated")) lines.push(` ${c.cyan("\u2191")} ${r.id} ${verb}`);
2231
2355
  for (const r of by("local-changes"))
2232
2356
  lines.push(
2233
2357
  ` ${c.yellow("\u25B2")} ${r.id} ${c.yellow("local changes - skipped (use --force)")}`
@@ -2239,7 +2363,11 @@ var init_update = __esm({
2239
2363
  const updated = by("updated").length;
2240
2364
  lines.push(
2241
2365
  "",
2242
- ` ${updated ? c.cyan(`\u2191 ${updated} updated - review and commit .agents/ + quiver.lock`) : c.green("\u2713 everything up to date with the catalog")}`,
2366
+ ` ${updated ? options.dryRun ? c.cyan(
2367
+ `\u2191 ${updated} would be updated - run without --dry-run to apply`
2368
+ ) : c.cyan(
2369
+ `\u2191 ${updated} updated - review and commit .agents/ + quiver.lock`
2370
+ ) : c.green("\u2713 everything up to date with the catalog")}`,
2243
2371
  ""
2244
2372
  );
2245
2373
  block(lines);
@@ -2287,14 +2415,16 @@ var init_list = __esm({
2287
2415
  const skills = [];
2288
2416
  const commands = [];
2289
2417
  const mcp = [];
2418
+ const plugins = [];
2290
2419
  for (const [id, entry] of Object.entries(lock.entries)) {
2291
2420
  const p = parseEntryId(id);
2292
2421
  if (!p) continue;
2293
2422
  if (entry.type === "skill") skills.push({ name: p.name, entry });
2294
2423
  else if (entry.type === "command") commands.push({ name: p.name, entry });
2295
2424
  else if (entry.type === "mcp") mcp.push({ name: p.name, entry });
2425
+ else if (entry.type === "plugin") plugins.push({ name: p.name, entry });
2296
2426
  }
2297
- for (const group of [skills, commands, mcp]) {
2427
+ for (const group of [skills, commands, mcp, plugins]) {
2298
2428
  group.sort((a, b) => a.name.localeCompare(b.name));
2299
2429
  }
2300
2430
  if (options.json) {
@@ -2313,6 +2443,11 @@ var init_list = __esm({
2313
2443
  transport: entry.transport,
2314
2444
  detail: serverDetail2.get(name) ?? null,
2315
2445
  toolCount: entry.tools ? Object.keys(entry.tools).length : null
2446
+ })),
2447
+ plugins: plugins.map(({ name, entry }) => ({
2448
+ name,
2449
+ provider: entry.provider,
2450
+ requires: entry.requires
2316
2451
  }))
2317
2452
  },
2318
2453
  null,
@@ -2370,11 +2505,18 @@ var init_list = __esm({
2370
2505
  );
2371
2506
  }
2372
2507
  }
2508
+ if (plugins.length) {
2509
+ lines.push("", ` ${c.bold("plugins")}`);
2510
+ for (const { name, entry } of plugins) {
2511
+ const requires = entry.requires.length ? ` ${c.dim(`requires: ${entry.requires.join(", ")}`)}` : "";
2512
+ lines.push(` ${name} ${c.dim(entry.provider)}${requires}`);
2513
+ }
2514
+ }
2373
2515
  const providers2 = lock.providers?.length ? lock.providers.join(", ") : "claude, opencode, codex";
2374
2516
  lines.push(
2375
2517
  "",
2376
2518
  ` ${c.bold(
2377
- `${skills.length} skills \xB7 ${commands.length} commands \xB7 ${mcp.length} MCP servers`
2519
+ `${skills.length} skills \xB7 ${commands.length} commands \xB7 ${mcp.length} MCP servers \xB7 ${plugins.length} plugins`
2378
2520
  )} ${c.dim(`providers: ${providers2}`)}`
2379
2521
  );
2380
2522
  if (missingTools) {
@@ -2386,84 +2528,6 @@ var init_list = __esm({
2386
2528
  }
2387
2529
  });
2388
2530
 
2389
- // src/commands/status.ts
2390
- var status_exports = {};
2391
- __export(status_exports, {
2392
- status: () => status
2393
- });
2394
- var status;
2395
- var init_status = __esm({
2396
- "src/commands/status.ts"() {
2397
- "use strict";
2398
- init_repo();
2399
- init_io();
2400
- init_schema();
2401
- init_write();
2402
- init_prompts();
2403
- status = async (options) => {
2404
- const lock = readLockfile(options.targetRoot);
2405
- if (!lock) {
2406
- if (options.json) console.log(JSON.stringify({ ok: false, error: "no-lockfile" }));
2407
- else await error("No quiver.lock found. Run `quiver-cli init` first.");
2408
- process.exitCode = 1;
2409
- return;
2410
- }
2411
- if (!repoCatalogExists(options.targetRoot)) {
2412
- if (options.json) console.log(JSON.stringify({ ok: false, error: "no-agents" }));
2413
- else await error("No .agents/ directory found. Run `quiver-cli init` first.");
2414
- process.exitCode = 1;
2415
- return;
2416
- }
2417
- const { catalog } = loadRepoCatalog(options.targetRoot, lock.catalog.source);
2418
- const haveSkills = new Set(catalog.skills.map((s) => s.name));
2419
- const haveCommands = new Set(catalog.commands.map((c) => c.name));
2420
- const haveMcp = new Set(catalog.mcp.map((m) => m.name));
2421
- const missing = [];
2422
- const changed = [];
2423
- const skillByName = new Map(catalog.skills.map((s) => [s.name, s]));
2424
- const commandByName = new Map(catalog.commands.map((c) => [c.name, c]));
2425
- const mcpByName = new Map(catalog.mcp.map((m) => [m.name, m]));
2426
- for (const [id, entry] of Object.entries(lock.entries)) {
2427
- const p = parseEntryId(id);
2428
- if (!p) continue;
2429
- if (p.type === "skill") {
2430
- if (!haveSkills.has(p.name)) missing.push(id);
2431
- else if (entry.type === "skill" && skillByName.get(p.name).digest !== entry.digest)
2432
- changed.push(id);
2433
- } else if (p.type === "command") {
2434
- if (!haveCommands.has(p.name)) missing.push(id);
2435
- else if (entry.type === "command" && commandByName.get(p.name).digest !== entry.digest)
2436
- changed.push(id);
2437
- } else if (p.type === "mcp") {
2438
- if (!haveMcp.has(p.name)) missing.push(id);
2439
- else if (entry.type === "mcp" && mcpByName.get(p.name).configDigest !== entry.configDigest)
2440
- changed.push(id);
2441
- }
2442
- }
2443
- const shimProblems = checkProviders(options.targetRoot, catalog, lock);
2444
- const ok = missing.length === 0 && changed.length === 0 && shimProblems.length === 0;
2445
- if (options.json) {
2446
- console.log(JSON.stringify({ ok, missing, changed, shims: shimProblems }, null, 2));
2447
- if (!ok) process.exitCode = 1;
2448
- return;
2449
- }
2450
- if (ok) {
2451
- await success("In sync: lockfile, .agents/ and provider shims agree.");
2452
- return;
2453
- }
2454
- if (missing.length)
2455
- await warn(`Missing in .agents/: ${missing.join(", ")}`);
2456
- if (changed.length)
2457
- await warn(`Content changed since lockfile: ${changed.join(", ")}`);
2458
- if (shimProblems.length)
2459
- await warn(`Provider shims out of date:
2460
- - ${shimProblems.join("\n - ")}`);
2461
- await info("Run `quiver-cli sync` to regenerate provider shims and refresh digests.");
2462
- process.exitCode = 1;
2463
- };
2464
- }
2465
- });
2466
-
2467
2531
  // src/mcp/diff.ts
2468
2532
  var diffSnapshots, isEmptyDiff;
2469
2533
  var init_diff = __esm({
@@ -2598,9 +2662,12 @@ var init_snapshot = __esm({
2598
2662
  var check_exports = {};
2599
2663
  __export(check_exports, {
2600
2664
  check: () => check,
2665
+ hasCommand: () => hasCommand,
2601
2666
  summarize: () => summarize
2602
2667
  });
2603
- var check, report2, driftLines, list2, recommend, summarize, truncate2, fail;
2668
+ import { accessSync as accessSync2, constants as constants2 } from "fs";
2669
+ import { delimiter, resolve as resolve17 } from "path";
2670
+ var check, report2, driftLines, list2, recommend, summarize, hasCommand, truncate2, fail;
2604
2671
  var init_check = __esm({
2605
2672
  "src/commands/check.ts"() {
2606
2673
  "use strict";
@@ -2610,6 +2677,7 @@ var init_check = __esm({
2610
2677
  init_diff();
2611
2678
  init_introspect();
2612
2679
  init_snapshot();
2680
+ init_write();
2613
2681
  init_interpolate();
2614
2682
  init_prompts();
2615
2683
  check = async (options) => {
@@ -2624,8 +2692,10 @@ var init_check = __esm({
2624
2692
  const { catalog } = loadRepoCatalog(options.targetRoot, lock.catalog.source);
2625
2693
  const skillByName = new Map(catalog.skills.map((s) => [s.name, s]));
2626
2694
  const commandByName = new Map(catalog.commands.map((c) => [c.name, c]));
2695
+ const pluginByName = new Map(catalog.plugins.map((p) => [p.name, p]));
2627
2696
  const skillDrift = [];
2628
- const checked = { skills: 0, commands: 0, mcp: 0 };
2697
+ const pluginRequirements = [];
2698
+ const checked = { skills: 0, commands: 0, mcp: 0, plugins: 0 };
2629
2699
  for (const [id, entry] of Object.entries(lock.entries)) {
2630
2700
  const p = parseEntryId(id);
2631
2701
  if (!p) continue;
@@ -2639,11 +2709,24 @@ var init_check = __esm({
2639
2709
  if (!cat) continue;
2640
2710
  checked.commands += 1;
2641
2711
  if (cat.digest !== entry.digest) skillDrift.push({ id, kind: "content" });
2712
+ } else if (entry.type === "plugin") {
2713
+ const cat = pluginByName.get(p.name);
2714
+ if (!cat) continue;
2715
+ checked.plugins += 1;
2716
+ if (cat.digest !== entry.digest) skillDrift.push({ id, kind: "content" });
2717
+ if (lock.providers?.length && !lock.providers.includes(entry.provider)) {
2718
+ continue;
2719
+ }
2720
+ for (const command of entry.requires) {
2721
+ if (!hasCommand(command)) pluginRequirements.push({ id, command });
2722
+ }
2642
2723
  }
2643
2724
  }
2725
+ const shimProblems = checkProviders(options.targetRoot, catalog, lock);
2644
2726
  const mcpReports = [];
2645
2727
  let lockChanged = false;
2646
2728
  for (const [id, entry] of Object.entries(lock.entries)) {
2729
+ if (options.offline) break;
2647
2730
  if (entry.type !== "mcp") continue;
2648
2731
  const p = parseEntryId(id);
2649
2732
  const catMcp = catalog.mcp.find((m) => m.name === p.name);
@@ -2677,11 +2760,18 @@ var init_check = __esm({
2677
2760
  }
2678
2761
  }
2679
2762
  if (lockChanged) writeLockfile(options.targetRoot, lock);
2680
- const hasDrift = skillDrift.length > 0 || mcpReports.some((r) => r.status === "drift");
2763
+ const hasDrift = skillDrift.length > 0 || pluginRequirements.length > 0 || shimProblems.length > 0 || mcpReports.some((r) => r.status === "drift");
2681
2764
  if (options.json) {
2682
2765
  console.log(
2683
2766
  JSON.stringify(
2684
- { ok: !hasDrift, checked, skillDrift, mcp: mcpReports },
2767
+ {
2768
+ ok: !hasDrift,
2769
+ checked,
2770
+ skillDrift,
2771
+ pluginRequirements,
2772
+ shims: shimProblems,
2773
+ mcp: mcpReports
2774
+ },
2685
2775
  null,
2686
2776
  2
2687
2777
  )
@@ -2689,16 +2779,35 @@ var init_check = __esm({
2689
2779
  if (hasDrift) process.exitCode = 1;
2690
2780
  return;
2691
2781
  }
2692
- await report2(skillDrift, mcpReports, checked, options);
2782
+ await report2(
2783
+ skillDrift,
2784
+ pluginRequirements,
2785
+ shimProblems,
2786
+ mcpReports,
2787
+ checked,
2788
+ options
2789
+ );
2693
2790
  if (hasDrift) process.exitCode = 1;
2694
2791
  };
2695
- report2 = async (skillDrift, mcpReports, checked, options) => {
2792
+ report2 = async (skillDrift, pluginRequirements, shimProblems, mcpReports, checked, options) => {
2696
2793
  if (skillDrift.length) {
2697
2794
  await warn(
2698
- `Skill/command content changed since lockfile:
2795
+ `Managed content changed since lockfile:
2699
2796
  - ${skillDrift.map((s) => s.id).join("\n - ")}`
2700
2797
  );
2701
2798
  }
2799
+ if (pluginRequirements.length) {
2800
+ await warn(
2801
+ `Plugin requirements missing:
2802
+ - ${pluginRequirements.map((issue) => `${issue.id}: ${issue.command}`).join("\n - ")}`
2803
+ );
2804
+ }
2805
+ if (shimProblems.length) {
2806
+ await warn(
2807
+ `Provider shims out of date:
2808
+ - ${shimProblems.join("\n - ")}`
2809
+ );
2810
+ }
2702
2811
  const skipped = mcpReports.filter((r) => r.status === "skipped");
2703
2812
  if (skipped.length) {
2704
2813
  const names = skipped.map((r) => parseEntryId(r.id)?.name ?? r.id);
@@ -2723,12 +2832,13 @@ var init_check = __esm({
2723
2832
  - ${driftLines(r.diff, options.verbose).join("\n - ")}`);
2724
2833
  }
2725
2834
  const summary = summarize(checked);
2726
- const hasDrift = skillDrift.length > 0 || drifted.length > 0;
2727
- if (!hasDrift) {
2835
+ const hasDrift = skillDrift.length > 0 || shimProblems.length > 0 || drifted.length > 0;
2836
+ const hasProblems = hasDrift || pluginRequirements.length > 0;
2837
+ if (!hasProblems) {
2728
2838
  await success(`check passed: ${summary}, no drift detected.`);
2729
2839
  } else {
2730
2840
  await info(`checked ${summary}, drift detected.`);
2731
- await recommend(skillDrift, drifted);
2841
+ await recommend(skillDrift, shimProblems, drifted);
2732
2842
  }
2733
2843
  };
2734
2844
  driftLines = (diff, verbose) => {
@@ -2762,9 +2872,12 @@ var init_check = __esm({
2762
2872
  const rest = names.length - sample;
2763
2873
  return `${names.length} tools (${names.slice(0, sample).join(", ")}, \u2026 +${rest} more)`;
2764
2874
  };
2765
- recommend = async (skillDrift, drifted) => {
2875
+ recommend = async (skillDrift, shimProblems, drifted) => {
2766
2876
  const c = palette();
2767
2877
  const lines = ["to update the lockfile baseline:"];
2878
+ if (shimProblems.length) {
2879
+ lines.push(` ${c.cyan("quiver-cli sync")} regenerate provider shims`);
2880
+ }
2768
2881
  if (drifted.length) {
2769
2882
  lines.push(` ${c.cyan("quiver-cli check --accept")} accept the new MCP tool snapshots`);
2770
2883
  }
@@ -2786,8 +2899,23 @@ var init_check = __esm({
2786
2899
  if (c.skills) parts.push(plural(c.skills, "skill"));
2787
2900
  if (c.commands) parts.push(plural(c.commands, "command"));
2788
2901
  if (c.mcp) parts.push(plural(c.mcp, "MCP server"));
2902
+ if (c.plugins) parts.push(plural(c.plugins, "plugin"));
2789
2903
  return parts.length ? parts.join(", ") : "nothing";
2790
2904
  };
2905
+ hasCommand = (command) => {
2906
+ if (!/^[A-Za-z0-9._-]+$/.test(command)) return false;
2907
+ const extensions = process.platform === "win32" ? (process.env["PATHEXT"] ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
2908
+ for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
2909
+ for (const extension of extensions) {
2910
+ try {
2911
+ accessSync2(resolve17(dir, command + extension), constants2.X_OK);
2912
+ return true;
2913
+ } catch {
2914
+ }
2915
+ }
2916
+ }
2917
+ return false;
2918
+ };
2791
2919
  truncate2 = (s, max = 120) => s.length > max ? s.slice(0, max) + "\u2026" : s;
2792
2920
  fail = async (options, code, message) => {
2793
2921
  if (options.json) console.log(JSON.stringify({ ok: false, error: code }));
@@ -2802,9 +2930,9 @@ import { execFileSync as execFileSync3 } from "child_process";
2802
2930
  import {
2803
2931
  existsSync as existsSync13,
2804
2932
  mkdtempSync as mkdtempSync2,
2805
- readFileSync as readFileSync10,
2806
- rmSync as rmSync7,
2807
- writeFileSync as writeFileSync8
2933
+ readFileSync as readFileSync9,
2934
+ rmSync as rmSync6,
2935
+ writeFileSync as writeFileSync7
2808
2936
  } from "fs";
2809
2937
  import { tmpdir } from "os";
2810
2938
  import { join, resolve as resolve18 } from "path";
@@ -2819,10 +2947,10 @@ var init_upstreams = __esm({
2819
2947
  loadUpstreams = (catalog) => {
2820
2948
  const path = upstreamsPath(catalog);
2821
2949
  if (!existsSync13(path)) return {};
2822
- return JSON.parse(readFileSync10(path, "utf8"));
2950
+ return JSON.parse(readFileSync9(path, "utf8"));
2823
2951
  };
2824
2952
  writeUpstreams = (catalog, map) => {
2825
- writeFileSync8(upstreamsPath(catalog), JSON.stringify(map, null, 2) + "\n");
2953
+ writeFileSync7(upstreamsPath(catalog), JSON.stringify(map, null, 2) + "\n");
2826
2954
  };
2827
2955
  fetchLatestCommit = async (origin) => {
2828
2956
  const url = `https://api.github.com/repos/${origin.repo}/commits?path=${encodeURIComponent(origin.path)}&sha=${encodeURIComponent(origin.ref)}&per_page=1`;
@@ -2861,7 +2989,7 @@ var init_upstreams = __esm({
2861
2989
  };
2862
2990
  fetchUpstreamDir = (origin) => {
2863
2991
  const tmp = mkdtempSync2(join(tmpdir(), "quiver-pull-"));
2864
- const cleanup = () => rmSync7(tmp, { recursive: true, force: true });
2992
+ const cleanup = () => rmSync6(tmp, { recursive: true, force: true });
2865
2993
  const git = (args, cwd) => {
2866
2994
  execFileSync3("git", args, {
2867
2995
  cwd,
@@ -2936,7 +3064,7 @@ var upstream_exports = {};
2936
3064
  __export(upstream_exports, {
2937
3065
  upstream: () => upstream
2938
3066
  });
2939
- import { cpSync as cpSync3, rmSync as rmSync8 } from "fs";
3067
+ import { cpSync as cpSync3, rmSync as rmSync7 } from "fs";
2940
3068
  var upstream, guardWritableCatalog, STATUS_ORDER, pull, report3, countByStatus;
2941
3069
  var init_upstream = __esm({
2942
3070
  "src/commands/upstream.ts"() {
@@ -2988,7 +3116,7 @@ var init_upstream = __esm({
2988
3116
  guardWritableCatalog = async (catalog) => {
2989
3117
  if (isCatalogWritable(catalog)) return false;
2990
3118
  await error(
2991
- "upstream is a catalog-maintenance command and the catalog here is not writable (the installed package or a remote cache).\nRun it inside the quiver-cli repo, or point at a writable local catalog with --catalog <path>.\nTo update a consuming repo's installed entries, use quiver-cli check / quiver-cli update instead."
3119
+ "upstream is a catalog-maintenance command and the catalog here is not writable (the installed package or a remote cache).\nRun it inside the quiver-cli repo, or point at a writable local catalog with --catalog=local:/absolute/path/.agents.\nTo update a consuming repo's installed entries, use quiver-cli check / quiver-cli update instead."
2992
3120
  );
2993
3121
  process.exitCode = 1;
2994
3122
  return true;
@@ -3046,7 +3174,7 @@ var init_upstream = __esm({
3046
3174
  continue;
3047
3175
  }
3048
3176
  try {
3049
- rmSync8(skill.absDir, { recursive: true, force: true });
3177
+ rmSync7(skill.absDir, { recursive: true, force: true });
3050
3178
  cpSync3(fetched.dir, skill.absDir, { recursive: true, dereference: true });
3051
3179
  } finally {
3052
3180
  fetched.cleanup();
@@ -3120,14 +3248,23 @@ var init_upstream = __esm({
3120
3248
  lines.push(` ${mark[r.status]} ${c.bold(name)} ${detail} ${where}`);
3121
3249
  }
3122
3250
  block(lines);
3123
- const drifted = sorted.filter(
3124
- (r) => r.status === "drift" || r.status === "drift-curated"
3125
- );
3126
- if (drifted.length) {
3251
+ const drifted = sorted.filter((r) => r.status === "drift");
3252
+ const curated = sorted.filter((r) => r.status === "drift-curated");
3253
+ if (drifted.length || curated.length) {
3127
3254
  const hint = [""];
3128
- for (const r of drifted) {
3129
- const note = r.status === "drift-curated" ? "curated \u2014 reconcile changes by hand" : "re-fetch with the skills CLI, then copy into the catalog";
3130
- hint.push(` ${c.yellow(r.name)}: ${c.dim(note)}`);
3255
+ if (drifted.length) {
3256
+ hint.push(
3257
+ ` ${c.cyan("quiver-cli upstream pull")} ${c.dim(
3258
+ `update the catalog copy (${drifted.map((r) => r.name).join(", ")})`
3259
+ )}`
3260
+ );
3261
+ }
3262
+ for (const r of curated) {
3263
+ hint.push(
3264
+ ` ${c.yellow(r.name)}: ${c.dim(
3265
+ "curated \u2014 reconcile by hand, or upstream pull --force"
3266
+ )}`
3267
+ );
3131
3268
  }
3132
3269
  block(hint);
3133
3270
  }
@@ -3163,83 +3300,6 @@ var init_upstream = __esm({
3163
3300
  }
3164
3301
  });
3165
3302
 
3166
- // src/commands/login.ts
3167
- var login_exports = {};
3168
- __export(login_exports, {
3169
- login: () => login
3170
- });
3171
- import process3 from "process";
3172
- var login, promptToken, readStdin;
3173
- var init_login = __esm({
3174
- "src/commands/login.ts"() {
3175
- "use strict";
3176
- init_auth();
3177
- init_prompts();
3178
- login = async (options) => {
3179
- const token = process3.stdin.isTTY ? await promptToken() : await readStdin();
3180
- if (!token) {
3181
- await error(
3182
- "No token provided. Create a GitHub personal access token with `repo` read access and try again."
3183
- );
3184
- process3.exitCode = 1;
3185
- return;
3186
- }
3187
- const validation = await validateToken(token);
3188
- if (!validation.ok) {
3189
- await error(`Token validation failed: ${validation.reason}`);
3190
- process3.exitCode = 1;
3191
- return;
3192
- }
3193
- writeStoredToken(token, validation.login);
3194
- if (options.json) {
3195
- console.log(JSON.stringify({ ok: true, login: validation.login }, null, 2));
3196
- return;
3197
- }
3198
- await success(
3199
- `Logged in as ${validation.login}. Token stored in ${authFilePath()}.`
3200
- );
3201
- };
3202
- promptToken = async () => {
3203
- await info(
3204
- "Paste a GitHub personal access token (used for github: catalogs; needs read access to the catalog repo)."
3205
- );
3206
- const value = await password("GitHub token");
3207
- return value?.trim() || null;
3208
- };
3209
- readStdin = async () => {
3210
- let data = "";
3211
- for await (const chunk of process3.stdin) data += chunk;
3212
- return data.trim() || null;
3213
- };
3214
- }
3215
- });
3216
-
3217
- // src/commands/logout.ts
3218
- var logout_exports = {};
3219
- __export(logout_exports, {
3220
- logout: () => logout
3221
- });
3222
- var logout;
3223
- var init_logout = __esm({
3224
- "src/commands/logout.ts"() {
3225
- "use strict";
3226
- init_auth();
3227
- init_prompts();
3228
- logout = async (options) => {
3229
- const removed = deleteStoredToken();
3230
- if (options.json) {
3231
- console.log(JSON.stringify({ ok: true, removed }, null, 2));
3232
- return;
3233
- }
3234
- if (removed) {
3235
- await success(`Logged out. Removed ${authFilePath()}.`);
3236
- } else {
3237
- await info("Not logged in - nothing to remove.");
3238
- }
3239
- };
3240
- }
3241
- });
3242
-
3243
3303
  // src/version/notifier.ts
3244
3304
  var notifier_exports = {};
3245
3305
  __export(notifier_exports, {
@@ -3249,8 +3309,8 @@ __export(notifier_exports, {
3249
3309
  installHint: () => installHint,
3250
3310
  notifierSuppressed: () => notifierSuppressed
3251
3311
  });
3252
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync9 } from "fs";
3253
- import { homedir as homedir3 } from "os";
3312
+ import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync10, writeFileSync as writeFileSync8 } from "fs";
3313
+ import { homedir as homedir2 } from "os";
3254
3314
  import { dirname as dirname6, resolve as resolve19 } from "path";
3255
3315
  var REGISTRY_URL, CHECK_TTL_MS, FETCH_TIMEOUT_MS, INSTALL_HINT, cacheFilePath, installHint, getCurrentVersion, compareSemver, readCache, writeCache, fetchLatestVersion, checkForUpdate, notifierSuppressed;
3256
3316
  var init_notifier = __esm({
@@ -3262,14 +3322,14 @@ var init_notifier = __esm({
3262
3322
  FETCH_TIMEOUT_MS = 2e3;
3263
3323
  INSTALL_HINT = "pnpm add -g quiver-cli";
3264
3324
  cacheFilePath = () => {
3265
- const base = process.env["XDG_CACHE_HOME"] || resolve19(homedir3(), ".cache");
3325
+ const base = process.env["XDG_CACHE_HOME"] || resolve19(homedir2(), ".cache");
3266
3326
  return resolve19(base, "quiver", "update-check.json");
3267
3327
  };
3268
3328
  installHint = () => INSTALL_HINT;
3269
3329
  getCurrentVersion = () => {
3270
3330
  try {
3271
3331
  const pkg = JSON.parse(
3272
- readFileSync11(resolve19(packageRoot, "package.json"), "utf8")
3332
+ readFileSync10(resolve19(packageRoot, "package.json"), "utf8")
3273
3333
  );
3274
3334
  return pkg.version;
3275
3335
  } catch {
@@ -3297,7 +3357,7 @@ var init_notifier = __esm({
3297
3357
  const path = cacheFilePath();
3298
3358
  if (!existsSync14(path)) return null;
3299
3359
  try {
3300
- return JSON.parse(readFileSync11(path, "utf8"));
3360
+ return JSON.parse(readFileSync10(path, "utf8"));
3301
3361
  } catch {
3302
3362
  return null;
3303
3363
  }
@@ -3305,8 +3365,8 @@ var init_notifier = __esm({
3305
3365
  writeCache = (cache) => {
3306
3366
  try {
3307
3367
  const path = cacheFilePath();
3308
- mkdirSync7(dirname6(path), { recursive: true });
3309
- writeFileSync9(path, JSON.stringify(cache, null, 2) + "\n");
3368
+ mkdirSync6(dirname6(path), { recursive: true });
3369
+ writeFileSync8(path, JSON.stringify(cache, null, 2) + "\n");
3310
3370
  } catch {
3311
3371
  }
3312
3372
  };
@@ -3350,58 +3410,85 @@ var init_notifier = __esm({
3350
3410
 
3351
3411
  // src/cli.ts
3352
3412
  init_prompts();
3353
- import process4 from "process";
3354
- var HELP = `quiver-cli - compose agent skills, commands & MCP servers into any repo
3413
+ import process3 from "process";
3414
+ var HELP = `quiver-cli - compose agent skills, commands, plugins & MCP servers into any repo
3355
3415
 
3356
3416
  Usage:
3357
3417
  quiver-cli <command> [options]
3358
3418
 
3359
3419
  Commands:
3360
3420
  init Interactive picker over the catalog; write native configs + quiver.lock
3361
- add <id> Add a single catalog entry (skill:<name>, command:<name>, mcp:<name>)
3421
+ add <id> Add an entry (skill:<name>, command:<name>, mcp:<name>, plugin:<name>)
3362
3422
  remove <id> Remove a single entry; keep lockfile + configs consistent
3363
3423
  sync Regenerate provider configs from .agents/ (warns on drift)
3364
3424
  providers [a,b] Change which tools get configs (claude, opencode, codex)
3365
3425
  update [id] Pull newer catalog content into .agents/ (all or one entry)
3366
- list Show installed entries (skills, commands, MCP tool counts)
3367
- status Diff the lockfile against what is actually in the repo
3368
- check Detect upstream drift (skill digests, MCP tool snapshots)
3426
+ list Show installed entries (skills, commands, plugins, MCP tool counts)
3427
+ check Detect drift: skill digests, provider shims, MCP tool
3428
+ snapshots (--offline skips MCP re-introspection)
3369
3429
  upstream Catalog maintenance: check source repos for skill updates
3370
3430
  (run in the quiver-cli repo or with a writable --catalog)
3371
3431
  upstream pull Pull latest upstream content into the catalog [skill]
3372
- login Store a GitHub token for remote (github:) catalogs
3373
- logout Remove the stored GitHub token
3374
3432
  help Show this help
3375
3433
  version Show the quiver-cli version
3376
3434
 
3377
3435
  Options:
3378
3436
  -f, --force Overwrite existing files
3379
3437
  --all, -y Keep everything without prompting (non-interactive)
3380
- --json Machine-readable output (status/check/upstream/list)
3438
+ --json Machine-readable output (check/upstream/list)
3381
3439
  -V, --verbose Show full tool lists and description diffs (check)
3382
3440
  --accept Record the current MCP tool snapshots as the new baseline (check)
3441
+ --offline Skip MCP re-introspection; check digests + shims only (check)
3442
+ --dry-run Report what would change without writing (update)
3383
3443
  --providers=a,b Generate configs only for these tools (init, sync, providers)
3384
3444
  --catalog=<source> Catalog source for init (e.g. github:owner/repo[/path][#ref])
3385
3445
  --introspect-stdio Allow introspecting stdio MCP servers (runs foreign code)
3386
3446
  -v, --version Show the quiver-cli version
3387
3447
  `;
3448
+ var KNOWN_FLAGS = /* @__PURE__ */ new Set([
3449
+ "--force",
3450
+ "-f",
3451
+ "--all",
3452
+ "--yes",
3453
+ "-y",
3454
+ "--json",
3455
+ "--verbose",
3456
+ "-V",
3457
+ "--accept",
3458
+ "--offline",
3459
+ "--dry-run",
3460
+ "--introspect-stdio",
3461
+ "--help",
3462
+ "-h",
3463
+ "--version",
3464
+ "-v"
3465
+ ]);
3466
+ var KNOWN_VALUE_FLAGS = ["--providers", "--catalog"];
3467
+ var isKnownFlag = (arg) => {
3468
+ if (KNOWN_FLAGS.has(arg)) return true;
3469
+ return KNOWN_VALUE_FLAGS.some((f) => arg.startsWith(`${f}=`));
3470
+ };
3388
3471
  var parse2 = (argv) => {
3389
3472
  const [command = "init", ...rest] = argv;
3390
3473
  const flags = new Set(rest.filter((a) => a.startsWith("-")));
3391
3474
  const positionals = rest.filter((a) => !a.startsWith("-"));
3475
+ const unknownFlags = [...flags].filter((a) => !isKnownFlag(a));
3392
3476
  const providersFlag = rest.find((a) => a.startsWith("--providers="));
3393
3477
  const providers2 = providersFlag ? providersFlag.slice("--providers=".length).split(",").map((p) => p.trim()).filter(Boolean) : null;
3394
3478
  const catalogFlag = rest.find((a) => a.startsWith("--catalog="));
3395
3479
  const catalog = catalogFlag ? catalogFlag.slice("--catalog=".length).trim() || null : null;
3396
3480
  return {
3397
3481
  command,
3482
+ unknownFlags,
3398
3483
  options: {
3399
- targetRoot: process4.cwd(),
3484
+ targetRoot: process3.cwd(),
3400
3485
  force: flags.has("--force") || flags.has("-f"),
3401
3486
  all: flags.has("--all") || flags.has("--yes") || flags.has("-y"),
3402
3487
  json: flags.has("--json"),
3403
3488
  verbose: flags.has("--verbose") || flags.has("-V"),
3404
3489
  accept: flags.has("--accept"),
3490
+ offline: flags.has("--offline"),
3491
+ dryRun: flags.has("--dry-run"),
3405
3492
  introspectStdio: flags.has("--introspect-stdio"),
3406
3493
  providers: providers2,
3407
3494
  catalog,
@@ -3410,13 +3497,22 @@ var parse2 = (argv) => {
3410
3497
  };
3411
3498
  };
3412
3499
  var run = async () => {
3413
- const { command, options } = parse2(process4.argv.slice(2));
3414
- if (process4.stdin.isTTY && !options.all) {
3500
+ const { command, options, unknownFlags } = parse2(process3.argv.slice(2));
3501
+ if (unknownFlags.length) {
3502
+ await error(
3503
+ `Unknown option${unknownFlags.length === 1 ? "" : "s"}: ${unknownFlags.join(", ")}
3504
+ `
3505
+ );
3506
+ console.log(HELP);
3507
+ process3.exitCode = 1;
3508
+ return;
3509
+ }
3510
+ if (process3.stdin.isTTY && !options.all) {
3415
3511
  const { checkNodeForCommand: checkNodeForCommand2 } = await Promise.resolve().then(() => (init_node_guard(), node_guard_exports));
3416
3512
  const guard = checkNodeForCommand2(command);
3417
3513
  if (!guard.ok) {
3418
3514
  await error(guard.message);
3419
- process4.exitCode = 1;
3515
+ process3.exitCode = 1;
3420
3516
  return;
3421
3517
  }
3422
3518
  }
@@ -3458,11 +3554,6 @@ var run = async () => {
3458
3554
  await list3(options);
3459
3555
  break;
3460
3556
  }
3461
- case "status": {
3462
- const { status: status2 } = await Promise.resolve().then(() => (init_status(), status_exports));
3463
- await status2(options);
3464
- break;
3465
- }
3466
3557
  case "check": {
3467
3558
  const { check: check2 } = await Promise.resolve().then(() => (init_check(), check_exports));
3468
3559
  await check2(options);
@@ -3473,16 +3564,6 @@ var run = async () => {
3473
3564
  await upstream2(options);
3474
3565
  break;
3475
3566
  }
3476
- case "login": {
3477
- const { login: login2 } = await Promise.resolve().then(() => (init_login(), login_exports));
3478
- await login2(options);
3479
- break;
3480
- }
3481
- case "logout": {
3482
- const { logout: logout2 } = await Promise.resolve().then(() => (init_logout(), logout_exports));
3483
- await logout2(options);
3484
- break;
3485
- }
3486
3567
  case "help":
3487
3568
  case "--help":
3488
3569
  case "-h":
@@ -3505,7 +3586,7 @@ var run = async () => {
3505
3586
  console.error(`Unknown command: ${command}
3506
3587
  `);
3507
3588
  console.log(HELP);
3508
- process4.exitCode = 1;
3589
+ process3.exitCode = 1;
3509
3590
  }
3510
3591
  if (!["version", "--version", "-v", "help", "--help", "-h"].includes(command)) {
3511
3592
  await maybeNotifyUpdate(options.json);
@@ -3527,7 +3608,14 @@ ${c.yellow("\u25B2")} ${c.bold("update available")} ${c.dim(
3527
3608
  } catch {
3528
3609
  }
3529
3610
  };
3530
- run().catch(async (err) => {
3531
- await error(err instanceof Error ? err.message : String(err));
3532
- process4.exitCode = 1;
3533
- });
3611
+ var main = () => {
3612
+ run().catch(async (err) => {
3613
+ await error(err instanceof Error ? err.message : String(err));
3614
+ process3.exitCode = 1;
3615
+ });
3616
+ };
3617
+ export {
3618
+ main,
3619
+ parse2 as parse,
3620
+ run
3621
+ };