clckernel 1.2.18 → 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 +27 -46
  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 +737 -309
  32. package/src/gitignore.js +144 -0
  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 -36
  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
@@ -0,0 +1,144 @@
1
+ /**
2
+ * CLC Kernel — Gitignore Provisioning Engine
3
+ * Ensures AI IDE mirror symlinks, local SDD specs, and ephemeral runtime artifacts
4
+ * are safely ignored in consumer repositories to preserve AGENTS.md as the SSOT.
5
+ */
6
+
7
+ const path = require('path');
8
+ const { spawnSync } = require('child_process');
9
+ const { currentIO } = require('./io');
10
+
11
+ // Repository-locating variables a hook may export; they would point git at
12
+ // another repository than the target's.
13
+ const GIT_LOCAL_ENV = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_PREFIX', 'GIT_COMMON_DIR'];
14
+
15
+ /**
16
+ * Entries whose path (or, for a directory entry, anything under it) is
17
+ * tracked by git in targetDir (sdds/update-no-additions, decision 3). Only
18
+ * literal paths are checked: negations and glob patterns are kept as they
19
+ * are. Outside a git repository nothing is tracked. One `git ls-files` call.
20
+ * @param {string} targetDir - Project root
21
+ * @param {string[]} entries - Candidate .gitignore entries
22
+ * @returns {Set<string>}
23
+ */
24
+ function trackedEntries(targetDir, entries) {
25
+ const literal = entries.filter((e) => !e.startsWith('!') && !/[*?[\\]/.test(e));
26
+ if (literal.length === 0) return new Set();
27
+ const env = { ...process.env };
28
+ for (const key of GIT_LOCAL_ENV) delete env[key];
29
+ const bare = (e) => e.replace(/\/+$/, '');
30
+ const res = spawnSync('git', ['ls-files', '-z', '--', ...literal.map((e) => `:(literal)${bare(e)}`)], {
31
+ cwd: targetDir, env, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'],
32
+ });
33
+ if (res.error || res.status !== 0) return new Set();
34
+ const files = res.stdout.split('\0').filter(Boolean);
35
+ return new Set(literal.filter((e) => files.some((f) => f === bare(e) || f.startsWith(`${bare(e)}/`))));
36
+ }
37
+
38
+ /**
39
+ * Managed entries per section. An `{ entry, ifExists: true }` item names an
40
+ * artifact of an older release (sdds/onboarding): it is added only when that
41
+ * path exists in the project, so a fresh install names no retired artifact.
42
+ */
43
+ const MANAGED_GITIGNORE_SECTIONS = [
44
+ {
45
+ header: '# CLC Kernel: Local AI IDE Mirror Symlinks (SSOT: AGENTS.md)',
46
+ entries: [
47
+ 'CLAUDE.md',
48
+ 'GEMINI.md',
49
+ '.cursorrules',
50
+ '.windsurfrules',
51
+ '.cursor/',
52
+ '.antigravity/',
53
+ '.github/copilot-instructions.md',
54
+ ],
55
+ },
56
+ {
57
+ header: '# CLC Kernel: CLI Agent Skills & Playbooks',
58
+ entries: [
59
+ '.agents/skills/clckernel_cli/',
60
+ { entry: '.agents/skills/clc_forge_cli/', ifExists: true },
61
+ ],
62
+ },
63
+ {
64
+ header: '# CLC Kernel: Local SDD Specs & Ephemeral Harness Artifacts',
65
+ entries: [
66
+ 'sdds/*',
67
+ '!sdds/.gitkeep',
68
+ 'openspec/*',
69
+ '!openspec/.gitkeep',
70
+ { entry: '.clckernel.resolved.json', ifExists: true },
71
+ { entry: '.clc-forge.resolved.json', ifExists: true },
72
+ '.husky/_/',
73
+ '.ruff_cache/',
74
+ '*.clckernel-new',
75
+ ],
76
+ },
77
+ ];
78
+
79
+ /**
80
+ * Idempotently appends missing CLC Kernel entries to target project's .gitignore.
81
+ *
82
+ * Writes through the active io session (sdds/safe-update), so a dry run
83
+ * only records the entries it would add. An entry whose path git tracks is
84
+ * not added (sdds/update-no-additions, decision 3).
85
+ *
86
+ * @param {string} targetDir - Target project root
87
+ * @returns {boolean} True if .gitignore was created or modified, false if already complete.
88
+ */
89
+ function ensureGitignoreEntries(targetDir) {
90
+ const io = currentIO(targetDir);
91
+ const gitignorePath = path.join(targetDir, '.gitignore');
92
+ const existing = io.read(gitignorePath);
93
+ const currentContent = existing ? existing.toString('utf-8') : '';
94
+
95
+ const existingLines = new Set(
96
+ currentContent
97
+ .split('\n')
98
+ .map(line => line.trim())
99
+ .filter(line => line.length > 0)
100
+ );
101
+
102
+ const sectionsToAppend = [];
103
+ const added = [];
104
+
105
+ const entriesOf = (section) => section.entries
106
+ .filter(item => typeof item === 'string' || io.exists(path.join(targetDir, item.entry)))
107
+ .map(item => (typeof item === 'string' ? item : item.entry));
108
+
109
+ // A path the project tracks is never ignored behind its back: the entry is
110
+ // left out and reported instead.
111
+ const candidates = MANAGED_GITIGNORE_SECTIONS.flatMap(entriesOf)
112
+ .filter(entry => !existingLines.has(entry.trim()));
113
+ const tracked = trackedEntries(targetDir, candidates);
114
+ for (const entry of tracked) io.noteTrackedNotIgnored(entry);
115
+
116
+ for (const section of MANAGED_GITIGNORE_SECTIONS) {
117
+ const missingEntries = entriesOf(section).filter(entry => !existingLines.has(entry.trim()) && !tracked.has(entry));
118
+ if (missingEntries.length > 0) {
119
+ sectionsToAppend.push(`${section.header}\n${missingEntries.join('\n')}`);
120
+ added.push(...missingEntries);
121
+ }
122
+ }
123
+
124
+ if (sectionsToAppend.length === 0) {
125
+ return false;
126
+ }
127
+
128
+ let newContent = currentContent;
129
+ if (newContent.length > 0 && !newContent.endsWith('\n')) {
130
+ newContent += '\n';
131
+ }
132
+ if (newContent.length > 0) {
133
+ newContent += '\n';
134
+ }
135
+ newContent += sectionsToAppend.join('\n\n') + '\n';
136
+
137
+ io.appendGitignore(gitignorePath, newContent, added);
138
+ return true;
139
+ }
140
+
141
+ module.exports = {
142
+ ensureGitignoreEntries,
143
+ MANAGED_GITIGNORE_SECTIONS,
144
+ };
package/src/index.js CHANGED
@@ -3,9 +3,11 @@
3
3
  * Coordinates auto-discovery, UI rendering, generator engine, and doctor checks.
4
4
  */
5
5
 
6
+ const fs = require('fs');
6
7
  const path = require('path');
8
+ const { spawnSync } = require('child_process');
7
9
  const { autoDetectStack } = require('./detector');
8
- const { printBanner, printBox, promptOptions, printSuccess, colors } = require('./ui');
10
+ const { printBanner, printBox, promptOptions, printSuccess, partLines, colors } = require('./ui');
9
11
  const { generateHarness } = require('./generator');
10
12
  const { runDoctor } = require('./doctor');
11
13
 
@@ -18,28 +20,102 @@ const READ_ONLY_COMMANDS = new Set(['help', '--help', '-h', 'version', '--versio
18
20
  */
19
21
  const INSTALL_FLAGS = new Set(['--yes', '-y']);
20
22
 
23
+ /** Prints the plan instead of writing (sdds/safe-update, decision 5). Not an opt-in by itself. */
24
+ const DRY_RUN_FLAG = '--dry-run';
25
+
26
+ /** Opts guards the project does not have into an update (sdds/update-no-additions). */
27
+ const ADD_GUARDS_FLAG = '--add-guards';
28
+
29
+ /**
30
+ * Parses the arguments after `update`: at most one target dir, --dry-run,
31
+ * and `--add-guards <a,b|all>` (or `--add-guards=<...>`). The names are
32
+ * checked against the stack's manifest by the updater, before any write.
33
+ * @param {string[]} rest - Arguments without the `update` word
34
+ * @returns {{ positionals: string[], dryRun: boolean, addGuards?: string[]|'all' }|null}
35
+ * null when the input is not understood
36
+ */
37
+ function parseUpdateArgs(rest) {
38
+ const positionals = [];
39
+ let dryRun = false;
40
+ let addGuards;
41
+ for (let i = 0; i < rest.length; i++) {
42
+ const arg = rest[i];
43
+ let value = null;
44
+ if (arg === DRY_RUN_FLAG) {
45
+ dryRun = true;
46
+ continue;
47
+ } else if (arg === ADD_GUARDS_FLAG) {
48
+ value = rest[i + 1];
49
+ if (value === undefined || value.startsWith('-')) return null;
50
+ i++;
51
+ } else if (arg.startsWith(`${ADD_GUARDS_FLAG}=`)) {
52
+ value = arg.slice(ADD_GUARDS_FLAG.length + 1);
53
+ } else if (arg.startsWith('-')) {
54
+ return null;
55
+ } else {
56
+ positionals.push(arg);
57
+ continue;
58
+ }
59
+ if (addGuards !== undefined) return null;
60
+ const names = value.split(',').map((n) => n.trim()).filter(Boolean);
61
+ if (names.length === 0) return null;
62
+ addGuards = names.includes('all') ? 'all' : names;
63
+ }
64
+ if (positionals.length > 1) return null;
65
+ return { positionals, dryRun, addGuards };
66
+ }
67
+
68
+ /**
69
+ * Words AGENTS.md documents as agent commands (`clckernel test`, ...). They are
70
+ * executed by the AI agent, not by this CLI; without this list they would be
71
+ * taken as an install target directory.
72
+ */
73
+ const AGENT_COMMANDS = new Set(['test', 'start', 'feature', 'fix', 'commit', 'guard']);
74
+
21
75
  const HELP_TEXT = `
22
76
  ${'clckernel'} — CLC Kernel: The AI Agent Governance Engine
23
77
 
24
78
  USAGE
25
79
  clckernel <command> [targetDir]
26
80
 
27
- READ-ONLY COMMANDS (never modify your project)
81
+ READ-ONLY COMMANDS (do not modify your project's code or config)
28
82
  help, --help, -h Show this help
29
83
  version, --version, -v Print the installed version
30
84
  doctor [targetDir] Run diagnostics on a project
31
- update [targetDir] Refresh tools, playbooks and IDE symlinks
85
+ audit [targetDir] [--change <name>]
86
+ Run the installed audit orchestrators (tools/audit.js
87
+ and/or tools/audit.py) and print the summary. Writes
88
+ nothing, unless --change <name>: then the report and
89
+ receipt go to sdds/<name>/ (only there)
90
+ Checks staged changes, or the last commit if none
32
91
 
33
- INSTALLATION (writes to the target project)
92
+ WRITES TO THE TARGET PROJECT
93
+ update [targetDir] Refresh tools, hooks, playbooks, IDE symlinks,
94
+ .gitignore and AGENTS.md. A kernel file you
95
+ edited is kept; the new version is written
96
+ next to it as <file>.clckernel-new. Only the
97
+ guards and mirrors you have are refreshed;
98
+ none are added
99
+ --add-guards <a,b|all> With update: also install these guards
34
100
  clckernel [targetDir] --yes Provision the governance harness
101
+ --dry-run With update or --yes: print the plan, write nothing
102
+
103
+ Installation requires an explicit opt-in and an existing target directory.
104
+ Bare \`clckernel\` runs the interactive confirmation flow; with no TTY it
105
+ refuses to install.
35
106
 
36
- Requires an explicit opt-in. Bare \`clckernel\` runs the interactive
37
- confirmation flow; with no TTY it refuses to install.
107
+ AGENT COMMANDS
108
+ test, start, feature, fix, commit, guard are defined in AGENTS.md and are
109
+ run by your AI agent, not by this CLI.
38
110
 
39
111
  EXAMPLES
40
112
  clckernel --help
41
113
  clckernel doctor .
114
+ clckernel audit .
115
+ clckernel audit . --change my-feature
42
116
  clckernel update .
117
+ clckernel update . --dry-run
118
+ clckernel update . --add-guards check_skills
43
119
  clckernel . --yes
44
120
  `;
45
121
 
@@ -59,6 +135,109 @@ function rejectUnknownInput(args) {
59
135
  return 1;
60
136
  }
61
137
 
138
+ /**
139
+ * Explains that an AGENTS.md agent command is not a CLI command.
140
+ * @param {string} word - The reserved word the user typed
141
+ * @returns {number} Process exit code
142
+ */
143
+ function rejectAgentCommand(word) {
144
+ process.stderr.write(
145
+ `\n❌ \`${word}\` is an agent command defined in AGENTS.md, not a CLI command.\n` +
146
+ ' Ask your AI agent to run it, or run `clckernel --help` to see CLI commands.\n\n'
147
+ );
148
+ return 1;
149
+ }
150
+
151
+ /**
152
+ * Resolves a user-supplied directory and checks it exists and is a directory.
153
+ * Prints the error itself so callers only decide the exit code.
154
+ * @param {string|undefined} input - Raw path argument (cwd when absent)
155
+ * @returns {string|null} Absolute path, or null when unusable
156
+ */
157
+ function resolveExistingDir(input) {
158
+ const dir = input ? path.resolve(input) : process.cwd();
159
+ let stat;
160
+ try {
161
+ stat = fs.statSync(dir);
162
+ } catch (e) {
163
+ process.stderr.write(`\n❌ Target directory does not exist: ${dir}\n Nothing was written.\n\n`);
164
+ return null;
165
+ }
166
+ if (!stat.isDirectory()) {
167
+ process.stderr.write(`\n❌ Target is not a directory: ${dir}\n Nothing was written.\n\n`);
168
+ return null;
169
+ }
170
+ return dir;
171
+ }
172
+
173
+ /**
174
+ * `clckernel audit [dir] [--change <name>]`: runs every orchestrator installed
175
+ * in dir/tools (audit.js, then audit.py) with cwd = dir. A mixed stack has
176
+ * both, and each only discovers guards of its own language, so all of them
177
+ * must run. --change is passed through (the orchestrators validate the name).
178
+ * @param {string|undefined} input - Target directory argument
179
+ * @param {string|null} [change] - SDD change name for the report, or null
180
+ * @returns {number} Exit code: the first non-zero orchestrator status, else 0
181
+ */
182
+ function runAudit(input, change = null) {
183
+ const dir = resolveExistingDir(input);
184
+ if (!dir) return 1;
185
+
186
+ const candidates = [
187
+ { file: 'audit.js', cmd: process.execPath },
188
+ { file: 'audit.py', cmd: 'python3' },
189
+ ];
190
+ const orchestrators = candidates.filter((c) => fs.existsSync(path.join(dir, 'tools', c.file)));
191
+ if (orchestrators.length === 0) {
192
+ process.stderr.write(
193
+ `\n❌ No audit orchestrator found in ${path.join(dir, 'tools')} (audit.js or audit.py).\n` +
194
+ ` Install the harness first: clckernel ${input || '.'} --yes\n\n`
195
+ );
196
+ return 1;
197
+ }
198
+
199
+ let exitCode = 0;
200
+ for (const orchestrator of orchestrators) {
201
+ const orchestratorArgs = [path.join('tools', orchestrator.file), ...(change ? ['--change', change] : [])];
202
+ const res = spawnSync(orchestrator.cmd, orchestratorArgs, {
203
+ cwd: dir,
204
+ stdio: 'inherit',
205
+ });
206
+ let status;
207
+ if (res.error) {
208
+ process.stderr.write(`\n❌ Could not run tools/${orchestrator.file}: ${res.error.message}\n\n`);
209
+ status = 1;
210
+ } else {
211
+ status = res.status === null ? 1 : res.status;
212
+ }
213
+ if (status !== 0 && exitCode === 0) exitCode = status;
214
+ }
215
+ return exitCode;
216
+ }
217
+
218
+ /**
219
+ * `audit` arguments: at most one target dir and one `--change <name>`.
220
+ * @param {string[]} rest - Arguments after `audit`
221
+ * @returns {{ targetDir?: string, change: string|null } | null} null on unknown input
222
+ */
223
+ function parseAuditArgs(rest) {
224
+ const out = { targetDir: undefined, change: null };
225
+ for (let i = 0; i < rest.length; i += 1) {
226
+ const arg = rest[i];
227
+ if (arg === '--change') {
228
+ const name = rest[i + 1];
229
+ if (out.change !== null || name === undefined || name.startsWith('-')) return null;
230
+ out.change = name;
231
+ i += 1;
232
+ } else if (arg.startsWith('-') || out.targetDir !== undefined) {
233
+ return null;
234
+ } else {
235
+ out.targetDir = arg;
236
+ }
237
+ }
238
+ return out;
239
+ }
240
+
62
241
  async function runCli() {
63
242
  const args = process.argv.slice(2);
64
243
  const command = args[0];
@@ -78,16 +257,39 @@ async function runCli() {
78
257
  return;
79
258
  }
80
259
 
260
+ if (AGENT_COMMANDS.has(command)) {
261
+ process.exitCode = rejectAgentCommand(command);
262
+ return;
263
+ }
264
+
265
+ if (command === 'audit') {
266
+ const parsed = parseAuditArgs(args.slice(1));
267
+ if (!parsed) {
268
+ process.exitCode = rejectUnknownInput(args);
269
+ return;
270
+ }
271
+ process.exitCode = runAudit(parsed.targetDir, parsed.change);
272
+ return;
273
+ }
274
+
81
275
  if (command === 'doctor' || args.includes('--doctor')) {
82
276
  const target = args[1] && args[1] !== '--doctor' ? args[1] : (args[0] === 'doctor' ? null : args[0]);
83
- runDoctor(target ? path.resolve(target) : process.cwd());
277
+ const failures = runDoctor(target ? path.resolve(target) : process.cwd());
278
+ if (failures > 0) process.exitCode = 1;
84
279
  return;
85
280
  }
86
281
 
87
282
  if (command === 'update' || args.includes('--update')) {
88
- const target = args[1] && args[1] !== '--update' ? args[1] : (args[0] === 'update' ? null : args[0]);
283
+ const rest = [...args];
284
+ rest.splice(rest.indexOf(command === 'update' ? 'update' : '--update'), 1);
285
+ const parsed = parseUpdateArgs(rest);
286
+ if (!parsed) {
287
+ process.exitCode = rejectUnknownInput(args);
288
+ return;
289
+ }
89
290
  const { runUpdate } = require('./updater');
90
- await runUpdate(target ? path.resolve(target) : process.cwd());
291
+ const target = parsed.positionals[0];
292
+ await runUpdate(target ? path.resolve(target) : process.cwd(), { dryRun: parsed.dryRun, addGuards: parsed.addGuards });
91
293
  return;
92
294
  }
93
295
 
@@ -95,15 +297,26 @@ async function runCli() {
95
297
  // or an explicit install flag.
96
298
  const flags = args.filter((a) => a.startsWith('-'));
97
299
  const positionals = args.filter((a) => !a.startsWith('-'));
98
- const unknownFlags = flags.filter((f) => !INSTALL_FLAGS.has(f));
300
+ const unknownFlags = flags.filter((f) => !INSTALL_FLAGS.has(f) && f !== DRY_RUN_FLAG);
301
+ const dryRun = flags.includes(DRY_RUN_FLAG);
99
302
 
100
303
  if (unknownFlags.length > 0 || positionals.length > 1) {
101
304
  process.exitCode = rejectUnknownInput(args);
102
305
  return;
103
306
  }
104
307
 
105
- const targetDir = positionals.length === 1 ? path.resolve(positionals[0]) : process.cwd();
106
- const explicitInstall = INSTALL_FLAGS.has(command) || flags.length > 0;
308
+ if (positionals.length === 1 && AGENT_COMMANDS.has(positionals[0])) {
309
+ process.exitCode = rejectAgentCommand(positionals[0]);
310
+ return;
311
+ }
312
+
313
+ // A mistyped path must never become a freshly created directory.
314
+ const targetDir = resolveExistingDir(positionals[0]);
315
+ if (!targetDir) {
316
+ process.exitCode = 1;
317
+ return;
318
+ }
319
+ const explicitInstall = flags.some((f) => INSTALL_FLAGS.has(f));
107
320
  const isInteractive = Boolean(process.stdin.isTTY) && !process.env.CI;
108
321
 
109
322
  if (!explicitInstall && !isInteractive) {
@@ -116,6 +329,20 @@ async function runCli() {
116
329
  return;
117
330
  }
118
331
 
332
+ // The project config is read before the prompt and before any write: an
333
+ // invalid one stops the install here (sdds/config-foundation).
334
+ const { readProjectConfig } = require('./config');
335
+ let projectConfig;
336
+ try {
337
+ projectConfig = readProjectConfig(targetDir);
338
+ } catch (err) {
339
+ if (err.name !== 'ConfigError') throw err;
340
+ process.stderr.write(`\n❌ ${err.message}\n Nothing was written.\n\n`);
341
+ process.exitCode = 1;
342
+ return;
343
+ }
344
+ for (const warning of projectConfig.warnings) process.stderr.write(`⚠ ${warning}\n`);
345
+
119
346
  // 1. Print Pro ASCII Banner
120
347
  printBanner();
121
348
 
@@ -124,15 +351,18 @@ async function runCli() {
124
351
 
125
352
  // 3. Render Formatted Configuration Box
126
353
  printBox('🔍 CONFIGURACIÓN DETECTADA AUTOMÁTICAMENTE', [
354
+ { label: '📁 Target', value: targetDir, badgeColor: colors.cyan },
127
355
  { label: '📦 Tipo de Proyecto', value: detected.projectType.toUpperCase(), badgeColor: colors.amber },
128
356
  { label: '🚀 Framework', value: detected.framework, badgeColor: colors.emerald },
129
357
  { label: '🎨 Estilos & UI', value: `${detected.styling} (${detected.uiLibrary})`, badgeColor: colors.violet },
130
358
  { label: '🧪 Test Runner', value: detected.testRunner, badgeColor: colors.cyan },
131
- { label: '🔒 Git Hooks', value: detected.gitHooks, badgeColor: colors.blue }
359
+ { label: '🔒 Git Hooks', value: detected.gitHooks, badgeColor: colors.blue },
360
+ // Multi-part repos (sdds/fullstack-monorepo): one row per part.
361
+ ...partLines(detected).map((line) => ({ label: '🧩 Parte', value: line, badgeColor: colors.emerald }))
132
362
  ]);
133
363
 
134
364
  // 4. Prompt User for Confirmation or Override
135
- const finalConfig = await promptOptions(detected);
365
+ const finalConfig = await promptOptions(detected, { targetDir });
136
366
 
137
367
  // 5. Generate Harness & Safeguards
138
368
  let s;
@@ -142,14 +372,23 @@ async function runCli() {
142
372
  s.start('Configurando salvaguardas y motor de gobernanza...');
143
373
  }
144
374
 
145
- generateHarness(targetDir, finalConfig);
375
+ const { installedGuards, hook, plan } = generateHarness(targetDir, finalConfig, { dryRun, projectConfig }) || {};
146
376
 
147
377
  if (s) {
148
- s.stop('Salvaguardas y motor de gobernanza configurados.');
378
+ s.stop(dryRun ? 'Plan listo (dry run).' : 'Salvaguardas y motor de gobernanza configurados.');
149
379
  }
150
380
 
151
- // 6. Render Pro Success Card
152
- printSuccess(targetDir, finalConfig.projectType === 'frontend', finalConfig);
381
+ // 6. Render Pro Success Card (or, for a dry run, only the plan)
382
+ const { printPlan } = require('./ui');
383
+ if (dryRun) {
384
+ if (plan) printPlan(plan, { dryRun: true });
385
+ return;
386
+ }
387
+ printSuccess(targetDir, hook, finalConfig, installedGuards);
388
+ if (plan) printPlan(plan);
389
+ // sdds/onboarding: what to do next, true to this install.
390
+ const { nextStepsLines } = require('./ui');
391
+ console.log(`\n${nextStepsLines(hook, plan, finalConfig).join('\n')}\n`);
153
392
  }
154
393
 
155
394
  module.exports = { runCli };