clckernel 1.2.13 → 1.2.18
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/.agents/skills/clckernel_cli/SKILL.md +46 -0
- package/.agents/skills/clckernel_cli/reference/audit.md +16 -0
- package/.agents/skills/clckernel_cli/reference/commit.md +23 -0
- package/.agents/skills/clckernel_cli/reference/doctor.md +10 -0
- package/.agents/skills/clckernel_cli/reference/feature.md +38 -0
- package/.agents/skills/clckernel_cli/reference/fix.md +24 -0
- package/.agents/skills/clckernel_cli/reference/guard.md +23 -0
- package/.agents/skills/clckernel_cli/reference/phase.md +18 -0
- package/.agents/skills/clckernel_cli/reference/start.md +28 -0
- package/.agents/skills/clckernel_cli/reference/test.md +22 -0
- package/.agents/skills/clckernel_cli/reference/update.md +33 -0
- package/README.es.md +86 -0
- package/README.md +86 -0
- package/package.json +2 -1
- package/src/adapters/agent-os.js +11 -11
- package/src/catalog.js +45 -29
- package/src/config.js +1 -1
- package/src/generator.js +86 -22
- package/src/index.js +92 -7
- package/tools/audit.js +197 -38
- package/tools/audit.py +151 -25
- package/tools/check_custom.js +1 -1
- package/tools/check_hitl.js +16 -4
- /package/{src → tools}/yaml.js +0 -0
package/src/catalog.js
CHANGED
|
@@ -28,147 +28,152 @@ const SAFEGUARD_CATALOG = [
|
|
|
28
28
|
{ name: 'scan_secrets', labels: ['scan_secrets', 'scan_secrets.js', 'scan_secrets.py'],
|
|
29
29
|
checks: 'Secret & private-key leak scan', verify: ['verify_secret_leaks', 'verify_no_hardcoded_secrets'],
|
|
30
30
|
applies_when: 'always', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
31
|
-
phase: 'audit', file: 'scan_secrets.js+scan_secrets.py', manifest: true, planned: false },
|
|
31
|
+
phase: 'audit', file: 'scan_secrets.js+scan_secrets.py', manifest: true, planned: false, lens: 'security' },
|
|
32
32
|
|
|
33
33
|
{ name: 'check_architecture', labels: ['check_architecture', 'check_architecture.js', 'check_architecture.py'],
|
|
34
34
|
checks: 'Clean Architecture layer / server-component boundaries', verify: ['verify_architecture_layers', 'verify_server_component_boundaries'],
|
|
35
35
|
applies_when: 'frontend,backend', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
36
|
-
phase: 'audit', file: 'check_architecture.js+check_architecture.py', manifest: true, planned: false },
|
|
36
|
+
phase: 'audit', file: 'check_architecture.js+check_architecture.py', manifest: true, planned: false, lens: 'architecture' },
|
|
37
37
|
|
|
38
38
|
{ name: 'check_custom', labels: ['check_custom', 'check_custom.js', 'check_custom.py'],
|
|
39
39
|
checks: 'User-defined regex rules from `.clc-forge.yml`', verify: ['verify_custom_rules', 'verify_user_defined_regex_rules'],
|
|
40
40
|
applies_when: 'always', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
41
|
-
phase: 'audit', file: 'check_custom.js+check_custom.py', manifest: true, planned: false },
|
|
41
|
+
phase: 'audit', file: 'check_custom.js+check_custom.py', manifest: true, planned: false, lens: 'architecture' },
|
|
42
42
|
|
|
43
43
|
{ name: 'check_a11y', labels: ['check_a11y', 'check_a11y.js'],
|
|
44
44
|
checks: 'ARIA + alt accessibility', verify: ['verify_a11y', 'verify_accessibility'],
|
|
45
45
|
applies_when: 'frontend', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
46
|
-
phase: 'audit', file: 'check_a11y.js', manifest: true, planned: false },
|
|
46
|
+
phase: 'audit', file: 'check_a11y.js', manifest: true, planned: false, lens: 'correctness' },
|
|
47
47
|
|
|
48
48
|
{ name: 'check_ui_reuse', labels: ['check_ui_reuse', 'check_ui_reuse.js'],
|
|
49
49
|
checks: 'UI primitive reuse from components/ui/', verify: ['verify_ui_reuse', 'verify_component_reuse'],
|
|
50
50
|
applies_when: 'frontend', severity: 'warning', gate_mode: 'advisory', config_keys: ['scope', 'exclude_paths'],
|
|
51
|
-
phase: 'audit', file: 'check_ui_reuse.js', manifest: true, planned: false },
|
|
51
|
+
phase: 'audit', file: 'check_ui_reuse.js', manifest: true, planned: false, lens: 'architecture' },
|
|
52
52
|
|
|
53
53
|
{ name: 'check_performance', labels: ['check_performance', 'check_performance.js'],
|
|
54
54
|
checks: 'next/image + tree-shaking', verify: ['verify_performance', 'verify_image_optimization'],
|
|
55
55
|
applies_when: 'frontend', severity: 'warning', gate_mode: 'advisory', config_keys: ['scope', 'exclude_paths'],
|
|
56
|
-
phase: 'audit', file: 'check_performance.js', manifest: true, planned: false },
|
|
56
|
+
phase: 'audit', file: 'check_performance.js', manifest: true, planned: false, lens: 'performance' },
|
|
57
57
|
|
|
58
58
|
{ name: 'check_responsive', labels: ['check_responsive', 'check_responsive.js'],
|
|
59
59
|
checks: 'Mobile-first responsiveness, fixed-width overflow & touch target size', verify: ['verify_responsive', 'verify_mobile_adaptability'],
|
|
60
60
|
applies_when: 'frontend', severity: 'warning', gate_mode: 'advisory', config_keys: ['scope', 'exclude_paths'],
|
|
61
|
-
phase: 'audit', file: 'check_responsive.js', manifest: true, planned: false },
|
|
61
|
+
phase: 'audit', file: 'check_responsive.js', manifest: true, planned: false, lens: 'performance' },
|
|
62
62
|
|
|
63
63
|
{ name: 'check_seo', labels: ['check_seo', 'check_seo.js'],
|
|
64
64
|
checks: 'SEO metadata, OpenGraph, GEO tags, semantic HTML & LCP priority', verify: ['verify_seo', 'verify_geo_metadata', 'verify_core_web_vitals'],
|
|
65
65
|
applies_when: 'frontend', severity: 'warning', gate_mode: 'advisory', config_keys: ['scope', 'exclude_paths'],
|
|
66
|
-
phase: 'audit', file: 'check_seo.js', manifest: true, planned: false },
|
|
66
|
+
phase: 'audit', file: 'check_seo.js', manifest: true, planned: false, lens: 'performance' },
|
|
67
67
|
|
|
68
68
|
{ name: 'check_storybook', labels: ['check_storybook', 'check_storybook.js'],
|
|
69
69
|
checks: 'Storybook coverage (`.stories.tsx`)', verify: ['verify_storybook_coverage'],
|
|
70
70
|
applies_when: 'frontend', severity: 'warning', gate_mode: 'advisory', config_keys: ['scope', 'exclude_paths'],
|
|
71
|
-
phase: 'audit', file: 'check_storybook.js', manifest: true, planned: false },
|
|
71
|
+
phase: 'audit', file: 'check_storybook.js', manifest: true, planned: false, lens: 'performance' },
|
|
72
72
|
|
|
73
73
|
{ name: 'check_api_contracts', labels: ['check_api_contracts', 'check_api_contracts.js'],
|
|
74
74
|
checks: 'Zod response-schema parsing', verify: ['verify_api_contracts', 'verify_response_schemas'],
|
|
75
75
|
applies_when: 'frontend', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
76
|
-
phase: 'audit', file: 'check_api_contracts.js', manifest: true, planned: false },
|
|
76
|
+
phase: 'audit', file: 'check_api_contracts.js', manifest: true, planned: false, lens: 'correctness' },
|
|
77
77
|
|
|
78
78
|
{ name: 'check_migrations', labels: ['check_migrations', 'check_migrations.py'],
|
|
79
79
|
checks: 'Alembic/Django migration idempotency', verify: ['verify_migration_idempotency'],
|
|
80
80
|
applies_when: 'backend,fastapi,django', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
81
|
-
phase: 'audit', file: 'check_migrations.py', manifest: true, planned: false },
|
|
81
|
+
phase: 'audit', file: 'check_migrations.py', manifest: true, planned: false, lens: 'architecture' },
|
|
82
82
|
|
|
83
83
|
{ name: 'verify_tdd', labels: ['verify_tdd', 'verify_tdd.py'],
|
|
84
84
|
checks: 'Real red→green TDD validation', verify: ['verify_tdd_red_green', 'verify_tdd_evidence'],
|
|
85
85
|
applies_when: 'backend', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
86
|
-
phase: 'audit', file: 'verify_tdd.py', manifest: false, planned: false },
|
|
86
|
+
phase: 'audit', file: 'verify_tdd.py', manifest: false, planned: false, lens: 'correctness' },
|
|
87
87
|
|
|
88
88
|
{ name: 'check_scope', labels: ['check_scope', 'check_scope.py'],
|
|
89
89
|
checks: 'git-diff vs SDD-authorization scope guardrail', verify: ['verify_scope_authorization', 'verify_sdd_scope'],
|
|
90
90
|
applies_when: 'always', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
91
|
-
phase: 'audit', file: 'check_scope.py', manifest: false, planned: false },
|
|
91
|
+
phase: 'audit', file: 'check_scope.py', manifest: false, planned: false, lens: 'correctness' },
|
|
92
92
|
|
|
93
93
|
{ name: 'celery_guard', labels: ['celery_guard', 'celery_guard.py'],
|
|
94
94
|
checks: 'Broker URL exposure, ignore_result, result backend', verify: ['verify_celery_broker_config'],
|
|
95
95
|
applies_when: 'celery', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
96
|
-
phase: 'audit', file: 'celery_guard.py', manifest: true, planned: false },
|
|
96
|
+
phase: 'audit', file: 'celery_guard.py', manifest: true, planned: false, lens: 'security' },
|
|
97
97
|
|
|
98
98
|
{ name: 'redis_guard', labels: ['redis_guard', 'redis_guard.py'],
|
|
99
99
|
checks: 'decode_responses, connect timeout, hardcoded URLs', verify: ['verify_redis_config'],
|
|
100
100
|
applies_when: 'redis', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
101
|
-
phase: 'audit', file: 'redis_guard.py', manifest: true, planned: false },
|
|
101
|
+
phase: 'audit', file: 'redis_guard.py', manifest: true, planned: false, lens: 'security' },
|
|
102
102
|
|
|
103
103
|
{ name: 'postgres_guard', labels: ['postgres_guard', 'postgres_guard.py'],
|
|
104
104
|
checks: 'Raw SQL string formatting, hstore misuse', verify: ['verify_postgres_sql_usage'],
|
|
105
105
|
applies_when: 'postgresql', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
106
|
-
phase: 'audit', file: 'postgres_guard.py', manifest: true, planned: false },
|
|
106
|
+
phase: 'audit', file: 'postgres_guard.py', manifest: true, planned: false, lens: 'security' },
|
|
107
107
|
|
|
108
108
|
{ name: 'docker_guard', labels: ['docker_guard', 'docker_guard.py'],
|
|
109
109
|
checks: 'Non-root USER, ENV secrets, missing HEALTHCHECK', verify: ['verify_dockerfile_security'],
|
|
110
110
|
applies_when: 'docker', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
111
|
-
phase: 'audit', file: 'docker_guard.py', manifest: true, planned: false },
|
|
111
|
+
phase: 'audit', file: 'docker_guard.py', manifest: true, planned: false, lens: 'security' },
|
|
112
112
|
|
|
113
113
|
{ name: 'check_symlinks', labels: ['check_symlinks', 'check_symlinks.js'],
|
|
114
114
|
checks: 'AI IDE symlinks and rule mirror verification', verify: ['verify_symlinks', 'verify_rule_mirrors'],
|
|
115
115
|
applies_when: 'agent-os', severity: 'error', gate_mode: 'hard', config_keys: ['scope'],
|
|
116
|
-
phase: 'audit', file: 'check_symlinks.js', manifest: true, planned: false },
|
|
116
|
+
phase: 'audit', file: 'check_symlinks.js', manifest: true, planned: false, lens: 'architecture' },
|
|
117
117
|
|
|
118
118
|
{ name: 'check_hitl', labels: ['check_hitl', 'check_hitl.js'],
|
|
119
119
|
checks: 'Human-in-the-Loop review checkpoint validator in subagent skills', verify: ['verify_hitl', 'verify_human_review_checkpoints'],
|
|
120
120
|
applies_when: 'agent-os', severity: 'error', gate_mode: 'hard', config_keys: ['scope'],
|
|
121
|
-
phase: 'audit', file: 'check_hitl.js', manifest: true, planned: false },
|
|
121
|
+
phase: 'audit', file: 'check_hitl.js', manifest: true, planned: false, lens: 'architecture' },
|
|
122
122
|
|
|
123
123
|
{ name: 'check_domain_data', labels: ['check_domain_data', 'check_domain_data.js'],
|
|
124
124
|
checks: 'Domain deliverables schema and clean state integrity', verify: ['verify_domain_data', 'verify_clean_state_metrics'],
|
|
125
125
|
applies_when: 'agent-os', severity: 'error', gate_mode: 'hard', config_keys: ['scope'],
|
|
126
|
-
phase: 'audit', file: 'check_domain_data.js', manifest: true, planned: false },
|
|
126
|
+
phase: 'audit', file: 'check_domain_data.js', manifest: true, planned: false, lens: 'architecture' },
|
|
127
|
+
|
|
128
|
+
{ name: 'check_test_calendar_sync', labels: ['check_test_calendar_sync', 'check_test_calendar_sync.js'],
|
|
129
|
+
checks: 'Test calendar harness synchronization and parity check', verify: ['verify_calendar_sync', 'verify_test_calendar_sync'],
|
|
130
|
+
applies_when: 'agent-os', severity: 'error', gate_mode: 'hard', config_keys: ['scope'],
|
|
131
|
+
phase: 'audit', file: 'check_test_calendar_sync.js', manifest: true, planned: false, lens: 'performance' },
|
|
127
132
|
|
|
128
133
|
{ name: 'ruff', labels: ['ruff'],
|
|
129
134
|
checks: 'Python AST linter (external CLI)', verify: ['verify_python_lint'],
|
|
130
135
|
applies_when: 'backend', severity: 'warning', gate_mode: 'advisory', config_keys: [],
|
|
131
|
-
phase: 'test', file: null, manifest: true, planned: false },
|
|
136
|
+
phase: 'test', file: null, manifest: true, planned: false, lens: 'correctness' },
|
|
132
137
|
|
|
133
138
|
{ name: 'pytest', labels: ['pytest'],
|
|
134
139
|
checks: 'Test runner (external CLI)', verify: ['verify_test_coverage'],
|
|
135
140
|
applies_when: 'backend', severity: 'warning', gate_mode: 'advisory', config_keys: [],
|
|
136
|
-
phase: 'test', file: null, manifest: true, planned: false },
|
|
141
|
+
phase: 'test', file: null, manifest: true, planned: false, lens: 'correctness' },
|
|
137
142
|
|
|
138
143
|
{ name: 'tsc', labels: ['tsc'],
|
|
139
144
|
checks: 'TypeScript typecheck (external)', verify: ['verify_typescript_types'],
|
|
140
145
|
applies_when: 'frontend', severity: 'error', gate_mode: 'hard', config_keys: [],
|
|
141
|
-
phase: 'test', file: null, manifest: true, planned: false },
|
|
146
|
+
phase: 'test', file: null, manifest: true, planned: false, lens: 'correctness' },
|
|
142
147
|
|
|
143
148
|
{ name: 'lint', labels: ['lint', 'lint-staged'],
|
|
144
149
|
checks: 'lint-staged (external)', verify: ['verify_lint_staged'],
|
|
145
150
|
applies_when: 'frontend', severity: 'warning', gate_mode: 'advisory', config_keys: [],
|
|
146
|
-
phase: 'test', file: null, manifest: true, planned: false },
|
|
151
|
+
phase: 'test', file: null, manifest: true, planned: false, lens: 'correctness' },
|
|
147
152
|
|
|
148
153
|
{ name: 'graphify', labels: ['graphify'],
|
|
149
154
|
checks: 'Memory graph (external, graceful-skip)', verify: ['verify_memory_graph'],
|
|
150
155
|
applies_when: 'always', severity: 'warning', gate_mode: 'advisory', config_keys: [],
|
|
151
|
-
phase: 'review', file: null, manifest: false, planned: false },
|
|
156
|
+
phase: 'review', file: null, manifest: false, planned: false, lens: 'architecture' },
|
|
152
157
|
|
|
153
158
|
{ name: 'gentle_ai_review', labels: ['gentle_ai_review', 'gentle-ai'],
|
|
154
159
|
checks: 'Native bounded review gate (external, graceful-skip)', verify: ['verify_rdd_review_gate'],
|
|
155
160
|
applies_when: 'always', severity: 'error', gate_mode: 'hard', config_keys: [],
|
|
156
|
-
phase: 'review', file: null, manifest: false, planned: false },
|
|
161
|
+
phase: 'review', file: null, manifest: false, planned: false, lens: 'architecture' },
|
|
157
162
|
|
|
158
163
|
{ name: 'check_openapi_drift', labels: ['check_openapi_drift', 'check_openapi_drift.py'],
|
|
159
164
|
checks: 'FastAPI OpenAPI schema drift vs app routes', verify: ['verify_openapi_drift'],
|
|
160
165
|
applies_when: 'fastapi', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
161
|
-
phase: 'audit', file: 'check_openapi_drift.py', manifest: true, planned: true },
|
|
166
|
+
phase: 'audit', file: 'check_openapi_drift.py', manifest: true, planned: true, lens: 'correctness' },
|
|
162
167
|
|
|
163
168
|
{ name: 'check_db_efficiency', labels: ['check_db_efficiency', 'check_db_efficiency.py'],
|
|
164
169
|
checks: 'ORM N+1 / select_related / prefetch_related', verify: ['verify_db_efficiency', 'verify_no_n_plus_1'],
|
|
165
170
|
applies_when: 'django|sqlalchemy', severity: 'error', gate_mode: 'hard', config_keys: ['scope', 'exclude_paths'],
|
|
166
|
-
phase: 'audit', file: 'check_db_efficiency.py', manifest: true, planned: false },
|
|
171
|
+
phase: 'audit', file: 'check_db_efficiency.py', manifest: true, planned: false, lens: 'performance' },
|
|
167
172
|
|
|
168
173
|
{ name: 'check_observability', labels: ['check_observability', 'check_observability.py'],
|
|
169
174
|
checks: 'structlog structured logging; bare print/logger ban', verify: ['verify_structured_logging'],
|
|
170
175
|
applies_when: 'backend', severity: 'warning', gate_mode: 'advisory', config_keys: ['scope', 'exclude_paths'],
|
|
171
|
-
phase: 'audit', file: 'check_observability.py', manifest: true, planned: true },
|
|
176
|
+
phase: 'audit', file: 'check_observability.py', manifest: true, planned: true, lens: 'performance' },
|
|
172
177
|
];
|
|
173
178
|
|
|
174
179
|
// Freeze the registry so config validation and generation can rely on it.
|
|
@@ -190,6 +195,9 @@ const VALID_APPLIES_WHEN = [
|
|
|
190
195
|
'agent-os',
|
|
191
196
|
];
|
|
192
197
|
|
|
198
|
+
/** Canonical 4 RDD Lenses */
|
|
199
|
+
const VALID_LENSES = ['correctness', 'security', 'architecture', 'performance'];
|
|
200
|
+
|
|
193
201
|
/** Normalize an input label for catalog lookup. */
|
|
194
202
|
function normalizeLabel(name) {
|
|
195
203
|
return typeof name === 'string' ? name.trim().toLowerCase() : '';
|
|
@@ -221,13 +229,21 @@ function gateFor(name) {
|
|
|
221
229
|
return entry ? entry.gate_mode : null;
|
|
222
230
|
}
|
|
223
231
|
|
|
232
|
+
/** Canonical RDD review lens for a guard name/label; null when unknown. */
|
|
233
|
+
function lensFor(name) {
|
|
234
|
+
const entry = applyTo(name);
|
|
235
|
+
return entry ? entry.lens : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
224
238
|
module.exports = {
|
|
225
239
|
SAFEGUARD_CATALOG,
|
|
226
240
|
VALID_SEVERITIES,
|
|
227
241
|
VALID_GATE_MODES,
|
|
228
242
|
VALID_PHASES,
|
|
229
243
|
VALID_APPLIES_WHEN,
|
|
244
|
+
VALID_LENSES,
|
|
230
245
|
applyTo,
|
|
231
246
|
severityFor,
|
|
232
247
|
gateFor,
|
|
248
|
+
lensFor,
|
|
233
249
|
};
|
package/src/config.js
CHANGED
package/src/generator.js
CHANGED
|
@@ -8,9 +8,33 @@ const path = require('path');
|
|
|
8
8
|
const { execSync } = require('child_process');
|
|
9
9
|
const { resolveConfig } = require('./config.js');
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Detects whether a Node CLI required by a hook command is actually installed
|
|
13
|
+
* in the target project. Resolving through node_modules/.bin is what `npx` does
|
|
14
|
+
* first, so a missing entry there means `npx` would download it on the fly (or
|
|
15
|
+
* fail inside a commit hook, where the network is not available).
|
|
16
|
+
* @param {string} targetDir - Target project root
|
|
17
|
+
* @param {string} pkg - Package name to look for
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function isCliInstalled(targetDir, pkg) {
|
|
21
|
+
if (!pkg) return false;
|
|
22
|
+
try {
|
|
23
|
+
return fs.existsSync(path.join(targetDir, 'node_modules', '.bin', pkg));
|
|
24
|
+
} catch (e) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
11
29
|
/**
|
|
12
30
|
* Generates a pre-commit hook dynamically from the adapter's tool manifest.
|
|
13
31
|
* Frontend projects use .husky; backend projects use .githooks.
|
|
32
|
+
*
|
|
33
|
+
* External CLIs (language === 'external', e.g. tsc, lint-staged) are only emitted
|
|
34
|
+
* when the target project already depends on them: the generator does not add
|
|
35
|
+
* dependencies, so referencing an uninstalled binary produces a hook that fails
|
|
36
|
+
* on every commit.
|
|
37
|
+
*
|
|
14
38
|
* @param {object} adapter - Adapter with getTools() and projectType
|
|
15
39
|
* @param {string} targetDir - Target project root
|
|
16
40
|
*/
|
|
@@ -23,10 +47,17 @@ function generateHookFromManifest(adapter, targetDir) {
|
|
|
23
47
|
: path.join(targetDir, '.githooks');
|
|
24
48
|
const hookFile = path.join(hookDir, 'pre-commit');
|
|
25
49
|
|
|
50
|
+
// The husky bootstrap only exists when the project ran `husky init`. Source it
|
|
51
|
+
// only in that case, so a generated hook never points at a missing file.
|
|
52
|
+
const huskyBootstrap = path.join(hookDir, '_', 'husky.sh');
|
|
53
|
+
const useHuskyBootstrap = isFront && fs.existsSync(huskyBootstrap);
|
|
54
|
+
|
|
26
55
|
const lines = [];
|
|
27
56
|
if (isFront) {
|
|
28
57
|
lines.push('#!/usr/bin/env sh');
|
|
29
|
-
|
|
58
|
+
if (useHuskyBootstrap) {
|
|
59
|
+
lines.push('. "$(dirname -- "$0")/_/husky.sh"');
|
|
60
|
+
}
|
|
30
61
|
lines.push('');
|
|
31
62
|
} else {
|
|
32
63
|
lines.push('#!/usr/bin/env bash');
|
|
@@ -34,13 +65,26 @@ function generateHookFromManifest(adapter, targetDir) {
|
|
|
34
65
|
lines.push('');
|
|
35
66
|
}
|
|
36
67
|
|
|
68
|
+
const skipped = [];
|
|
37
69
|
tools.forEach(tool => {
|
|
38
70
|
if (!tool.command) return;
|
|
71
|
+
if (tool.language === 'external') {
|
|
72
|
+
if (!isCliInstalled(targetDir, tool.extCLI)) {
|
|
73
|
+
skipped.push(tool.extCLI || tool.command);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
39
77
|
lines.push(tool.command);
|
|
40
78
|
});
|
|
41
79
|
|
|
80
|
+
if (skipped.length > 0) {
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(`# Not run here (not a dependency of this project): ${skipped.join(', ')}`);
|
|
83
|
+
lines.push('# Install it and re-run `clckernel update` to enable it.');
|
|
84
|
+
}
|
|
85
|
+
|
|
42
86
|
fs.mkdirSync(hookDir, { recursive: true });
|
|
43
|
-
fs.writeFileSync(hookFile, lines.join('\n'), 'utf-8');
|
|
87
|
+
fs.writeFileSync(hookFile, lines.join('\n') + '\n', 'utf-8');
|
|
44
88
|
try { fs.chmodSync(hookFile, '755'); } catch (e) {}
|
|
45
89
|
|
|
46
90
|
if (!isFront) {
|
|
@@ -62,6 +106,10 @@ function installToolsFromManifest(targetDir, manifest, config) {
|
|
|
62
106
|
const sourceToolsDir = path.join(__dirname, '..', 'tools');
|
|
63
107
|
const isFront = config.projectType === 'frontend';
|
|
64
108
|
|
|
109
|
+
// tools/ must exist before anything is copied into it. Provisioning a project
|
|
110
|
+
// that has no tools/ yet used to throw ENOENT on the first copyFileSync.
|
|
111
|
+
fs.mkdirSync(path.join(targetDir, 'tools'), { recursive: true });
|
|
112
|
+
|
|
65
113
|
// Validate descriptors: skip + warn tools missing required fields
|
|
66
114
|
const validTools = manifest.filter(tool => {
|
|
67
115
|
if (!tool.name || !tool.language || !tool.command) {
|
|
@@ -72,7 +120,8 @@ function installToolsFromManifest(targetDir, manifest, config) {
|
|
|
72
120
|
});
|
|
73
121
|
|
|
74
122
|
// Always copy the audit orchestrator for the right language
|
|
75
|
-
const
|
|
123
|
+
const isNodeStack = config.projectType === 'frontend' || config.projectType === 'agent-os';
|
|
124
|
+
const orchestrator = isNodeStack ? 'audit.js' : 'audit.py';
|
|
76
125
|
const orchestratorSrc = path.join(sourceToolsDir, orchestrator);
|
|
77
126
|
const orchestratorDest = path.join(targetDir, 'tools', orchestrator);
|
|
78
127
|
if (fs.existsSync(orchestratorSrc)) {
|
|
@@ -93,25 +142,29 @@ function installToolsFromManifest(targetDir, manifest, config) {
|
|
|
93
142
|
}
|
|
94
143
|
});
|
|
95
144
|
|
|
96
|
-
// check_custom.js
|
|
97
|
-
//
|
|
145
|
+
// check_custom.js parses the project YAML config, so the generated project
|
|
146
|
+
// must ship the parser alongside it.
|
|
98
147
|
if (validTools.some(tool => tool.path === 'check_custom.js')) {
|
|
99
|
-
installYamlModule(targetDir
|
|
148
|
+
installYamlModule(targetDir);
|
|
100
149
|
}
|
|
101
150
|
}
|
|
102
151
|
|
|
103
152
|
/**
|
|
104
|
-
* Copies the zero-dependency YAML parser into
|
|
105
|
-
*
|
|
153
|
+
* Copies the zero-dependency YAML parser into the target's tools/ directory.
|
|
154
|
+
*
|
|
155
|
+
* It deliberately does NOT go into the consumer's src/: that directory belongs
|
|
156
|
+
* to the project, and writing there both pollutes it and silently overwrites a
|
|
157
|
+
* consumer file that happens to be named yaml.js. tools/ is the directory the
|
|
158
|
+
* harness provisions and owns.
|
|
159
|
+
*
|
|
106
160
|
* @param {string} targetDir - Target project root
|
|
107
|
-
* @param {string} sourceToolsDir - Bundled tools/ directory of this package
|
|
108
161
|
*/
|
|
109
|
-
function installYamlModule(targetDir
|
|
110
|
-
const yamlSrc = path.join(
|
|
162
|
+
function installYamlModule(targetDir) {
|
|
163
|
+
const yamlSrc = path.join(__dirname, '..', 'tools', 'yaml.js');
|
|
111
164
|
if (!fs.existsSync(yamlSrc)) return;
|
|
112
|
-
const
|
|
113
|
-
fs.mkdirSync(
|
|
114
|
-
fs.copyFileSync(yamlSrc,
|
|
165
|
+
const toolsDest = path.join(targetDir, 'tools');
|
|
166
|
+
fs.mkdirSync(toolsDest, { recursive: true });
|
|
167
|
+
fs.copyFileSync(yamlSrc, path.join(toolsDest, 'yaml.js'));
|
|
115
168
|
}
|
|
116
169
|
|
|
117
170
|
/**
|
|
@@ -139,7 +192,7 @@ function installFallbackTools(targetDir) {
|
|
|
139
192
|
});
|
|
140
193
|
|
|
141
194
|
if (fallbackFiles.includes('check_custom.js')) {
|
|
142
|
-
installYamlModule(targetDir
|
|
195
|
+
installYamlModule(targetDir);
|
|
143
196
|
}
|
|
144
197
|
}
|
|
145
198
|
|
|
@@ -155,9 +208,12 @@ function generateFallbackHook(targetDir, projectType) {
|
|
|
155
208
|
const hookDir = path.join(targetDir, '.husky');
|
|
156
209
|
fs.mkdirSync(hookDir, { recursive: true });
|
|
157
210
|
const hookFile = path.join(hookDir, 'pre-commit');
|
|
211
|
+
// Only source the husky bootstrap when the project actually has it.
|
|
212
|
+
const bootstrap = fs.existsSync(path.join(hookDir, '_', 'husky.sh'))
|
|
213
|
+
? '. "$(dirname -- "$0")/_/husky.sh"\n'
|
|
214
|
+
: '';
|
|
158
215
|
fs.writeFileSync(hookFile, `#!/usr/bin/env sh
|
|
159
|
-
|
|
160
|
-
|
|
216
|
+
${bootstrap}
|
|
161
217
|
node tools/scan_secrets.js || true
|
|
162
218
|
`, 'utf-8');
|
|
163
219
|
try { fs.chmodSync(hookFile, '755'); } catch (e) {}
|
|
@@ -434,19 +490,27 @@ function installAgenticCLI(targetDir) {
|
|
|
434
490
|
symlinks.forEach(link => {
|
|
435
491
|
const linkPath = path.join(targetDir, link);
|
|
436
492
|
const linkDir = path.dirname(linkPath);
|
|
437
|
-
|
|
493
|
+
|
|
438
494
|
if (!fs.existsSync(linkDir)) {
|
|
439
495
|
fs.mkdirSync(linkDir, { recursive: true });
|
|
440
496
|
}
|
|
441
497
|
|
|
442
498
|
const relPath = path.relative(linkDir, path.join(targetDir, sourceFile));
|
|
443
|
-
|
|
499
|
+
|
|
444
500
|
try {
|
|
501
|
+
// Only replace an existing symlink (a stale mirror needs refreshing).
|
|
502
|
+
// A regular file belongs to the consumer: unlinking it would destroy
|
|
503
|
+
// their own rules, so leave it in place instead.
|
|
504
|
+
let existing = null;
|
|
445
505
|
try {
|
|
446
|
-
|
|
447
|
-
fs.unlinkSync(linkPath);
|
|
448
|
-
}
|
|
506
|
+
existing = fs.lstatSync(linkPath);
|
|
449
507
|
} catch (err) {}
|
|
508
|
+
|
|
509
|
+
if (existing) {
|
|
510
|
+
if (!existing.isSymbolicLink()) return;
|
|
511
|
+
fs.unlinkSync(linkPath);
|
|
512
|
+
}
|
|
513
|
+
|
|
450
514
|
fs.symlinkSync(relPath, linkPath, 'file');
|
|
451
515
|
} catch (e) {
|
|
452
516
|
// Ignorar fallos de permisos o symlinks existentes
|
package/src/index.js
CHANGED
|
@@ -9,25 +9,110 @@ const { printBanner, printBox, promptOptions, printSuccess, colors } = require('
|
|
|
9
9
|
const { generateHarness } = require('./generator');
|
|
10
10
|
const { runDoctor } = require('./doctor');
|
|
11
11
|
|
|
12
|
+
/** Commands that only read state. Safe to run against any project. */
|
|
13
|
+
const READ_ONLY_COMMANDS = new Set(['help', '--help', '-h', 'version', '--version', '-v']);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Flags that explicitly opt into the installation path. Everything else is
|
|
17
|
+
* rejected: a destructive default is fixed by making the default non-destructive.
|
|
18
|
+
*/
|
|
19
|
+
const INSTALL_FLAGS = new Set(['--yes', '-y']);
|
|
20
|
+
|
|
21
|
+
const HELP_TEXT = `
|
|
22
|
+
${'clckernel'} — CLC Kernel: The AI Agent Governance Engine
|
|
23
|
+
|
|
24
|
+
USAGE
|
|
25
|
+
clckernel <command> [targetDir]
|
|
26
|
+
|
|
27
|
+
READ-ONLY COMMANDS (never modify your project)
|
|
28
|
+
help, --help, -h Show this help
|
|
29
|
+
version, --version, -v Print the installed version
|
|
30
|
+
doctor [targetDir] Run diagnostics on a project
|
|
31
|
+
update [targetDir] Refresh tools, playbooks and IDE symlinks
|
|
32
|
+
|
|
33
|
+
INSTALLATION (writes to the target project)
|
|
34
|
+
clckernel [targetDir] --yes Provision the governance harness
|
|
35
|
+
|
|
36
|
+
Requires an explicit opt-in. Bare \`clckernel\` runs the interactive
|
|
37
|
+
confirmation flow; with no TTY it refuses to install.
|
|
38
|
+
|
|
39
|
+
EXAMPLES
|
|
40
|
+
clckernel --help
|
|
41
|
+
clckernel doctor .
|
|
42
|
+
clckernel update .
|
|
43
|
+
clckernel . --yes
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Reject any input that is not a recognized command or install flag.
|
|
48
|
+
* The rejected tokens are echoed so the user can see what was not understood.
|
|
49
|
+
* @param {string[]} args - Raw argv (without node/script)
|
|
50
|
+
* @returns {number} Process exit code
|
|
51
|
+
*/
|
|
52
|
+
function rejectUnknownInput(args) {
|
|
53
|
+
const rejected = args.join(' ');
|
|
54
|
+
process.stderr.write(
|
|
55
|
+
`\n❌ Unknown input: ${rejected}\n` +
|
|
56
|
+
' clckernel will not modify your project on unrecognized input.\n' +
|
|
57
|
+
' Run `clckernel --help` to see available commands.\n\n'
|
|
58
|
+
);
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
12
62
|
async function runCli() {
|
|
13
63
|
const args = process.argv.slice(2);
|
|
14
64
|
const command = args[0];
|
|
15
65
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
66
|
+
if (command && READ_ONLY_COMMANDS.has(command)) {
|
|
67
|
+
if (command === 'version' || command === '--version' || command === '-v') {
|
|
68
|
+
const { version } = require('../package.json');
|
|
69
|
+
process.stdout.write(`clckernel v${version}\n`);
|
|
70
|
+
} else {
|
|
71
|
+
process.stdout.write(HELP_TEXT);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (args.some((a) => READ_ONLY_COMMANDS.has(a))) {
|
|
77
|
+
process.exitCode = rejectUnknownInput(args);
|
|
78
|
+
return;
|
|
19
79
|
}
|
|
20
80
|
|
|
21
81
|
if (command === 'doctor' || args.includes('--doctor')) {
|
|
22
|
-
|
|
23
|
-
runDoctor(
|
|
82
|
+
const target = args[1] && args[1] !== '--doctor' ? args[1] : (args[0] === 'doctor' ? null : args[0]);
|
|
83
|
+
runDoctor(target ? path.resolve(target) : process.cwd());
|
|
24
84
|
return;
|
|
25
85
|
}
|
|
26
86
|
|
|
27
87
|
if (command === 'update' || args.includes('--update')) {
|
|
28
|
-
|
|
88
|
+
const target = args[1] && args[1] !== '--update' ? args[1] : (args[0] === 'update' ? null : args[0]);
|
|
29
89
|
const { runUpdate } = require('./updater');
|
|
30
|
-
await runUpdate(
|
|
90
|
+
await runUpdate(target ? path.resolve(target) : process.cwd());
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Everything below is destructive. Only reachable through an explicit command
|
|
95
|
+
// or an explicit install flag.
|
|
96
|
+
const flags = args.filter((a) => a.startsWith('-'));
|
|
97
|
+
const positionals = args.filter((a) => !a.startsWith('-'));
|
|
98
|
+
const unknownFlags = flags.filter((f) => !INSTALL_FLAGS.has(f));
|
|
99
|
+
|
|
100
|
+
if (unknownFlags.length > 0 || positionals.length > 1) {
|
|
101
|
+
process.exitCode = rejectUnknownInput(args);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const targetDir = positionals.length === 1 ? path.resolve(positionals[0]) : process.cwd();
|
|
106
|
+
const explicitInstall = INSTALL_FLAGS.has(command) || flags.length > 0;
|
|
107
|
+
const isInteractive = Boolean(process.stdin.isTTY) && !process.env.CI;
|
|
108
|
+
|
|
109
|
+
if (!explicitInstall && !isInteractive) {
|
|
110
|
+
process.stderr.write(
|
|
111
|
+
'\n❌ Refusing to install without an explicit opt-in.\n' +
|
|
112
|
+
' No TTY detected, so there is no confirmation prompt.\n' +
|
|
113
|
+
' Re-run with `clckernel --yes` to provision, or `clckernel --help`.\n\n'
|
|
114
|
+
);
|
|
115
|
+
process.exitCode = 1;
|
|
31
116
|
return;
|
|
32
117
|
}
|
|
33
118
|
|