clckernel 1.2.19 → 2.0.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.
Files changed (78) hide show
  1. package/.agents/skills/clckernel_cli/SKILL.md +23 -6
  2. package/.agents/skills/clckernel_cli/reference/audit.md +13 -3
  3. package/.agents/skills/clckernel_cli/reference/commit.md +1 -1
  4. package/.agents/skills/clckernel_cli/reference/doctor.md +5 -3
  5. package/.agents/skills/clckernel_cli/reference/feature.md +3 -3
  6. package/.agents/skills/clckernel_cli/reference/guard.md +48 -12
  7. package/.agents/skills/clckernel_cli/reference/phase.md +6 -3
  8. package/.agents/skills/clckernel_cli/reference/start.md +14 -4
  9. package/.agents/skills/clckernel_cli/reference/test.md +1 -1
  10. package/.agents/skills/clckernel_cli/reference/update.md +19 -19
  11. package/CHANGELOG.md +244 -0
  12. package/LICENSE +21 -0
  13. package/README.es.md +280 -51
  14. package/README.md +274 -50
  15. package/bin/cli.js +8 -0
  16. package/package.json +12 -2
  17. package/src/adapters/agent-os.js +161 -84
  18. package/src/adapters/astro.js +8 -6
  19. package/src/adapters/base.js +23 -38
  20. package/src/adapters/django.js +14 -13
  21. package/src/adapters/fastapi.js +16 -15
  22. package/src/adapters/go.js +11 -9
  23. package/src/adapters/laravel.js +12 -10
  24. package/src/adapters/nextjs.js +7 -5
  25. package/src/adapters/rails.js +13 -11
  26. package/src/adapters/rust.js +11 -9
  27. package/src/catalog.js +58 -49
  28. package/src/config.js +38 -240
  29. package/src/detector.js +243 -44
  30. package/src/doctor.js +18 -9
  31. package/src/generator.js +734 -302
  32. package/src/gitignore.js +60 -14
  33. package/src/index.js +257 -18
  34. package/src/io.js +409 -0
  35. package/src/known-hashes.json +309 -0
  36. package/src/onboarding.js +15 -0
  37. package/src/technologies/detector.js +1 -1
  38. package/src/technologies/guards/_shared.js +1 -1
  39. package/src/ui.js +233 -40
  40. package/src/updater.js +271 -38
  41. package/tools/_changes.js +279 -0
  42. package/tools/_changes.py +233 -0
  43. package/tools/_config.js +294 -0
  44. package/tools/_config.py +271 -0
  45. package/tools/_yaml.py +489 -0
  46. package/tools/audit.js +511 -111
  47. package/tools/audit.py +506 -128
  48. package/tools/celery_guard.py +13 -35
  49. package/tools/check_a11y.js +8 -28
  50. package/tools/check_api_contracts.js +13 -39
  51. package/tools/check_architecture.js +35 -44
  52. package/tools/check_architecture.py +172 -105
  53. package/tools/check_custom.js +108 -61
  54. package/tools/check_custom.py +108 -271
  55. package/tools/check_data_files.js +88 -0
  56. package/tools/check_db_efficiency.py +80 -65
  57. package/tools/check_hitl.js +112 -69
  58. package/tools/check_links.js +138 -0
  59. package/tools/check_migrations.py +143 -94
  60. package/tools/check_performance.js +35 -44
  61. package/tools/check_placeholders.js +104 -0
  62. package/tools/check_responsive.js +8 -28
  63. package/tools/check_scope.py +40 -31
  64. package/tools/check_seo.js +8 -28
  65. package/tools/check_skills.js +158 -0
  66. package/tools/check_storybook.js +8 -21
  67. package/tools/check_symlinks.js +40 -22
  68. package/tools/check_ui_reuse.js +17 -29
  69. package/tools/docker_guard.py +19 -41
  70. package/tools/postgres_guard.py +15 -36
  71. package/tools/redis_guard.py +13 -35
  72. package/tools/scan_secrets.js +134 -67
  73. package/tools/scan_secrets.py +163 -72
  74. package/tools/verify_tdd.js +867 -0
  75. package/tools/verify_tdd.py +407 -165
  76. package/tools/yaml.js +404 -132
  77. package/tools/check_domain_data.js +0 -100
  78. package/tools/check_test_calendar_sync.js +0 -111
package/src/ui.js CHANGED
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  const p = require('@clack/prompts');
7
+ const { adapters, buildStackConfig } = require('./detector');
8
+ const { version: PACKAGE_VERSION } = require('../package.json');
7
9
 
8
10
  // ANSI Color Palette
9
11
  const colors = {
@@ -33,7 +35,7 @@ function printBanner() {
33
35
  ╚██████╗███████╗╚██████╗ ██║ ╚██╗███████╗██║ ██║██║ ╚████║███████╗███████╗
34
36
  ╚═════╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚══════╝╚══════╝
35
37
  ${colors.reset}`);
36
- console.log(` ${colors.bgCyan} CLC KERNEL ${colors.reset} ${colors.bold}The AI Agent Governance & Quality Engineering Engine${colors.reset} ${colors.gray}v1.2.6${colors.reset}\n`);
38
+ console.log(` ${colors.bgCyan} CLC KERNEL ${colors.reset} ${colors.bold}The AI Agent Governance & Quality Engineering Engine${colors.reset} ${colors.gray}v${PACKAGE_VERSION}${colors.reset}\n`);
37
39
  }
38
40
 
39
41
  function printBox(title, items) {
@@ -52,19 +54,37 @@ function printBox(title, items) {
52
54
  console.log(`${colors.dim}└${'─'.repeat(width)}┘${colors.reset}\n`);
53
55
  }
54
56
 
55
- async function promptOptions(detected) {
56
- const isInteractive = Boolean(process.stdin.isTTY) &&
57
- !process.env.CI &&
58
- !process.argv.includes('--yes') &&
59
- !process.argv.includes('-y');
57
+ /** Second-level select value for the stack-agnostic harness. */
58
+ const GENERIC_STACK = 'generic';
60
59
 
61
- if (!isInteractive) {
60
+ /**
61
+ * Lets the user confirm the detected stack, pick another one, or cancel.
62
+ * Picking another stack returns the config the detector builds for that
63
+ * adapter, so the chosen adapter drives provisioning, AGENTS.md and the hook.
64
+ * @param {object} detected - Config from autoDetectStack
65
+ * @param {object} [options]
66
+ * @param {string} [options.targetDir] - Project root (cwd when absent)
67
+ * @param {object} [options.prompts] - Prompts implementation (@clack/prompts)
68
+ * @param {boolean} [options.interactive] - Defaults to TTY, no CI, no --yes/-y
69
+ * @returns {Promise<object>} Final config
70
+ */
71
+ async function promptOptions(detected, options = {}) {
72
+ const {
73
+ targetDir = process.cwd(),
74
+ prompts = p,
75
+ interactive = Boolean(process.stdin.isTTY) &&
76
+ !process.env.CI &&
77
+ !process.argv.includes('--yes') &&
78
+ !process.argv.includes('-y')
79
+ } = options;
80
+
81
+ if (!interactive) {
62
82
  return detected;
63
83
  }
64
84
 
65
- p.intro(`${colors.cyan}${colors.bold}CLC Kernel Setup${colors.reset}`);
85
+ prompts.intro(`${colors.cyan}${colors.bold}CLC Kernel Setup${colors.reset}`);
66
86
 
67
- const choice = await p.select({
87
+ const choice = await prompts.select({
68
88
  message: '¿Cómo deseas inicializar CLC Kernel en este proyecto?',
69
89
  initialValue: 'confirm',
70
90
  options: [
@@ -74,19 +94,9 @@ async function promptOptions(detected) {
74
94
  hint: `Recomendado (${detected.projectType.toUpperCase()} - ${detected.framework})`
75
95
  },
76
96
  {
77
- value: 'frontend',
78
- label: 'Forzar modo FRONTEND',
79
- hint: 'Salvaguardas A11y, Zod, Next/Image, UI Reuse'
80
- },
81
- {
82
- value: 'backend',
83
- label: 'Forzar modo BACKEND',
84
- hint: 'Salvaguardas TDD, Clean Arch, N+1, Secrets'
85
- },
86
- {
87
- value: 'agent-os',
88
- label: 'Forzar modo AGENT-OS',
89
- hint: 'Salvaguardas HITL, Symlinks, Schemas, Memoria SSOT'
97
+ value: 'other',
98
+ label: 'Elegir otro stack',
99
+ hint: 'Instalar las salvaguardas de otro framework'
90
100
  },
91
101
  {
92
102
  value: 'cancel',
@@ -96,35 +106,77 @@ async function promptOptions(detected) {
96
106
  ]
97
107
  });
98
108
 
99
- if (p.isCancel(choice) || choice === 'cancel') {
100
- p.cancel('Instalación de CLC Kernel cancelada.');
109
+ if (prompts.isCancel(choice) || choice === 'cancel') {
110
+ prompts.cancel('Instalación de CLC Kernel cancelada.');
101
111
  process.exit(0);
102
112
  }
103
113
 
104
- if (choice === 'frontend') {
105
- detected.projectType = 'frontend';
106
- } else if (choice === 'backend') {
107
- detected.projectType = 'backend';
108
- } else if (choice === 'agent-os') {
109
- detected.projectType = 'agent-os';
114
+ if (choice !== 'other') {
115
+ return detected;
116
+ }
117
+
118
+ const stack = await prompts.select({
119
+ message: '¿Qué stack deseas instalar?',
120
+ initialValue: detected.adapter ? detected.adapter.name : GENERIC_STACK,
121
+ options: [
122
+ ...adapters.map((adapter) => ({
123
+ value: adapter.name,
124
+ label: adapter.name,
125
+ hint: `${adapter.projectType.toUpperCase()} - ${adapter.testRunner}`
126
+ })),
127
+ {
128
+ value: GENERIC_STACK,
129
+ label: 'Genérico',
130
+ hint: 'Salvaguardas agnósticas de stack'
131
+ }
132
+ ]
133
+ });
134
+
135
+ if (prompts.isCancel(stack)) {
136
+ prompts.cancel('Instalación de CLC Kernel cancelada.');
137
+ process.exit(0);
110
138
  }
111
139
 
112
- return detected;
140
+ const adapter = adapters.find((a) => a.name === stack) || null;
141
+ return buildStackConfig(targetDir, adapter);
113
142
  }
114
143
 
115
- function printSuccess(targetDir, isFront, config) {
116
- const isAgentOS = config.projectType === 'agent-os';
117
- const guardsDesc = isAgentOS
118
- ? `${colors.cyan}6 Guardias Agent-OS (HITL, Symlinks, Schemas, Memoria, Secrets)${colors.reset}`
119
- : isFront
120
- ? `${colors.emerald}10 Guardias Frontend (A11y, Zod, Image, UI Reuse, Storybook)${colors.reset}`
121
- : `${colors.violet}7 Guardias Backend (TDD, Architecture, Scope, Secret Scan)${colors.reset}`;
144
+ /**
145
+ * One line per part of a multi-part config: `dir: framework (test runner)`.
146
+ * Empty for a single-stack config, whose summary stays as it was.
147
+ * @param {object} config - Project config
148
+ * @returns {string[]}
149
+ */
150
+ function partLines(config) {
151
+ if (!config || !Array.isArray(config.parts)) return [];
152
+ return config.parts.map((part) => `${part.dir}: ${part.framework} (${part.testRunner})`);
153
+ }
154
+
155
+ /**
156
+ * Prints the installation summary.
157
+ * @param {string} targetDir - Provisioned project root
158
+ * @param {{ file: string, active: boolean }} [hook] - Hook written (relative path)
159
+ * and whether git runs it
160
+ * @param {object} config - Final project config
161
+ * @param {string[]} [installedGuards] - Guards actually copied into tools/
162
+ */
163
+ function printSuccess(targetDir, hook, config, installedGuards = []) {
164
+ const hookDesc = !hook
165
+ ? `${colors.amber}no hook written${colors.reset}`
166
+ : hook.active
167
+ ? `active hook ${colors.amber}${hook.file}${colors.reset}`
168
+ : `${colors.amber}${hook.file} written but not active (core.hooksPath unchanged)${colors.reset}`;
169
+ const guardsDesc = installedGuards.length > 0
170
+ ? `${colors.emerald}${installedGuards.length} guard(s): ${installedGuards.join(', ')}${colors.reset}`
171
+ : `${colors.amber}no guards installed${colors.reset}`;
122
172
 
123
173
  const summary = [
124
174
  `🔨 ${colors.bold}Proyecto Inicializado:${colors.reset} ${colors.cyan}${targetDir}${colors.reset}`,
125
175
  `🛡️ ${colors.bold}Salvaguardas:${colors.reset} ${guardsDesc}`,
126
176
  `📝 ${colors.bold}Leyes & SDD:${colors.reset} AGENTS.md, sdds/, docs/Journal/ precargados`,
127
- `🔒 ${colors.bold}Git Hooks:${colors.reset} Configurados en ${colors.amber}${config.gitHooks === 'husky' ? '.husky/pre-commit' : '.githooks/pre-commit'}${colors.reset}`
177
+ `🔒 ${colors.bold}Git Hooks:${colors.reset} ${hookDesc}`,
178
+ ...(partLines(config).length > 0 ? [`🧩 ${colors.bold}Partes:${colors.reset}`] : []),
179
+ ...partLines(config).map((line) => ` • ${line}`)
128
180
  ].join('\n');
129
181
 
130
182
  if (p.note && p.outro) {
@@ -136,10 +188,151 @@ function printSuccess(targetDir, isFront, config) {
136
188
  }
137
189
  }
138
190
 
191
+ /**
192
+ * Lines of the install/update summary (sdds/safe-update, decision 4): written,
193
+ * unchanged, protected (with the .clckernel-new path), moved, removed, added
194
+ * to .gitignore and git config; then what was left out on purpose: guards
195
+ * available but not installed, mirrors not present, tracked paths not
196
+ * ignored. A dry run prints the same sections as a plan.
197
+ * @param {object} plan - io.plan()
198
+ * @param {{ dryRun?: boolean }} [options]
199
+ * @returns {string[]}
200
+ */
201
+ function planLines(plan, { dryRun = false } = {}) {
202
+ const label = (real, planned) => (dryRun ? planned : real);
203
+ const lines = [];
204
+ const section = (title, items, color = colors.emerald) => {
205
+ if (items.length === 0) return;
206
+ lines.push(`${colors.bold}${title} (${items.length}):${colors.reset}`);
207
+ for (const item of items) lines.push(` ${color}•${colors.reset} ${item}`);
208
+ };
209
+ section(label('Written', 'Would write'), plan.written);
210
+ if (plan.unchanged.length > 0) {
211
+ lines.push(`${colors.bold}${label('Unchanged', 'Unchanged')} (${plan.unchanged.length}):${colors.reset} already up to date`);
212
+ }
213
+ section(label('Protected (local changes kept)', 'Would protect (local changes kept)'),
214
+ plan.protected.map((p) => `${p.path} (${p.reason}) -> new version in ${p.newPath}`), colors.amber);
215
+ section(label('Moved', 'Would move'), plan.moved.map((m) => `${m.from} -> ${m.to}`));
216
+ section(label('Removed', 'Would remove'), plan.removed);
217
+ section(label('Added to .gitignore', 'Would add to .gitignore'), plan.gitignore);
218
+ section(label('git config', 'Would set git config'), plan.gitConfig.map((c) => `${c.key} = ${c.value}`));
219
+ // sdds/update-no-additions, decision 5: what was left out on purpose.
220
+ section('Available, not installed (add with --add-guards <name,...|all>)', plan.available || [], colors.cyan);
221
+ section('Mirrors not present (skipped)', plan.mirrorsSkipped || [], colors.cyan);
222
+ section('Tracked, not ignored (git tracks them; no .gitignore entry added)', plan.trackedNotIgnored || [], colors.cyan);
223
+ // sdds/config-foundation: update.exclude in .clckernel.yaml.
224
+ section('Excluded by config (update.exclude; left as they are)', plan.excluded || [], colors.cyan);
225
+ if (plan.protected.length > 0) {
226
+ lines.push('');
227
+ lines.push(`${colors.amber}⚠ ${plan.protected.length} file(s) ${dryRun ? 'would be' : 'were'} protected: the kernel cannot prove they are its own and intact.${colors.reset}`);
228
+ lines.push(`${colors.amber} Review each *.clckernel-new copy, merge what you want, then delete it (it is gitignored).${colors.reset}`);
229
+ }
230
+ if (dryRun) {
231
+ lines.push('');
232
+ lines.push(`${colors.cyan}Dry run: nothing was written (no files, .gitignore or git config).${colors.reset}`);
233
+ }
234
+ return lines;
235
+ }
236
+
237
+ /**
238
+ * AI harnesses by the AGENTS.md mirror they read (sdds/onboarding). Agents
239
+ * that read AGENTS.md itself (Codex, OpenCode, ...) need no mirror.
240
+ */
241
+ const HARNESS_MIRRORS = [
242
+ { name: 'Claude Code (CLAUDE.md)', match: (p) => p === 'CLAUDE.md' },
243
+ { name: 'Gemini (GEMINI.md)', match: (p) => p === 'GEMINI.md' },
244
+ { name: 'Cursor', match: (p) => p === '.cursorrules' || p.startsWith('.cursor/') },
245
+ { name: 'Copilot', match: (p) => p === '.github/copilot-instructions.md' },
246
+ { name: 'Antigravity', match: (p) => p.startsWith('.antigravity/') },
247
+ { name: 'Windsurf', match: (p) => p === '.windsurfrules' },
248
+ ];
249
+
250
+ /**
251
+ * Harness names whose mirror the run wrote or found up to date.
252
+ * @param {object} plan - io.plan()
253
+ * @returns {string[]}
254
+ */
255
+ function harnessNames(plan) {
256
+ const paths = [...(plan.written || []), ...(plan.unchanged || [])].map((entry) => entry.split(' -> ')[0]);
257
+ return HARNESS_MIRRORS.filter((h) => paths.some(h.match)).map((h) => h.name);
258
+ }
259
+
260
+ /**
261
+ * "Next steps" after a real install (sdds/onboarding), true to what was
262
+ * installed: the hook file and whether git runs it (or the command that
263
+ * activates it), the harness mirrors written and the parts. English, like
264
+ * the plan listing.
265
+ * @param {{ file: string, active: boolean, activate?: string|null }|undefined} hook
266
+ * @param {object} plan - io.plan()
267
+ * @param {object} [config] - Final project config
268
+ * @returns {string[]}
269
+ */
270
+ function nextStepsLines(hook, plan, config = {}) {
271
+ const c = colors;
272
+ const cmd = (text) => `${c.cyan}${text}${c.reset}`;
273
+ const lines = [`${c.bold}Next steps${c.reset}`];
274
+ const commit = cmd('git add -A && git commit -m "chore: install clckernel"');
275
+ const parts = Array.isArray(config.parts) && config.parts.length > 0
276
+ ? ` The hook runs the checks of every part (${config.parts.map((p) => p.dir).join(', ')}) from the repository root.`
277
+ : null;
278
+ if (hook && hook.active) {
279
+ lines.push(` 1. Review and commit the harness: ${commit}`);
280
+ lines.push(` The pre-commit hook (${hook.file}) runs on this commit.`);
281
+ } else if (hook) {
282
+ lines.push(` 1. The pre-commit hook (${hook.file}) is not active yet. Activate it: ${cmd(hook.activate || 'git config core.hooksPath .githooks')}`);
283
+ lines.push(` Then review and commit the harness: ${commit}`);
284
+ } else {
285
+ lines.push(` 1. Review and commit the harness: ${commit} (no pre-commit hook was written)`);
286
+ }
287
+ if (hook && parts) lines.push(parts);
288
+ const harnesses = harnessNames(plan || {});
289
+ const agents = harnesses.length > 0
290
+ ? `AGENTS.md mirrors written for: ${harnesses.join(', ')}.`
291
+ : 'No AGENTS.md mirrors written: use an agent that reads AGENTS.md itself.';
292
+ lines.push(` 2. Open your AI agent and ask it to run ${cmd('clckernel start')} to tailor the rules (.clckernel.yaml),`);
293
+ lines.push(' or just start working: it follows AGENTS.md.');
294
+ lines.push(` ${agents}`);
295
+ lines.push(` 3. Run ${cmd('npx clckernel audit')} anytime to see every guard's result.`);
296
+ lines.push(' 4. Project-specific checks go in tools/guards/ (the hook and the audit run them).');
297
+ lines.push(` 5. Later, preview kernel updates with ${cmd('npx clckernel update --dry-run')}.`);
298
+ return lines;
299
+ }
300
+
301
+ /**
302
+ * Shorter "Next steps" after an update: only what needs attention
303
+ * (protected files to merge, guards available but not installed); empty
304
+ * when nothing does.
305
+ * @param {object} plan - io.plan()
306
+ * @returns {string[]}
307
+ */
308
+ function updateNextStepsLines(plan) {
309
+ const c = colors;
310
+ const steps = [];
311
+ for (const entry of plan.protected || []) {
312
+ steps.push(`Review ${entry.newPath}, merge what you want into ${entry.path}, then delete the copy.`);
313
+ }
314
+ if ((plan.available || []).length > 0) {
315
+ steps.push(`Optional guards: ${c.cyan}npx clckernel update --add-guards ${plan.available.join(',')}${c.reset}`);
316
+ }
317
+ if (steps.length === 0) return [];
318
+ return [`${c.bold}Next steps${c.reset}`, ...steps.map((step, i) => ` ${i + 1}. ${step}`)];
319
+ }
320
+
321
+ /** Prints planLines. */
322
+ function printPlan(plan, options) {
323
+ console.log(planLines(plan, options).join('\n'));
324
+ }
325
+
139
326
  module.exports = {
327
+ planLines,
328
+ nextStepsLines,
329
+ updateNextStepsLines,
330
+ harnessNames,
331
+ printPlan,
140
332
  colors,
141
333
  printBanner,
142
334
  printBox,
143
335
  promptOptions,
144
- printSuccess
336
+ printSuccess,
337
+ partLines
145
338
  };