auriga-cli 1.7.0 → 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/guide.js ADDED
@@ -0,0 +1,120 @@
1
+ const BOLD = "\x1b[1m";
2
+ const DIM = "\x1b[2m";
3
+ const CYAN = "\x1b[36m";
4
+ const YELLOW = "\x1b[33m";
5
+ const RESET = "\x1b[0m";
6
+ function c(color, code, text) {
7
+ return color ? `${code}${text}${RESET}` : text;
8
+ }
9
+ /**
10
+ * Renders the Agent-bootstrap SOP per spec §3.6. Plain-text when
11
+ * `color: false`; adds ANSI escapes for headings / command examples
12
+ * / warnings when `color: true`. Color detection happens at the call
13
+ * site (`process.stdout.isTTY && !process.env.NO_COLOR`); this
14
+ * function just renders what it's told.
15
+ */
16
+ export function renderGuide(opts) {
17
+ const h = (s) => c(opts.color, BOLD + CYAN, s);
18
+ const cmd = (s) => c(opts.color, DIM, s);
19
+ const warn = (s) => c(opts.color, YELLOW, s);
20
+ return `${h(`# auriga-cli bootstrap SOP (v${opts.version})`)}
21
+
22
+ This guide walks an Agent through installing the auriga harness
23
+ (CLAUDE.md + skills + plugins + hooks) into the current repository.
24
+
25
+ Run each step in order. If any step fails with exit 1, stop and report.
26
+ If exit 2, see stderr for per-category status and follow the "Retry"
27
+ hint.
28
+
29
+ ${h("## Step 1 — Prerequisite check")}
30
+
31
+ Ensure these CLIs are in PATH:
32
+ - node (>= 18)
33
+ - git
34
+ - claude (required for plugins; see https://docs.claude.com/claude-code)
35
+
36
+ Optional (only if you'll push a PR): gh
37
+
38
+ Verify:
39
+ ${cmd("node --version && git --version && claude --version")}
40
+
41
+ If \`claude\` is missing: install Claude Code first, then re-run this guide.
42
+
43
+ ${h("## Step 2 — Read --help BEFORE installing (do not skip)")}
44
+
45
+ ${warn("⚠")} Always inspect the catalog first so you know which skills,
46
+ plugins, and hooks are actually relevant for this project. Blindly
47
+ running \`install --all\` works as a turnkey preset, but for anything
48
+ beyond a greenfield bootstrap you should narrow scope.
49
+
50
+ Top-level catalog (every workflow skill / recommended skill / plugin /
51
+ hook with a short description):
52
+ ${cmd("npx -y auriga-cli --help")}
53
+
54
+ Per-type detail (flags + only that category's catalog slice):
55
+ ${cmd("npx -y auriga-cli install workflow --help")}
56
+ ${cmd("npx -y auriga-cli install skills --help")}
57
+ ${cmd("npx -y auriga-cli install recommended --help")}
58
+ ${cmd("npx -y auriga-cli install plugins --help")}
59
+ ${cmd("npx -y auriga-cli install hooks --help")}
60
+
61
+ ${h("## Step 3 — Install")}
62
+
63
+ Preset — the full default-on set (workflow + skills + plugins + hooks;
64
+ recommended skills are NOT included):
65
+ ${cmd("npx -y auriga-cli install --all")}
66
+
67
+ Targeted — single category, picking from the catalog surfaced in Step 2:
68
+ ${cmd("npx -y auriga-cli install workflow --lang en")}
69
+ ${cmd("npx -y auriga-cli install skills --skill brainstorming test-driven-development")}
70
+ ${cmd("npx -y auriga-cli install plugins --plugin skill-creator codex --scope user")}
71
+ ${cmd("npx -y auriga-cli install hooks --hook pr-ready-guard")}
72
+
73
+ Opt-in hooks: some hooks (e.g. \`notify\`) are NOT in the default set
74
+ because they have side effects (OS notifications, platform-gated deps).
75
+ Name them explicitly to install:
76
+ ${cmd("npx -y auriga-cli install hooks --hook notify")}
77
+
78
+ Opt-in recommended skills (cross-model delegation helpers —
79
+ claude-code-agent, codex-agent):
80
+ ${cmd("npx -y auriga-cli install recommended")}
81
+
82
+ (The leading \`-y\` is npx's flag; it suppresses the "is it OK to install
83
+ this package?" prompt. Required for non-interactive sessions.)
84
+
85
+ Exit codes:
86
+ 0 — all requested categories installed
87
+ 1 — fatal error (parse / fetch / missing prerequisite). Read stderr;
88
+ fix the root cause and re-run the SAME command.
89
+ 2 — partial success. stderr lists per-category status. Retry only the
90
+ failed category (the retry line is printed verbatim on stderr).
91
+
92
+ ${h("## Step 4 — Reload session (REQUIRED when installed non-interactively)")}
93
+
94
+ ${warn("⚠")} CLAUDE.md, .agents/skills/, .claude/plugins.json, and hook
95
+ registrations are loaded at Claude Code session startup. If you ran
96
+ \`npx -y auriga-cli install\` inside an existing Claude Code session
97
+ (e.g., \`claude -p\` / \`claude -p --worktree\`), the current session
98
+ will NOT see the new harness.
99
+
100
+ Action:
101
+ - Commit any in-flight work first
102
+ - Exit this session and start a new one to pick up the harness
103
+ - Resume the original task in the new session
104
+
105
+ ${h("## Step 5 — Verify install")}
106
+
107
+ Expected artifacts:
108
+ - CLAUDE.md (workflow manifesto)
109
+ - AGENTS.md -> CLAUDE.md (symlink)
110
+ - .agents/skills/<name>/ (one per installed skill)
111
+ - .claude/plugins.json
112
+ - .claude/settings.json (updated hook registrations, if hooks selected)
113
+
114
+ ${h("## Troubleshooting")}
115
+
116
+ - Network error during fetch → retry; if persistent, check GitHub raw access
117
+ - "catalog missing" error → re-install the package (\`npx clear-npx-cache\`)
118
+ - \`claude plugins install\` hangs → abort, report; see known issue list
119
+ `;
120
+ }
package/dist/help.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { Catalog } from "./catalog.js";
2
+ import type { CategoryName } from "./types.js";
3
+ /**
4
+ * Renders the detailed `--help` output per spec §4. Agent-readable
5
+ * catalog of every installable: Agent can decide what to pass to
6
+ * `install <type>` without a second round-trip.
7
+ */
8
+ export declare function renderHelp(catalog: Catalog, version: string): string;
9
+ /**
10
+ * Per-type help (`install <type> --help`). Shows just the flags and
11
+ * the matching catalog slice so an Agent can make a precise pick
12
+ * without scrolling past unrelated categories.
13
+ */
14
+ export declare function renderTypeHelp(catalog: Catalog, type: CategoryName, version: string): string;
package/dist/help.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Renders the detailed `--help` output per spec §4. Agent-readable
3
+ * catalog of every installable: Agent can decide what to pass to
4
+ * `install <type>` without a second round-trip.
5
+ */
6
+ export function renderHelp(catalog, version) {
7
+ const col = (entries) => entries.map((e) => ` ${padRight(e.name, 30)} ${truncate(e.description, 50)}`).join("\n");
8
+ return `auriga-cli v${version} — install Claude Code harness modules
9
+
10
+ USAGE
11
+ npx auriga-cli guide Agent bootstrap SOP (start here)
12
+ npx auriga-cli install (TTY only) checkbox menu
13
+ npx auriga-cli install --all [--scope <s>] workflow + skills + plugins + hooks
14
+ (excludes recommended — install separately)
15
+ npx auriga-cli install <type> [type-specific flags] single category
16
+ npx auriga-cli install <type> --help per-category help + catalog subset
17
+ npx auriga-cli --help
18
+
19
+ For non-interactive (Agent) use, prepend npx's own -y flag:
20
+ npx -y auriga-cli guide
21
+ npx -y auriga-cli install --all
22
+
23
+ TYPES (exactly one with <type> form)
24
+ workflow CLAUDE.md + AGENTS.md (workflow manifesto)
25
+ skills Default-on workflow skills (listed below)
26
+ recommended Opt-in utility skills (listed below)
27
+ plugins Claude Code plugins (listed below)
28
+ hooks Project-level hooks for Claude Code (listed below)
29
+
30
+ TYPE-SPECIFIC FLAGS
31
+ workflow: --lang <code> default en; available: en, zh-CN
32
+ --cwd <dir> default current working directory
33
+ skills: --skill <names...> space-separated; '*' = all
34
+ --scope <project|user> default project
35
+ recommended: --recommended-skill <names...>
36
+ --scope <project|user> default project
37
+ plugins: --plugin <names...>
38
+ --scope <project|user> default project
39
+ hooks: --hook <names...> non-interactive default installs every
40
+ hook with defaultOn != false
41
+ --scope <project|user> default project
42
+
43
+ TOP-LEVEL OPTIONS
44
+ -h, --help show this help
45
+ -v, --version show version
46
+
47
+ ──────────────────────────────────────────────────────
48
+ CATALOG (what each category contains)
49
+ ──────────────────────────────────────────────────────
50
+
51
+ Workflow skills (category: skills) ← installed by --all
52
+ ${col(catalog.workflowSkills)}
53
+
54
+ Recommended skills (category: recommended) ← NOT installed by --all
55
+ ${col(catalog.recommendedSkills)}
56
+
57
+ Plugins (category: plugins)
58
+ ${col(catalog.plugins)}
59
+
60
+ Hooks (category: hooks)
61
+ ${col(catalog.hooks)}
62
+
63
+ More: https://github.com/Ben2pc/auriga-cli
64
+ `;
65
+ }
66
+ /**
67
+ * Per-type help (`install <type> --help`). Shows just the flags and
68
+ * the matching catalog slice so an Agent can make a precise pick
69
+ * without scrolling past unrelated categories.
70
+ */
71
+ export function renderTypeHelp(catalog, type, version) {
72
+ const col = (entries) => entries.map((e) => ` ${padRight(e.name, 30)} ${truncate(e.description, 50)}`).join("\n");
73
+ const header = `auriga-cli v${version} — install ${type}`;
74
+ switch (type) {
75
+ case "workflow":
76
+ return `${header}
77
+
78
+ USAGE
79
+ npx auriga-cli install workflow [--lang <code>] [--cwd <dir>]
80
+
81
+ FLAGS
82
+ --lang <code> default en; available: en, zh-CN
83
+ --cwd <dir> default current working directory
84
+
85
+ NOTE
86
+ workflow has no --scope flag (single file + AGENTS.md symlink).
87
+ `;
88
+ case "skills":
89
+ return `${header}
90
+
91
+ USAGE
92
+ npx auriga-cli install skills [--skill <names...>] [--scope <project|user>]
93
+
94
+ FLAGS
95
+ --skill <names...> space-separated; '*' = all
96
+ omit → install every workflow skill listed below
97
+ --scope <project|user> default project
98
+
99
+ CATALOG (workflow skills — default-on set)
100
+ ${col(catalog.workflowSkills)}
101
+ `;
102
+ case "recommended":
103
+ return `${header}
104
+
105
+ USAGE
106
+ npx auriga-cli install recommended [--recommended-skill <names...>] [--scope <project|user>]
107
+
108
+ FLAGS
109
+ --recommended-skill <names...> space-separated; '*' = all
110
+ omit → install every recommended skill listed below
111
+ --scope <project|user> default project
112
+
113
+ CATALOG (recommended skills — opt-in, NOT installed by --all)
114
+ ${col(catalog.recommendedSkills)}
115
+ `;
116
+ case "plugins":
117
+ return `${header}
118
+
119
+ USAGE
120
+ npx auriga-cli install plugins [--plugin <names...>] [--scope <project|user>]
121
+
122
+ FLAGS
123
+ --plugin <names...> space-separated; '*' = all
124
+ omit → install every plugin listed below
125
+ --scope <project|user> default project
126
+
127
+ CATALOG (plugins)
128
+ ${col(catalog.plugins)}
129
+ `;
130
+ case "hooks":
131
+ return `${header}
132
+
133
+ USAGE
134
+ npx auriga-cli install hooks [--hook <names...>] [--scope <project|user>]
135
+
136
+ FLAGS
137
+ --hook <names...> space-separated; '*' = every compatible hook
138
+ omit → install every hook with defaultOn != false
139
+ --scope <project|user> default project
140
+ (project-local is only reachable via the TTY menu)
141
+
142
+ CATALOG (hooks — entries flagged "(opt-in)" require explicit --hook)
143
+ ${col(catalog.hooks)}
144
+ `;
145
+ }
146
+ }
147
+ function padRight(s, width) {
148
+ return s.length >= width ? s : s + " ".repeat(width - s.length);
149
+ }
150
+ function truncate(s, width) {
151
+ return s.length <= width ? s : s.slice(0, width - 1) + "…";
152
+ }
package/dist/hooks.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type InstallOpts } from "./utils.js";
1
2
  export interface HookDep {
2
3
  name: string;
3
4
  via: "brew";
@@ -34,6 +35,15 @@ export interface HookDef {
34
35
  * back to a generic "see <dir>/README.md" pointer.
35
36
  */
36
37
  customizeHints?: string[];
38
+ /**
39
+ * Whether the hook is part of the default-on set. `false` makes the
40
+ * hook opt-in: non-interactive `install hooks` with no `--hook` filter
41
+ * skips it, and the interactive checkbox leaves it unchecked. Absent
42
+ * / `true` → installed by default. Used for hooks with intrusive
43
+ * side effects (OS notifications, brew deps, platform constraints)
44
+ * that users probably want to pick up consciously.
45
+ */
46
+ defaultOn?: boolean;
37
47
  }
38
48
  export interface HooksConfig {
39
49
  hooks: HookDef[];
@@ -108,6 +118,19 @@ export declare function removeHookFromSettings(settings: SettingsFile, marker: s
108
118
  removed: number;
109
119
  };
110
120
  type Scope = "project-local" | "project" | "user";
121
+ /**
122
+ * Non-interactive scope map for hooks.
123
+ *
124
+ * Non-interactive surface only knows about two values — `project` (the
125
+ * default) and `user`. `project-local` exists only in the TTY menu; it's
126
+ * a per-developer uncommitted scope and carries enough "did you really
127
+ * mean this?" surface area that we gate it behind an interactive
128
+ * confirmation rather than exposing it as a CLI flag value.
129
+ *
130
+ * Exported so `tests/hooks.test.ts` can lock the contract down as a
131
+ * unit test.
132
+ */
133
+ export declare function mapNonInteractiveScope(scope: string | undefined): Scope;
111
134
  export declare function depBinary(dep: HookDep): string;
112
135
  export declare function loadHooksConfig(packageRoot: string): HooksConfig;
113
136
  export interface InstallHookResult {
@@ -158,5 +181,26 @@ export declare function cleanHookFromScope(hook: HookDef, scope: Scope, projectB
158
181
  removed: number;
159
182
  settingsPath: string;
160
183
  };
161
- export declare function installHooks(packageRoot: string): Promise<void>;
184
+ /**
185
+ * Non-interactive selection resolver for hooks.
186
+ *
187
+ * Diverges from resolvePluginSelection in the no-filter case. Hooks can
188
+ * carry intrusive side effects (OS notifications, brew deps), so the
189
+ * safe default is NOT "install everything". Three cases:
190
+ *
191
+ * - undefined (no --hook passed) → default-on set (filter on defaultOn !== false)
192
+ * - ["*"] (explicit opt-in to everything) → full compatible set
193
+ * - explicit names → exactly those (even if defaultOn is false)
194
+ */
195
+ export declare function resolveHookSelection(compatible: HookDef[], selected: string[] | undefined): HookDef[];
196
+ /**
197
+ * Given the full registry and the platform-filtered compatible subset,
198
+ * return the names in `selected` that refer to real hooks but aren't
199
+ * available on the current platform. Empty result means the selection
200
+ * is either fully compatible or references unknown hooks (that case is
201
+ * left to the catalog validator — we don't pretend unknown names are
202
+ * platform issues).
203
+ */
204
+ export declare function findIncompatibleExplicit(all: HookDef[], compatible: HookDef[], selected: string[]): string[];
205
+ export declare function installHooks(packageRoot: string, opts: InstallOpts): Promise<void>;
162
206
  export {};
package/dist/hooks.js CHANGED
@@ -148,6 +148,9 @@ function validateHookEntry(hook, idx) {
148
148
  }
149
149
  }
150
150
  }
151
+ if (h.defaultOn !== undefined && typeof h.defaultOn !== "boolean") {
152
+ throw new Error(`hooks.json: hooks[${idx}].defaultOn must be a boolean`);
153
+ }
151
154
  }
152
155
  /**
153
156
  * Pure, idempotent settings merge. Deep-clones input, dedupes by two
@@ -313,6 +316,21 @@ function resolveScope(scope, projectBase, hookName) {
313
316
  commandHookDir: `$CLAUDE_PROJECT_DIR/.claude/hooks/${hookName}`,
314
317
  };
315
318
  }
319
+ /**
320
+ * Non-interactive scope map for hooks.
321
+ *
322
+ * Non-interactive surface only knows about two values — `project` (the
323
+ * default) and `user`. `project-local` exists only in the TTY menu; it's
324
+ * a per-developer uncommitted scope and carries enough "did you really
325
+ * mean this?" surface area that we gate it behind an interactive
326
+ * confirmation rather than exposing it as a CLI flag value.
327
+ *
328
+ * Exported so `tests/hooks.test.ts` can lock the contract down as a
329
+ * unit test.
330
+ */
331
+ export function mapNonInteractiveScope(scope) {
332
+ return scope === "user" ? "user" : "project";
333
+ }
316
334
  function scopeChoices() {
317
335
  return [
318
336
  {
@@ -659,35 +677,100 @@ export function cleanHookFromScope(hook, scope, projectBase) {
659
677
  }
660
678
  return { removed: result.removed, settingsPath: r.settingsPath };
661
679
  }
662
- export async function installHooks(packageRoot) {
680
+ /**
681
+ * Non-interactive selection resolver for hooks.
682
+ *
683
+ * Diverges from resolvePluginSelection in the no-filter case. Hooks can
684
+ * carry intrusive side effects (OS notifications, brew deps), so the
685
+ * safe default is NOT "install everything". Three cases:
686
+ *
687
+ * - undefined (no --hook passed) → default-on set (filter on defaultOn !== false)
688
+ * - ["*"] (explicit opt-in to everything) → full compatible set
689
+ * - explicit names → exactly those (even if defaultOn is false)
690
+ */
691
+ export function resolveHookSelection(compatible, selected) {
692
+ if (!selected)
693
+ return compatible.filter((h) => h.defaultOn !== false);
694
+ if (selected.length === 1 && selected[0] === "*")
695
+ return compatible;
696
+ const wanted = new Set(selected);
697
+ return compatible.filter((h) => wanted.has(h.name));
698
+ }
699
+ /**
700
+ * Given the full registry and the platform-filtered compatible subset,
701
+ * return the names in `selected` that refer to real hooks but aren't
702
+ * available on the current platform. Empty result means the selection
703
+ * is either fully compatible or references unknown hooks (that case is
704
+ * left to the catalog validator — we don't pretend unknown names are
705
+ * platform issues).
706
+ */
707
+ export function findIncompatibleExplicit(all, compatible, selected) {
708
+ const compatibleNames = new Set(compatible.map((h) => h.name));
709
+ const allNames = new Set(all.map((h) => h.name));
710
+ return selected.filter((n) => allNames.has(n) && !compatibleNames.has(n));
711
+ }
712
+ export async function installHooks(packageRoot, opts) {
663
713
  const config = loadHooksConfig(packageRoot);
664
714
  const compatible = config.hooks.filter((h) => h.runtimePlatforms.includes(process.platform));
665
715
  if (compatible.length === 0) {
666
716
  log.warn(`No hooks available for your platform (${process.platform}). Skipping.`);
667
717
  return;
668
718
  }
669
- const selected = await withEsc(checkbox({
670
- message: "Select hooks to install:",
671
- choices: compatible.map((h) => ({
672
- name: `${h.name} ${h.description}`,
673
- value: h,
674
- checked: true,
675
- })),
676
- }));
719
+ // Non-interactive explicit `--hook <name>` has stronger intent than
720
+ // the default set: if the caller named a hook that isn't available
721
+ // on this platform, fail fast. A silent no-op would let CI pipelines
722
+ // report success with the intended hook missing.
723
+ if (!opts.interactive && opts.selected && opts.selected[0] !== "*") {
724
+ const missing = findIncompatibleExplicit(config.hooks, compatible, opts.selected);
725
+ if (missing.length > 0) {
726
+ throw new Error(`hook${missing.length > 1 ? "s" : ""} not available on ${process.platform}: ${missing.join(", ")}. Run \`npx -y auriga-cli install hooks --help\` to see platform compatibility.`);
727
+ }
728
+ }
729
+ const selected = opts.interactive
730
+ ? await withEsc(checkbox({
731
+ message: "Select hooks to install:",
732
+ choices: compatible.map((h) => ({
733
+ name: `${h.name} — ${h.description}`,
734
+ value: h,
735
+ checked: h.defaultOn !== false,
736
+ })),
737
+ }))
738
+ : resolveHookSelection(compatible, opts.selected);
739
+ // Surface any opt-in hooks skipped by the default set so the user
740
+ // isn't silently missing a hook they can see in --help / the README.
741
+ // Only fires on the non-interactive undefined-selection path (TTY
742
+ // checkbox already shows them unchecked).
743
+ if (!opts.interactive && opts.selected === undefined) {
744
+ const skippedOptIn = compatible.filter((h) => h.defaultOn === false);
745
+ for (const h of skippedOptIn) {
746
+ log.skip(`${h.name} (opt-in; re-run \`npx -y auriga-cli install hooks --hook ${h.name}\` to install)`);
747
+ }
748
+ }
677
749
  if (selected.length === 0) {
678
750
  log.skip("No hooks selected");
679
751
  return;
680
752
  }
753
+ // Non-interactive scope (two values only):
754
+ // undefined / "project" → project (shared .claude/settings.json)
755
+ // "user" → user (~/.claude/settings.json)
756
+ // project-local is reachable only via the TTY menu.
757
+ const nonInteractiveScope = mapNonInteractiveScope(opts.scope);
681
758
  // Lazily prompted on the first project-scoped hook, then reused. Users
682
759
  // who pick only "user" scope are never asked about a project directory.
760
+ //
761
+ // Non-interactive path always falls back to `process.cwd()` — the
762
+ // parser rejects `--cwd` for any non-workflow type (§3.5 rule 5), so
763
+ // reading `opts.cwd` here would just be dead dispatch.
683
764
  let projectBaseResolved = null;
684
765
  async function ensureProjectBase() {
685
766
  if (projectBaseResolved !== null)
686
767
  return projectBaseResolved;
687
- const projectBase = await withEsc(input({
688
- message: "Hooks install target directory:",
689
- default: process.cwd(),
690
- }));
768
+ const projectBase = opts.interactive
769
+ ? await withEsc(input({
770
+ message: "Hooks install target directory:",
771
+ default: process.cwd(),
772
+ }))
773
+ : process.cwd();
691
774
  const resolvedPath = path.resolve(projectBase);
692
775
  if (!fs.existsSync(resolvedPath) || !fs.statSync(resolvedPath).isDirectory()) {
693
776
  log.error(`Not a valid directory: ${resolvedPath}`);
@@ -696,22 +779,27 @@ export async function installHooks(packageRoot) {
696
779
  projectBaseResolved = resolvedPath;
697
780
  return projectBaseResolved;
698
781
  }
782
+ const failures = [];
699
783
  for (const hook of selected) {
700
784
  console.log(`\n· ${hook.name}`);
701
785
  // Per-hook scope is intentional (not a single upfront prompt like
702
786
  // plugins.ts / skills.ts): a future user may want personal dev tools
703
787
  // at user level and project-specific hooks at project level. The
704
788
  // single-hook case is functionally identical to a single prompt.
705
- const scope = await withEsc(select({
706
- message: `Where to install the ${hook.name} hook?`,
707
- choices: scopeChoices(),
708
- default: "project-local",
709
- }));
789
+ const scope = opts.interactive
790
+ ? await withEsc(select({
791
+ message: `Where to install the ${hook.name} hook?`,
792
+ choices: scopeChoices(),
793
+ default: "project-local",
794
+ }))
795
+ : nonInteractiveScope;
710
796
  // User scope mutates ~/.claude/settings.json — global, affects every
711
797
  // project on this machine. A passive select label and a one-line warn
712
798
  // both scroll past quickly. Make the user explicitly opt in to the
713
799
  // global mutation; default to "no" so a missed Enter is the safe path.
714
- if (scope === "user") {
800
+ // In non-interactive mode the caller has already expressed intent via
801
+ // `--scope user`, so we honor it without another confirmation gate.
802
+ if (opts.interactive && scope === "user") {
715
803
  const proceed = await withEsc(confirm({
716
804
  message: `Modify your global ~/.claude/settings.json? This affects every project on this machine. A .bak snapshot is taken before any change.`,
717
805
  default: false,
@@ -732,13 +820,17 @@ export async function installHooks(packageRoot) {
732
820
  // Cross-scope cleanup: if this hook's marker is already present in a
733
821
  // *different* scope's settings file, leaving it there means the hook
734
822
  // will fire from both scopes. Detect, prompt, clean before installing.
823
+ // In non-interactive mode the default (remove stale) is applied
824
+ // silently — matches the interactive default: true.
735
825
  const stale = findStaleScopes(hook, scope, projectBaseForHook);
736
826
  for (const entry of stale) {
737
827
  log.warn(`Found existing ${hook.name} hook in ${relativeFromCwd(entry.settingsPath)} (${entry.scope} scope, ${entry.count} entr${entry.count === 1 ? "y" : "ies"})`);
738
- const remove = await withEsc(confirm({
739
- message: `Remove the stale registration so the hook only fires once?`,
740
- default: true,
741
- }));
828
+ const remove = opts.interactive
829
+ ? await withEsc(confirm({
830
+ message: `Remove the stale registration so the hook only fires once?`,
831
+ default: true,
832
+ }))
833
+ : true;
742
834
  if (remove) {
743
835
  const cleaned = cleanHookFromScope(hook, entry.scope, projectBaseForHook);
744
836
  log.ok(`removed ${cleaned.removed} from ${relativeFromCwd(cleaned.settingsPath)}`);
@@ -756,10 +848,12 @@ export async function installHooks(packageRoot) {
756
848
  }
757
849
  catch (e) {
758
850
  log.error(`${hook.name}: ${e.message}`);
851
+ failures.push(hook.name);
759
852
  continue;
760
853
  }
761
854
  if (result.aborted) {
762
855
  log.error(`${hook.name} aborted: ${result.aborted}`);
856
+ failures.push(hook.name);
763
857
  continue;
764
858
  }
765
859
  const settingsRel = relativeFromCwd(result.settingsPath);
@@ -771,6 +865,10 @@ export async function installHooks(packageRoot) {
771
865
  if (result.settingsError) {
772
866
  log.error(`${hook.name}: ${result.settingsError}`);
773
867
  log.warn(`Files were copied to ${dirRel} but settings not updated. Add the hook entry manually if you want it active.`);
868
+ // Registration failure leaves the hook installed-but-inactive. Count
869
+ // it as a failure so non-interactive `install hooks` exits 2 and the
870
+ // caller can retry — quietly reporting success would ship a dead hook.
871
+ failures.push(hook.name);
774
872
  }
775
873
  else if (result.settingsMutated) {
776
874
  log.ok(`registered in ${settingsRel}`);
@@ -792,4 +890,7 @@ export async function installHooks(packageRoot) {
792
890
  console.log(` See ${dirRel}/README.md for customization options.`);
793
891
  }
794
892
  }
893
+ if (failures.length > 0 && !opts.interactive) {
894
+ throw new Error(`${failures.length} hook(s) failed to install: ${failures.join(", ")}`);
895
+ }
795
896
  }
package/dist/plugins.d.ts CHANGED
@@ -1 +1,3 @@
1
- export declare function installPlugins(packageRoot: string): Promise<void>;
1
+ import type { InstallOpts, PluginsConfig } from "./utils.js";
2
+ export declare function validatePluginsConfig(raw: unknown): asserts raw is PluginsConfig;
3
+ export declare function installPlugins(packageRoot: string, opts: InstallOpts): Promise<void>;