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/README.md +38 -7
- package/README.zh-CN.md +39 -8
- package/dist/catalog.d.ts +12 -0
- package/dist/catalog.js +9 -0
- package/dist/catalog.json +87 -0
- package/dist/cli.d.ts +23 -1
- package/dist/cli.js +526 -46
- package/dist/guide.d.ts +12 -0
- package/dist/guide.js +120 -0
- package/dist/help.d.ts +14 -0
- package/dist/help.js +152 -0
- package/dist/hooks.d.ts +45 -1
- package/dist/hooks.js +124 -23
- package/dist/plugins.d.ts +3 -1
- package/dist/plugins.js +101 -28
- package/dist/skills.d.ts +10 -2
- package/dist/skills.js +102 -33
- package/dist/types.d.ts +8 -0
- package/dist/types.js +13 -0
- package/dist/utils.d.ts +40 -0
- package/dist/utils.js +76 -7
- package/dist/workflow.d.ts +2 -1
- package/dist/workflow.js +21 -13
- package/package.json +21 -6
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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 =
|
|
688
|
-
|
|
689
|
-
|
|
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 =
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
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 =
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
|
|
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>;
|