@devrik-tools/claude-gates 0.1.2 → 0.2.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.
- package/.claude-plugin/marketplace.json +7 -1
- package/cli/__tests__/config.test.mjs +113 -101
- package/cli/__tests__/install.test.mjs +102 -0
- package/cli/__tests__/materialize.test.mjs +95 -0
- package/cli/__tests__/registry-gates-consistency.test.mjs +52 -44
- package/cli/__tests__/task.test.mjs +124 -0
- package/cli/__tests__/version-consistency.test.mjs +44 -0
- package/cli/config.mjs +124 -113
- package/cli/index.mjs +100 -97
- package/cli/init.mjs +259 -256
- package/cli/install.mjs +150 -80
- package/cli/materialize.mjs +102 -51
- package/cli/registry.mjs +136 -127
- package/cli/task.mjs +140 -0
- package/package.json +2 -1
- package/plugins/gates/.claude-plugin/plugin.json +8 -2
- package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +83 -0
- package/plugins/gates/hooks/__tests__/doctor.test.mjs +85 -0
- package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +65 -0
- package/plugins/gates/hooks/ask-adoption.mjs +147 -0
- package/plugins/gates/hooks/doctor.mjs +207 -0
- package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +88 -88
- package/plugins/gates/hooks/gates/audit-before-build/test.mjs +117 -92
- package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +104 -0
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +48 -45
- package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +165 -0
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +285 -268
- package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +151 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +265 -177
- package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +207 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +501 -456
- package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +161 -143
- package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +69 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +126 -118
- package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +68 -0
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +43 -49
- package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -100
- package/plugins/gates/hooks/gates/force-parallel/index.mjs +119 -0
- package/plugins/gates/hooks/gates/force-parallel/test.mjs +88 -0
- package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +155 -0
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -112
- package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +66 -0
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -186
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -238
- package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +96 -0
- package/plugins/gates/hooks/gates/intent-flow/test.mjs +165 -136
- package/plugins/gates/hooks/gates/lint-commit/index.mjs +149 -0
- package/plugins/gates/hooks/gates/lint-commit/test.mjs +85 -0
- package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +100 -0
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -166
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +79 -85
- package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +80 -0
- package/plugins/gates/hooks/gates/never-assume/index.mjs +58 -55
- package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +71 -0
- package/plugins/gates/hooks/gates/never-assume/test.mjs +95 -78
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +148 -142
- package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +109 -0
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +123 -120
- package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +83 -0
- package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +91 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +144 -147
- package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +89 -64
- package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +102 -0
- package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +94 -0
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +159 -150
- package/plugins/gates/hooks/gates/risk-level/index.mjs +263 -203
- package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +116 -0
- package/plugins/gates/hooks/gates/risk-level/test.mjs +153 -125
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +56 -50
- package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +58 -0
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +116 -111
- package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +97 -0
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +184 -99
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +126 -0
- package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +130 -0
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -251
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +159 -0
- package/plugins/gates/hooks/gates/stop-pending/test.mjs +114 -0
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -101
- package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +72 -0
- package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +63 -0
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -181
- package/plugins/gates/hooks/gates/test-matrix/test.mjs +114 -87
- package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +62 -0
- package/plugins/gates/hooks/gates/tool-map/index.mjs +143 -140
- package/plugins/gates/hooks/hooks.json +346 -286
- package/plugins/gates/hooks/lib/__tests__/config.test.mjs +173 -154
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +210 -154
- package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +114 -0
- package/plugins/gates/hooks/lib/config.mjs +172 -165
- package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +189 -0
- package/plugins/gates/hooks/lib/hook-io.mjs +318 -208
- package/plugins/gates/hooks/lib/signals.mjs +127 -0
- package/plugins/gates/hooks/wiring-check.mjs +227 -0
- package/plugins/tasks/.claude-plugin/plugin.json +14 -0
- package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +100 -0
- package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +95 -0
- package/plugins/tasks/hooks/hooks.json +26 -0
- package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +180 -132
- package/plugins/tasks/hooks/lib/task-store.mjs +197 -159
- package/plugins/tasks/hooks/register-requests.mjs +141 -108
- package/plugins/tasks/hooks/session-tasks.mjs +108 -0
- package/registry.json +793 -686
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Version coherence guard. This exists because of a real incident: the npm package was
|
|
2
|
+
// bumped to 0.1.3 and published, but plugin.json / marketplace.json stayed at 0.1.0. Claude
|
|
3
|
+
// Code versions a plugin by its plugin.json/marketplace version, so a machine that already
|
|
4
|
+
// had 0.1.0 cached NEVER updated the code on reinstall — it ran the old, pre-migration gates
|
|
5
|
+
// while the freshly written config.json said 0.1.3. The gates looked "enabled but broken".
|
|
6
|
+
//
|
|
7
|
+
// The rule this enforces: the npm package version, both plugin.json versions, and every
|
|
8
|
+
// marketplace.json plugin entry must be the SAME string. Bump them together, every release.
|
|
9
|
+
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
17
|
+
|
|
18
|
+
function readJson(relativePath) {
|
|
19
|
+
return JSON.parse(readFileSync(join(ROOT, relativePath), 'utf8').replace(/^/, ''));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('npm, both plugin.json, and every marketplace entry share one version', () => {
|
|
23
|
+
const versions = {
|
|
24
|
+
'package.json': readJson('package.json').version,
|
|
25
|
+
'plugins/gates/.claude-plugin/plugin.json': readJson(
|
|
26
|
+
'plugins/gates/.claude-plugin/plugin.json',
|
|
27
|
+
).version,
|
|
28
|
+
'plugins/tasks/.claude-plugin/plugin.json': readJson(
|
|
29
|
+
'plugins/tasks/.claude-plugin/plugin.json',
|
|
30
|
+
).version,
|
|
31
|
+
};
|
|
32
|
+
for (const plugin of readJson('.claude-plugin/marketplace.json').plugins) {
|
|
33
|
+
versions[`marketplace.json:${plugin.name}`] = plugin.version;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const distinct = [...new Set(Object.values(versions))];
|
|
37
|
+
assert.equal(
|
|
38
|
+
distinct.length,
|
|
39
|
+
1,
|
|
40
|
+
`all versions must match; found ${JSON.stringify(versions)}. ` +
|
|
41
|
+
'A plugin.json/marketplace version behind the npm version means Claude Code keeps ' +
|
|
42
|
+
'serving the OLD cached plugin code on reinstall. Bump them together.',
|
|
43
|
+
);
|
|
44
|
+
});
|
package/cli/config.mjs
CHANGED
|
@@ -1,113 +1,124 @@
|
|
|
1
|
-
// Where the selection is persisted and how it merges with what is already there.
|
|
2
|
-
//
|
|
3
|
-
// Scope "project": `<project root>/.ai/config.json` — the file the hooks read
|
|
4
|
-
// per project. Other keys the project already has (autoCommit, etc.) are kept.
|
|
5
|
-
// Scope "global": `~/.claude/claude-gates/config.json` — the fallback the hooks
|
|
6
|
-
// use when a project has no answer of its own.
|
|
7
|
-
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
-
import { dirname, join } from 'node:path';
|
|
10
|
-
import { findUpSync } from 'find-up';
|
|
11
|
-
import {
|
|
12
|
-
CLAUDE_USER_DIRECTORY,
|
|
13
|
-
CONFIG_FILE,
|
|
14
|
-
GLOBAL_STATE_DIRECTORY,
|
|
15
|
-
HOME_DIRECTORY,
|
|
16
|
-
JSON_INDENT,
|
|
17
|
-
PROJECT_ROOT_MARKERS,
|
|
18
|
-
PROJECT_STATE_DIRECTORY,
|
|
19
|
-
} from './constants.mjs';
|
|
20
|
-
|
|
21
|
-
export const SCOPES = Object.freeze({ GLOBAL: 'global', PROJECT: 'project' });
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Climbs from `startDirectory` to the nearest project marker (`.git`/`.ai`).
|
|
25
|
-
* The home directory (and anything above it) is never a project root: a stray
|
|
26
|
-
* `~/.ai` would otherwise turn every folder under home into "the project" and the
|
|
27
|
-
* config would land in the wrong place silently. `.git` may be a file (worktrees),
|
|
28
|
-
* so the matcher checks existence, not type.
|
|
29
|
-
*/
|
|
30
|
-
export function findProjectRoot(
|
|
31
|
-
startDirectory,
|
|
32
|
-
{ home = HOME_DIRECTORY } = {},
|
|
33
|
-
) {
|
|
34
|
-
if (startDirectory === home) return startDirectory;
|
|
35
|
-
const markerDirectory = findUpSync(
|
|
36
|
-
(directory) => {
|
|
37
|
-
if (directory === home) return findUpSync.stop;
|
|
38
|
-
const hasMarker = PROJECT_ROOT_MARKERS.some((marker) =>
|
|
39
|
-
existsSync(join(directory, marker)),
|
|
40
|
-
);
|
|
41
|
-
return hasMarker ? directory : undefined;
|
|
42
|
-
},
|
|
43
|
-
{ cwd: startDirectory, stopAt: home, type: 'directory' },
|
|
44
|
-
);
|
|
45
|
-
return markerDirectory ?? startDirectory;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function configPathFor(
|
|
49
|
-
scope,
|
|
50
|
-
{ cwd = process.cwd(), home = HOME_DIRECTORY } = {},
|
|
51
|
-
) {
|
|
52
|
-
if (scope === SCOPES.GLOBAL)
|
|
53
|
-
return join(
|
|
54
|
-
home,
|
|
55
|
-
CLAUDE_USER_DIRECTORY,
|
|
56
|
-
GLOBAL_STATE_DIRECTORY,
|
|
57
|
-
CONFIG_FILE,
|
|
58
|
-
);
|
|
59
|
-
if (scope === SCOPES.PROJECT)
|
|
60
|
-
return join(
|
|
61
|
-
findProjectRoot(cwd, { home }),
|
|
62
|
-
PROJECT_STATE_DIRECTORY,
|
|
63
|
-
CONFIG_FILE,
|
|
64
|
-
);
|
|
65
|
-
throw new Error(`Unknown scope: ${scope}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
*
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
1
|
+
// Where the selection is persisted and how it merges with what is already there.
|
|
2
|
+
//
|
|
3
|
+
// Scope "project": `<project root>/.ai/config.json` — the file the hooks read
|
|
4
|
+
// per project. Other keys the project already has (autoCommit, etc.) are kept.
|
|
5
|
+
// Scope "global": `~/.claude/claude-gates/config.json` — the fallback the hooks
|
|
6
|
+
// use when a project has no answer of its own.
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { findUpSync } from 'find-up';
|
|
11
|
+
import {
|
|
12
|
+
CLAUDE_USER_DIRECTORY,
|
|
13
|
+
CONFIG_FILE,
|
|
14
|
+
GLOBAL_STATE_DIRECTORY,
|
|
15
|
+
HOME_DIRECTORY,
|
|
16
|
+
JSON_INDENT,
|
|
17
|
+
PROJECT_ROOT_MARKERS,
|
|
18
|
+
PROJECT_STATE_DIRECTORY,
|
|
19
|
+
} from './constants.mjs';
|
|
20
|
+
|
|
21
|
+
export const SCOPES = Object.freeze({ GLOBAL: 'global', PROJECT: 'project' });
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Climbs from `startDirectory` to the nearest project marker (`.git`/`.ai`).
|
|
25
|
+
* The home directory (and anything above it) is never a project root: a stray
|
|
26
|
+
* `~/.ai` would otherwise turn every folder under home into "the project" and the
|
|
27
|
+
* config would land in the wrong place silently. `.git` may be a file (worktrees),
|
|
28
|
+
* so the matcher checks existence, not type.
|
|
29
|
+
*/
|
|
30
|
+
export function findProjectRoot(
|
|
31
|
+
startDirectory,
|
|
32
|
+
{ home = HOME_DIRECTORY } = {},
|
|
33
|
+
) {
|
|
34
|
+
if (startDirectory === home) return startDirectory;
|
|
35
|
+
const markerDirectory = findUpSync(
|
|
36
|
+
(directory) => {
|
|
37
|
+
if (directory === home) return findUpSync.stop;
|
|
38
|
+
const hasMarker = PROJECT_ROOT_MARKERS.some((marker) =>
|
|
39
|
+
existsSync(join(directory, marker)),
|
|
40
|
+
);
|
|
41
|
+
return hasMarker ? directory : undefined;
|
|
42
|
+
},
|
|
43
|
+
{ cwd: startDirectory, stopAt: home, type: 'directory' },
|
|
44
|
+
);
|
|
45
|
+
return markerDirectory ?? startDirectory;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function configPathFor(
|
|
49
|
+
scope,
|
|
50
|
+
{ cwd = process.cwd(), home = HOME_DIRECTORY } = {},
|
|
51
|
+
) {
|
|
52
|
+
if (scope === SCOPES.GLOBAL)
|
|
53
|
+
return join(
|
|
54
|
+
home,
|
|
55
|
+
CLAUDE_USER_DIRECTORY,
|
|
56
|
+
GLOBAL_STATE_DIRECTORY,
|
|
57
|
+
CONFIG_FILE,
|
|
58
|
+
);
|
|
59
|
+
if (scope === SCOPES.PROJECT)
|
|
60
|
+
return join(
|
|
61
|
+
findProjectRoot(cwd, { home }),
|
|
62
|
+
PROJECT_STATE_DIRECTORY,
|
|
63
|
+
CONFIG_FILE,
|
|
64
|
+
);
|
|
65
|
+
throw new Error(`Unknown scope: ${scope}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A leading UTF-8 BOM (EF BB BF, decoded as U+FEFF) is not stripped by readFileSync('utf8'),
|
|
69
|
+
// and JSON.parse rejects a string that starts with it. Without stripping it, a config saved
|
|
70
|
+
// by a BOM-adding editor or `PowerShell Set-Content -Encoding utf8` would read back as
|
|
71
|
+
// `corrupt: true` — a valid, user-edited config (with its gate overrides) mistaken for
|
|
72
|
+
// unreadable. init.mjs bails loudly on `corrupt`, but a caller relying on `data` alone (as
|
|
73
|
+
// `mergeConfig` does) would otherwise merge onto `{}` and silently drop every existing gate
|
|
74
|
+
// override the user made. This is the same failure mode fixed in the gates' own config.mjs.
|
|
75
|
+
function stripBom(text) {
|
|
76
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function readConfig(path) {
|
|
80
|
+
if (!existsSync(path)) return { exists: false, data: {}, corrupt: false };
|
|
81
|
+
try {
|
|
82
|
+
return {
|
|
83
|
+
exists: true,
|
|
84
|
+
data: JSON.parse(stripBom(readFileSync(path, 'utf8'))),
|
|
85
|
+
corrupt: false,
|
|
86
|
+
};
|
|
87
|
+
} catch {
|
|
88
|
+
return { exists: true, data: {}, corrupt: true };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Merges one gate's new value onto whatever the project already had, preserving the user's
|
|
94
|
+
* edits. A gate with params arrives as `{ enabled, ...defaults }`; if the user already
|
|
95
|
+
* tuned those params, their values win and only `enabled` follows the new selection. A
|
|
96
|
+
* paramless gate is a plain boolean. This is what keeps a re-run of `init` additive: it
|
|
97
|
+
* never overwrites a whitelist or pattern list the user changed by hand.
|
|
98
|
+
*/
|
|
99
|
+
function mergeGate(existingValue, newValue) {
|
|
100
|
+
if (typeof newValue === 'boolean') return newValue;
|
|
101
|
+
if (existingValue && typeof existingValue === 'object') {
|
|
102
|
+
return { ...newValue, ...existingValue, enabled: newValue.enabled };
|
|
103
|
+
}
|
|
104
|
+
return newValue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Merges the new selection into an existing config without touching unrelated keys. Gates
|
|
109
|
+
* are merged key by key: a gate absent from the new map keeps its old value (a registry
|
|
110
|
+
* that dropped a gate must not silently flip it), and a gate the user configured keeps its
|
|
111
|
+
* params (see mergeGate).
|
|
112
|
+
*/
|
|
113
|
+
export function mergeConfig(existing, { adopted, gates, gateVersion }) {
|
|
114
|
+
const mergedGates = { ...(existing.gates ?? {}) };
|
|
115
|
+
for (const [key, value] of Object.entries(gates)) {
|
|
116
|
+
mergedGates[key] = mergeGate(mergedGates[key], value);
|
|
117
|
+
}
|
|
118
|
+
return { ...existing, adopted, gateVersion, gates: mergedGates };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function writeConfig(path, data) {
|
|
122
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
123
|
+
writeFileSync(path, `${JSON.stringify(data, null, JSON_INDENT)}\n`, 'utf8');
|
|
124
|
+
}
|
package/cli/index.mjs
CHANGED
|
@@ -1,97 +1,100 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Entry point (commander). Commands:
|
|
3
|
-
// init interactive (or flag-driven) selection of gates, per project or globally
|
|
4
|
-
// registry --check validates registry.json; --list prints the catalog
|
|
5
|
-
|
|
6
|
-
import { readFileSync } from 'node:fs';
|
|
7
|
-
import { join } from 'node:path';
|
|
8
|
-
import { Command } from 'commander';
|
|
9
|
-
import {
|
|
10
|
-
EXIT_CODE,
|
|
11
|
-
LIST_SEPARATOR,
|
|
12
|
-
REGISTRY_PATH,
|
|
13
|
-
REPOSITORY_ROOT,
|
|
14
|
-
} from './constants.mjs';
|
|
15
|
-
import { loadRegistry, validateRegistry } from './registry.mjs';
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
process.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.
|
|
58
|
-
.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.
|
|
63
|
-
.
|
|
64
|
-
.option('--
|
|
65
|
-
.option('--
|
|
66
|
-
.option('--
|
|
67
|
-
.option('--
|
|
68
|
-
.option(
|
|
69
|
-
|
|
70
|
-
'
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
'
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.option('--
|
|
80
|
-
.option('--
|
|
81
|
-
.
|
|
82
|
-
|
|
83
|
-
await
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
.
|
|
89
|
-
.
|
|
90
|
-
.option('--
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
if (options.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Entry point (commander). Commands:
|
|
3
|
+
// init interactive (or flag-driven) selection of gates, per project or globally
|
|
4
|
+
// registry --check validates registry.json; --list prints the catalog
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
import {
|
|
10
|
+
EXIT_CODE,
|
|
11
|
+
LIST_SEPARATOR,
|
|
12
|
+
REGISTRY_PATH,
|
|
13
|
+
REPOSITORY_ROOT,
|
|
14
|
+
} from './constants.mjs';
|
|
15
|
+
import { loadRegistry, validateRegistry } from './registry.mjs';
|
|
16
|
+
import { registerTaskCommand } from './task.mjs';
|
|
17
|
+
|
|
18
|
+
const packageManifest = JSON.parse(
|
|
19
|
+
readFileSync(join(REPOSITORY_ROOT, 'package.json'), 'utf8'),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
function fail(message) {
|
|
23
|
+
process.stderr.write(`${message}\n`);
|
|
24
|
+
process.exit(EXIT_CODE.FAILURE);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function splitList(value) {
|
|
28
|
+
return value
|
|
29
|
+
.split(LIST_SEPARATOR)
|
|
30
|
+
.map((item) => item.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function registryCheck() {
|
|
35
|
+
const problems = validateRegistry(
|
|
36
|
+
JSON.parse(readFileSync(REGISTRY_PATH, 'utf8')),
|
|
37
|
+
);
|
|
38
|
+
if (problems.length > 0) {
|
|
39
|
+
fail(`registry.json is invalid:\n- ${problems.join('\n- ')}`);
|
|
40
|
+
}
|
|
41
|
+
process.stdout.write('registry.json OK\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function registryList() {
|
|
45
|
+
const registry = loadRegistry();
|
|
46
|
+
for (const family of registry.families) {
|
|
47
|
+
process.stdout.write(`${family.name} (${family.id})\n`);
|
|
48
|
+
for (const gate of family.gates) {
|
|
49
|
+
const marker = gate.default ? '*' : ' ';
|
|
50
|
+
process.stdout.write(` ${marker} ${gate.id} ${gate.configKey}\n`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
process.stdout.write('\n* = recommended default\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const program = new Command()
|
|
57
|
+
.name(packageManifest.name)
|
|
58
|
+
.description(packageManifest.description)
|
|
59
|
+
.version(packageManifest.version);
|
|
60
|
+
|
|
61
|
+
program
|
|
62
|
+
.command('init')
|
|
63
|
+
.description('Choose which gates to adopt and write the config.')
|
|
64
|
+
.option('--project', 'apply to this project (<root>/.ai/config.json)')
|
|
65
|
+
.option('--global', 'apply globally (~/.claude/claude-gates/config.json)')
|
|
66
|
+
.option('--defaults', 'enable the recommended defaults')
|
|
67
|
+
.option('--all', 'enable every gate')
|
|
68
|
+
.option('--none', "record a 'no' so you are not asked again")
|
|
69
|
+
.option(
|
|
70
|
+
'--families <ids>',
|
|
71
|
+
'enable whole families, comma-separated',
|
|
72
|
+
splitList,
|
|
73
|
+
)
|
|
74
|
+
.option(
|
|
75
|
+
'--gates <ids>',
|
|
76
|
+
'enable individual gates, comma-separated',
|
|
77
|
+
splitList,
|
|
78
|
+
)
|
|
79
|
+
.option('-y, --yes', 'never prompt; use flags and defaults')
|
|
80
|
+
.option('--no-install', 'write the config but do not install the plugin')
|
|
81
|
+
.option('--dry-run', 'show the selection without writing')
|
|
82
|
+
.action(async (options) => {
|
|
83
|
+
const { runInit } = await import('./init.mjs');
|
|
84
|
+
await runInit(options);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
program
|
|
88
|
+
.command('registry')
|
|
89
|
+
.description('Inspect registry.json.')
|
|
90
|
+
.option('--check', 'validate the registry')
|
|
91
|
+
.option('--list', 'print families and gates')
|
|
92
|
+
.action((options) => {
|
|
93
|
+
if (options.check) return registryCheck();
|
|
94
|
+
if (options.list) return registryList();
|
|
95
|
+
return fail('registry needs --check or --list');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
registerTaskCommand(program);
|
|
99
|
+
|
|
100
|
+
program.parseAsync(process.argv).catch((error) => fail(error.message));
|