the-grimoire-cli 0.4.0 → 0.5.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.
@@ -108,10 +108,15 @@ never edit `.agents/` in a consuming project — restate it in `local/` (loads l
108
108
 
109
109
  ### `bootstrap` — wire plugins / MCP / skills
110
110
  Reads base `tooling.json` ∪ `local/tooling.json` (base wins on key conflict; local adds new keys).
111
- - Dry-run (default): prints missing plugins, MCP servers to ensure, and skill install hints.
112
- - `--apply`: enables missing plugins in `~/.claude/settings.json` (backs it up first, only **adds**,
113
- never disables), and merges MCP servers into `.mcp.json` (never clobbers an existing server). Flags
114
- unresolved `${ENV}` placeholders.
111
+ For every missing plugin it prints the **paste-in-Claude** install command (`/plugin marketplace add
112
+ <source> && /plugin install <name>@<marketplace>`) the CLI can only flip the enable flag, it cannot
113
+ register a marketplace or run a slash command.
114
+ - **Interactive** (default, in a terminal): asks `enable <plugin>? [y/N]` per missing plugin, then
115
+ enables the chosen ones in `~/.claude/settings.json` and merges MCP servers.
116
+ - **Dry-run** (no TTY — CI/piped, and the `init`-embedded preview): prints the plan, writes nothing.
117
+ - `--apply`: non-interactive enable-all — enables every missing plugin in `~/.claude/settings.json`
118
+ (backs it up first, only **adds**, never disables), and merges MCP servers into `.mcp.json` (never
119
+ clobbers an existing server). Flags unresolved `${ENV}` placeholders.
115
120
 
116
121
  ### `index` — regenerate per-folder `INDEX.md`
117
122
  One-line-per-file table built from each file's frontmatter `description:` or its H1 + first sentence.
package/.agents/VERSION CHANGED
@@ -1,4 +1,4 @@
1
- grimoire v0.4.0
1
+ grimoire v0.5.0
2
2
  source: this repository IS the template (canonical source of truth)
3
3
  note: informational only — package.json `version` is the single source of truth;
4
4
  `grimoire init` / `grimoire sync` regenerate this from it and stamp the build sha.
@@ -60,7 +60,9 @@ simplify away" guardrail, and the `ponytail:` shortcut marker — tool-agnostic,
60
60
  Codex, OpenCode, Gemini, pi); install is per-machine, optional.
61
61
 
62
62
  - **Install** (Claude Code): `/plugin marketplace add DietrichGebert/ponytail` then
63
- `/plugin install ponytail@ponytail`. Other hosts: see the ponytail README.
63
+ `/plugin install ponytail@ponytail`. Other hosts: see the ponytail README. ponytail is declared in
64
+ `tooling.json` (with a `source` field = the marketplace repo), so `grimoire bootstrap` lists it and
65
+ prints exactly this paste command.
64
66
  - **`/ponytail-review`** — review the current diff for over-engineering; hands back a delete-list.
65
67
  - **`/ponytail-audit`** — same, whole repo instead of the diff.
66
68
  - **`/ponytail-debt`** — harvest the `ponytail:` shortcut markers into a ledger so "later" isn't
@@ -6,7 +6,8 @@
6
6
  { "name": "ui-ux-pro-max", "marketplace": "ui-ux-pro-max-skill", "scope": "user" },
7
7
  { "name": "andrej-karpathy-skills", "marketplace": "karpathy-skills", "scope": "user" },
8
8
  { "name": "pordee", "marketplace": "pordee", "scope": "user" },
9
- { "name": "caveman", "marketplace": "caveman", "scope": "user" }
9
+ { "name": "caveman", "marketplace": "caveman", "scope": "user" },
10
+ { "name": "ponytail", "marketplace": "ponytail", "scope": "user", "source": "DietrichGebert/ponytail" }
10
11
  ],
11
12
  "skills": [
12
13
  { "name": "mattpocock", "install": "npx skills@latest add mattpocock/skills", "setup": "/setup-matt-pocock-skills", "source": "https://github.com/mattpocock/skills", "scope": "user" },
package/bin/grimoire.mjs CHANGED
@@ -7,6 +7,7 @@ import path from "node:path";
7
7
  import fs from "node:fs";
8
8
  import os from "node:os";
9
9
  import { execFileSync } from "node:child_process";
10
+ import { createInterface } from "node:readline";
10
11
 
11
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
13
  const TEMPLATE_ROOT = path.resolve(__dirname, "..");
@@ -82,6 +83,25 @@ function applyPlugins(sp, settings, missing) {
82
83
  fs.writeFileSync(sp, JSON.stringify(settings, null, 2) + "\n");
83
84
  }
84
85
 
86
+ // The Claude `/plugin …` commands the user pastes to actually install a plugin: enabling the flag
87
+ // in settings.json only works once its marketplace is registered, and the CLI can't run a slash
88
+ // command. Official-marketplace plugins need no `marketplace add`; a custom one needs its `source`
89
+ // repo (omit the add line when we don't know it).
90
+ function pluginInstallHint(pl) {
91
+ const key = pluginKey(pl);
92
+ if (pl.marketplace === "claude-plugins-official") return `/plugin install ${key}`;
93
+ if (pl.source) return `/plugin marketplace add ${pl.source} && /plugin install ${key}`;
94
+ return `/plugin install ${key} (add its marketplace first)`;
95
+ }
96
+
97
+ // ponytail: TTY-only y/N prompt; callers must guard on process.stdin.isTTY so CI never blocks here.
98
+ function promptYesNo(question) {
99
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
100
+ return new Promise((resolve) => {
101
+ rl.question(`${question} [y/N] `, (a) => { rl.close(); resolve(/^y(es)?$/i.test(a.trim())); });
102
+ });
103
+ }
104
+
85
105
  // Collect unresolved ${ENV} placeholders anywhere in a server definition (env for stdio servers,
86
106
  // headers/url for http servers, etc.). Recurses over strings so transport shape does not matter.
87
107
  function unresolvedEnv(node, out) {
@@ -301,7 +321,9 @@ function init({ dir }) {
301
321
  log(" project-owned (seeded if absent): codex/ journal/ local/");
302
322
  log(" next: set the active stack profile + testing policy in local/AGENTS.local.md");
303
323
 
304
- bootstrap({ dir, apply: false });
324
+ // init only previews; `grimoire bootstrap` does the interactive install. The dry-run path has no
325
+ // await today, but .catch keeps a future async step from becoming a silent unhandled rejection.
326
+ bootstrap({ dir, apply: false, prompt: false }).catch((e) => fail(e.message));
305
327
  }
306
328
 
307
329
  function sync({ dir }) {
@@ -336,24 +358,39 @@ function sync({ dir }) {
336
358
  log(" tooling.json may have changed; run `grimoire bootstrap` to apply plugin/MCP updates.");
337
359
  }
338
360
 
339
- function bootstrap({ dir, apply }) {
361
+ async function bootstrap({ dir, apply, prompt = true }) {
340
362
  const tooling = mergedTooling(dir); // base ∪ local/tooling.json
341
363
  const sp = claudeSettingsPath();
342
364
  const settings = readSettings(sp);
343
365
  const missing = missingPlugins(tooling, settings);
366
+ // Interactive (per-plugin y/N) only in a real terminal; --apply forces enable-all; otherwise
367
+ // (CI / piped stdin / the init-embedded call) stay a dry-run so it never blocks on input.
368
+ const interactive = !apply && prompt && !!process.stdin.isTTY;
369
+ const writes = apply || interactive;
344
370
 
345
- log(`grimoire bootstrap (${apply ? "apply" : "dry-run"})`);
371
+ log(`grimoire bootstrap (${apply ? "apply" : interactive ? "interactive" : "dry-run"})`);
346
372
 
347
373
  if (missing.length === 0) {
348
374
  log(" plugins: all required plugins already enabled.");
349
375
  } else {
350
376
  log(" plugins missing:");
351
377
  for (const pl of missing) log(` - ${pluginKey(pl)}`);
352
- if (apply) {
353
- applyPlugins(sp, settings, missing);
354
- log(` enabled ${missing.length} plugin(s); backup at ${sp}.bak`);
355
- } else {
356
- log(` (dry-run) re-run with --apply to enable them in ${sp}`);
378
+ // The flag flip alone can't register a marketplace, so always show the paste-in-Claude commands.
379
+ log(" to install, paste in Claude Code:");
380
+ for (const pl of missing) log(` ${pluginInstallHint(pl)}`);
381
+
382
+ let chosen = missing; // --apply enables all; interactive narrows to the user's picks
383
+ if (interactive) {
384
+ chosen = [];
385
+ for (const pl of missing) if (await promptYesNo(` enable ${pluginKey(pl)}?`)) chosen.push(pl);
386
+ }
387
+ if (writes && chosen.length) {
388
+ applyPlugins(sp, settings, chosen);
389
+ log(` enabled ${chosen.length} plugin(s); backup at ${sp}.bak`);
390
+ } else if (interactive) {
391
+ log(" no plugins selected.");
392
+ } else if (!writes) {
393
+ log(` (dry-run) re-run with --apply to enable all, or run \`grimoire bootstrap\` in a terminal to choose`);
357
394
  }
358
395
  }
359
396
 
@@ -364,7 +401,7 @@ function bootstrap({ dir, apply }) {
364
401
  }
365
402
  }
366
403
 
367
- if (apply) {
404
+ if (writes) {
368
405
  const { added, needsEnv } = mergeMcp(dir, tooling);
369
406
  if (added.length) log(` mcp: added ${added.join(", ")} to .mcp.json`);
370
407
  else log(" mcp: all servers already present.");
@@ -607,7 +644,7 @@ function help() {
607
644
  log("grimoire <command> [--dir <path>]\n");
608
645
  log(" init scaffold .agents/ + CLAUDE.md + codex/ journal/ local/ (migrates an old layout; backs up first)");
609
646
  log(" sync wholesale-replace the .agents/ contract from the template (codex/ journal/ local/ untouched)");
610
- log(" bootstrap enable required plugins / MCP / skills (dry-run; --apply to write)");
647
+ log(" bootstrap enable plugins / MCP / skills — interactive y/N in a terminal, --apply enables all (dry-run otherwise)");
611
648
  log(" index regenerate per-folder INDEX.md (--check fails on drift, for CI)");
612
649
  log(" doctor health-check the project's wiring (exits non-zero on error, for CI)");
613
650
  log(" --version print the release version + build sha (-v)");
@@ -620,7 +657,7 @@ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith
620
657
  switch (args.cmd) {
621
658
  case "init": init(args); break;
622
659
  case "sync": sync(args); break;
623
- case "bootstrap": bootstrap(args); break;
660
+ case "bootstrap": await bootstrap(args); break;
624
661
  case "index": index(args); break;
625
662
  case "doctor": doctor(args); break;
626
663
  case "--version": case "-v": version(); break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "the-grimoire-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "A reusable, tool-agnostic AI-agent operating system. Init it into any project; sync template updates without clobbering local customization.",
5
5
  "type": "module",
6
6
  "bin": {