@eltonssouza/development-utility-kit 0.10.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 (131) hide show
  1. package/.claude/agents/README.md +24 -0
  2. package/.claude/agents/analyst.md +198 -0
  3. package/.claude/agents/backend-developer.md +126 -0
  4. package/.claude/agents/brain-keeper.md +229 -0
  5. package/.claude/agents/code-reviewer.md +181 -0
  6. package/.claude/agents/database-engineer.md +94 -0
  7. package/.claude/agents/devops-engineer.md +141 -0
  8. package/.claude/agents/frontend-developer.md +97 -0
  9. package/.claude/agents/gate-keeper.md +118 -0
  10. package/.claude/agents/migrator.md +291 -0
  11. package/.claude/agents/mobile-developer.md +80 -0
  12. package/.claude/agents/n8n-specialist.md +94 -0
  13. package/.claude/agents/product-owner.md +115 -0
  14. package/.claude/agents/qa-engineer.md +232 -0
  15. package/.claude/agents/release-engineer.md +204 -0
  16. package/.claude/agents/scaffold.md +87 -0
  17. package/.claude/agents/security-engineer.md +199 -0
  18. package/.claude/agents/sprint-runner.md +46 -0
  19. package/.claude/agents/stack-resolver.md +104 -0
  20. package/.claude/agents/tech-lead.md +182 -0
  21. package/.claude/agents/update-template.md +54 -0
  22. package/.claude/agents/ux-designer.md +118 -0
  23. package/.claude/hooks/flow-guard.js +261 -0
  24. package/.claude/hooks/flow-state.js +197 -0
  25. package/.claude/local/CLAUDE.md +71 -0
  26. package/.claude/settings.json +55 -0
  27. package/.claude/skills/README.md +331 -0
  28. package/.claude/skills/active-project/SKILL.md +131 -0
  29. package/.claude/skills/api-integration-test/SKILL.md +84 -0
  30. package/.claude/skills/auto-test-guard/SKILL.md +239 -0
  31. package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
  32. package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
  33. package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
  34. package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
  35. package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
  36. package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
  37. package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
  38. package/.claude/skills/brain-keeper/SKILL.md +62 -0
  39. package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
  40. package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
  41. package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
  42. package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
  43. package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
  44. package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
  45. package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
  46. package/.claude/skills/brain-keeper/templates/README.md +51 -0
  47. package/.claude/skills/brain-keeper/templates/adr.md +40 -0
  48. package/.claude/skills/brain-keeper/templates/bug.md +35 -0
  49. package/.claude/skills/brain-keeper/templates/daily.md +38 -0
  50. package/.claude/skills/brain-keeper/templates/feature.md +62 -0
  51. package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
  52. package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
  53. package/.claude/skills/caveman/SKILL.md +189 -0
  54. package/.claude/skills/create-stack-pack/SKILL.md +281 -0
  55. package/.claude/skills/grill-me/SKILL.md +80 -0
  56. package/.claude/skills/pair-debug/SKILL.md +288 -0
  57. package/.claude/skills/prd-ready-check/SKILL.md +86 -0
  58. package/.claude/skills/project-manager/SKILL.md +334 -0
  59. package/.claude/skills/quality-standards/SKILL.md +203 -0
  60. package/.claude/skills/quick-feature/SKILL.md +266 -0
  61. package/.claude/skills/run-sprint/SKILL.md +41 -0
  62. package/.claude/skills/scaffold/SKILL.md +60 -0
  63. package/.claude/skills/stack-discovery/SKILL.md +161 -0
  64. package/.claude/skills/test-coverage-auditor/SKILL.md +87 -0
  65. package/.claude/skills/to-issues/SKILL.md +163 -0
  66. package/.claude/skills/to-prd/SKILL.md +130 -0
  67. package/.claude/skills/update-template/SKILL.md +256 -0
  68. package/.claude/stacks/CODEOWNERS +30 -0
  69. package/.claude/stacks/README.md +97 -0
  70. package/.claude/stacks/_template.md +116 -0
  71. package/.claude/stacks/dotnet/aspire-9.md +528 -0
  72. package/.claude/stacks/go/gin-1.10.md +570 -0
  73. package/.claude/stacks/java/spring-boot-3.md +376 -0
  74. package/.claude/stacks/java/spring-boot-4.md +438 -0
  75. package/.claude/stacks/node/express-5.md +538 -0
  76. package/.claude/stacks/python/django-5.md +483 -0
  77. package/.claude/stacks/python/fastapi-0.115.md +522 -0
  78. package/.claude/stacks/typescript/angular-18.md +420 -0
  79. package/.claude/stacks/typescript/angular-19.md +397 -0
  80. package/.claude/stacks/typescript/angular-21.md +494 -0
  81. package/CLAUDE.md +472 -0
  82. package/README.md +412 -0
  83. package/bin/cli.js +848 -0
  84. package/bin/lib/adr.js +146 -0
  85. package/bin/lib/backup.js +62 -0
  86. package/bin/lib/detect-stack.js +476 -0
  87. package/bin/lib/doctor.js +527 -0
  88. package/bin/lib/help.js +328 -0
  89. package/bin/lib/identity.js +108 -0
  90. package/bin/lib/lint-allowlist.json +15 -0
  91. package/bin/lib/lint.js +798 -0
  92. package/bin/lib/local-dir.js +68 -0
  93. package/bin/lib/manifest.js +236 -0
  94. package/bin/lib/sync-all.js +394 -0
  95. package/bin/lib/version-check.js +398 -0
  96. package/dashboard/db.js +321 -0
  97. package/dashboard/package.json +22 -0
  98. package/dashboard/public/app.js +853 -0
  99. package/dashboard/public/content/docs/agents-reference.en.md +911 -0
  100. package/dashboard/public/content/docs/architecture-overview.en.md +252 -0
  101. package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
  102. package/dashboard/public/content/docs/cli-reference.en.md +538 -0
  103. package/dashboard/public/content/docs/git-flow.en.md +525 -0
  104. package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
  105. package/dashboard/public/content/docs/hooks-reference.en.md +404 -0
  106. package/dashboard/public/content/docs/pipeline.en.md +414 -0
  107. package/dashboard/public/content/docs/plugins.en.md +289 -0
  108. package/dashboard/public/content/docs/quality-gate.en.md +315 -0
  109. package/dashboard/public/content/docs/skills-reference.en.md +484 -0
  110. package/dashboard/public/content/docs/stack-rules.en.md +362 -0
  111. package/dashboard/public/content/docs/troubleshooting.en.md +565 -0
  112. package/dashboard/public/content/manifest.json +114 -0
  113. package/dashboard/public/content/manual/backend.en.md +1053 -0
  114. package/dashboard/public/content/manual/existing-project.en.md +848 -0
  115. package/dashboard/public/content/manual/frontend.en.md +1008 -0
  116. package/dashboard/public/content/manual/fullstack.en.md +1459 -0
  117. package/dashboard/public/content/manual/mobile.en.md +837 -0
  118. package/dashboard/public/content/manual/quickstart.en.md +169 -0
  119. package/dashboard/public/index.html +217 -0
  120. package/dashboard/public/style.css +857 -0
  121. package/dashboard/public/vendor/marked.min.js +69 -0
  122. package/dashboard/rtk.js +143 -0
  123. package/dashboard/server-app.js +421 -0
  124. package/dashboard/server.js +104 -0
  125. package/dashboard/test/sprint1.test.js +406 -0
  126. package/dashboard/test/sprint2.test.js +571 -0
  127. package/dashboard/test/sprint3.test.js +560 -0
  128. package/package.json +33 -0
  129. package/scripts/hooks/subagent-telemetry.sh +14 -0
  130. package/scripts/hooks/telemetry-writer.js +250 -0
  131. package/scripts/latest-versions.json +56 -0
package/bin/cli.js ADDED
@@ -0,0 +1,848 @@
1
+ #!/usr/bin/env node
2
+
3
+ 'use strict';
4
+
5
+ const { spawnSync, spawn } = require('child_process');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ const help = require('./lib/help');
10
+ const identity = require('./lib/identity');
11
+ const backup = require('./lib/backup');
12
+ const manifestLib = require('./lib/manifest');
13
+ const localDirLib = require('./lib/local-dir');
14
+ const doctor = require('./lib/doctor');
15
+ const lint = require('./lib/lint');
16
+ const adr = require('./lib/adr');
17
+
18
+ // ─── parseArgs ────────────────────────────────────────────────────────────────
19
+
20
+ /**
21
+ * @typedef {{
22
+ * command: string|null,
23
+ * positional: string[],
24
+ * sub: string|null,
25
+ * dryRun: boolean,
26
+ * help: boolean,
27
+ * version: boolean,
28
+ * checkOnly: boolean,
29
+ * force: boolean,
30
+ * apply: boolean,
31
+ * filters: string[],
32
+ * excludes: string[],
33
+ * dashboardPort: string|null,
34
+ * dashboardNoOpen: boolean,
35
+ * dashboardPassthrough: string[],
36
+ * json: boolean,
37
+ * strict: boolean,
38
+ * categories: string[],
39
+ * }} ParsedArgs
40
+ */
41
+
42
+ /**
43
+ * Parse the argv of a duk invocation.
44
+ * @param {string[]} argv
45
+ * @returns {ParsedArgs}
46
+ */
47
+ function parseArgs(argv) {
48
+ const args = argv.slice(2);
49
+ const result = {
50
+ command: null,
51
+ positional: [],
52
+ sub: null,
53
+ dryRun: false,
54
+ help: false,
55
+ version: false,
56
+ checkOnly: false,
57
+ force: false,
58
+ apply: false,
59
+ filters: [],
60
+ excludes: [],
61
+ dashboardPort: null,
62
+ dashboardNoOpen: false,
63
+ dashboardPassthrough: [],
64
+ json: false,
65
+ strict: false,
66
+ categories: [],
67
+ };
68
+
69
+ for (let i = 0; i < args.length; i++) {
70
+ const arg = args[i];
71
+
72
+ if (arg === '--help' || arg === '-h') {
73
+ result.help = true;
74
+ } else if (arg === '--version' || arg === '-v') {
75
+ result.version = true;
76
+ } else if (arg === '--path') {
77
+ process.stderr.write(
78
+ 'Error: --path is no longer supported. Use "duk install" from the target directory directly.\n'
79
+ );
80
+ process.exit(1);
81
+ } else if (arg === '--sub') {
82
+ if (i + 1 < args.length) {
83
+ result.sub = args[++i];
84
+ } else {
85
+ process.stderr.write('Error: --sub requires a directory name argument\n');
86
+ process.exit(1);
87
+ }
88
+ } else if (arg === '--dry-run') {
89
+ result.dryRun = true;
90
+ } else if (arg === '--check-only') {
91
+ result.checkOnly = true;
92
+ } else if (arg === '--force') {
93
+ result.force = true;
94
+ } else if (arg === '--apply') {
95
+ result.apply = true;
96
+ } else if (arg === '--filter') {
97
+ if (i + 1 < args.length) {
98
+ result.filters.push(args[++i]);
99
+ } else {
100
+ process.stderr.write('Error: --filter requires an expression argument\n');
101
+ process.exit(1);
102
+ }
103
+ } else if (arg === '--exclude') {
104
+ if (i + 1 < args.length) {
105
+ result.excludes.push(args[++i]);
106
+ } else {
107
+ process.stderr.write('Error: --exclude requires a name argument\n');
108
+ process.exit(1);
109
+ }
110
+ } else if (arg === '--port') {
111
+ if (i + 1 < args.length) {
112
+ result.dashboardPort = args[++i];
113
+ result.dashboardPassthrough.push('--port', result.dashboardPort);
114
+ } else {
115
+ process.stderr.write('Error: --port requires a port number\n');
116
+ process.exit(1);
117
+ }
118
+ } else if (arg === '--no-open') {
119
+ result.dashboardNoOpen = true;
120
+ result.dashboardPassthrough.push('--no-open');
121
+ } else if (arg === '--json') {
122
+ result.json = true;
123
+ } else if (arg === '--strict') {
124
+ result.strict = true;
125
+ } else if (arg === '--category') {
126
+ if (i + 1 < args.length) {
127
+ // Comma-separated values allowed
128
+ for (const c of args[++i].split(',')) {
129
+ const t = c.trim();
130
+ if (t) result.categories.push(t);
131
+ }
132
+ } else {
133
+ process.stderr.write('Error: --category requires a name argument\n');
134
+ process.exit(1);
135
+ }
136
+ } else if (!arg.startsWith('-')) {
137
+ if (result.command === null) {
138
+ // BR-006: normalise update → install
139
+ result.command = arg === 'update' ? 'install' : arg;
140
+ } else {
141
+ result.positional.push(arg);
142
+ }
143
+ } else {
144
+ // Unknown flag — surface usage
145
+ process.stderr.write(`Warning: unknown flag "${arg}" ignored.\n`);
146
+ }
147
+ }
148
+
149
+ return result;
150
+ }
151
+
152
+ // ─── detectProjectType ────────────────────────────────────────────────────────
153
+
154
+ /**
155
+ * @typedef {{ type: string, stackHints: string[] }} DetectionResult
156
+ */
157
+
158
+ function readJsonSafe(filePath) {
159
+ try {
160
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Legacy file-based detector. Newer richer stack detection lives in
168
+ * `bin/lib/detect-stack.js` (Wave 5 metade B). Kept here because adopt
169
+ * still uses it for type + stackHints in CLAUDE.md.
170
+ * @param {string} dir
171
+ * @returns {DetectionResult}
172
+ */
173
+ function detectProjectType(dir) {
174
+ const signals = new Set();
175
+
176
+ function scanDir(scanDir) {
177
+ let entries;
178
+ try {
179
+ entries = fs.readdirSync(scanDir);
180
+ } catch {
181
+ return;
182
+ }
183
+ for (const entry of entries) {
184
+ const full = path.join(scanDir, entry);
185
+ if (entry === 'pom.xml' || entry === 'build.gradle') {
186
+ signals.add('backend');
187
+ continue;
188
+ }
189
+ if (entry === 'n8n') {
190
+ try {
191
+ if (fs.statSync(full).isDirectory()) signals.add('automation/n8n');
192
+ } catch { /* ignore */ }
193
+ continue;
194
+ }
195
+ if (/^docker-compose.*\.yml$/.test(entry)) {
196
+ try {
197
+ const content = fs.readFileSync(full, 'utf8');
198
+ if (content.includes('n8n')) signals.add('automation/n8n');
199
+ } catch { /* ignore */ }
200
+ continue;
201
+ }
202
+ if (entry === 'package.json') {
203
+ const pkg = readJsonSafe(full);
204
+ if (pkg) {
205
+ const deps = Object.assign({}, pkg.dependencies || {}, pkg.devDependencies || {});
206
+ if ('react-native' in deps) signals.add('mobile');
207
+ else if ('@angular/core' in deps) signals.add('frontend-angular');
208
+ else if ('vite' in deps) signals.add('frontend-vite');
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ scanDir(dir);
215
+
216
+ let rootEntries;
217
+ try {
218
+ rootEntries = fs.readdirSync(dir);
219
+ } catch {
220
+ rootEntries = [];
221
+ }
222
+ for (const entry of rootEntries) {
223
+ const full = path.join(dir, entry);
224
+ try {
225
+ if (fs.statSync(full).isDirectory() && !entry.startsWith('.')) {
226
+ scanDir(full);
227
+ }
228
+ } catch { /* ignore */ }
229
+ }
230
+
231
+ const stackHints = Array.from(signals);
232
+ const hasBackend = signals.has('backend');
233
+ const hasMobile = signals.has('mobile');
234
+ const hasFrontend = signals.has('frontend-angular') || signals.has('frontend-vite');
235
+ const hasN8n = signals.has('automation/n8n');
236
+ const distinctDomains = [hasBackend, hasMobile, hasFrontend, hasN8n].filter(Boolean).length;
237
+
238
+ const friendlyHints = stackHints.map((s) => {
239
+ if (s === 'frontend-angular') return '@angular/core';
240
+ if (s === 'frontend-vite') return 'vite';
241
+ return s;
242
+ });
243
+
244
+ if (distinctDomains >= 3 || (hasBackend && hasMobile && hasN8n)) {
245
+ return { type: 'complex/multi-domain', stackHints: friendlyHints };
246
+ }
247
+ if (distinctDomains === 2) return { type: 'fullstack', stackHints: friendlyHints };
248
+ if (hasBackend) return { type: 'backend', stackHints: friendlyHints };
249
+ if (hasMobile) return { type: 'mobile', stackHints: friendlyHints };
250
+ if (hasFrontend) return { type: 'frontend', stackHints: friendlyHints };
251
+ if (hasN8n) return { type: 'automation/n8n', stackHints: friendlyHints };
252
+ return { type: 'fullstack', stackHints: [] };
253
+ }
254
+
255
+ // ─── package root + version ───────────────────────────────────────────────────
256
+
257
+ function packageRoot() {
258
+ return path.resolve(__dirname, '..');
259
+ }
260
+
261
+ function harnessVersion() {
262
+ try {
263
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot(), 'package.json'), 'utf8'));
264
+ return pkg.version || '0.0.0';
265
+ } catch {
266
+ return '0.0.0';
267
+ }
268
+ }
269
+
270
+ // ─── adoptProject (install / update) ──────────────────────────────────────────
271
+
272
+ /**
273
+ * @typedef {{
274
+ * cwd: string,
275
+ * sub: string|null,
276
+ * dryRun: boolean,
277
+ * force: boolean,
278
+ * initialIdentity?: string|null,
279
+ * }} AdoptOptions
280
+ */
281
+
282
+ /**
283
+ * Orchestrate install: drift check → backup → copy → MANIFEST + HARNESS_VERSION
284
+ * + local/ → CLAUDE.md merge.
285
+ *
286
+ * @param {AdoptOptions} opts
287
+ */
288
+ function adoptProject(opts) {
289
+ const { cwd, sub, dryRun, force } = opts;
290
+ const initialIdentity = opts.initialIdentity || null;
291
+
292
+ const subDir = sub ? path.join(cwd, sub) : cwd;
293
+
294
+ if (sub && !fs.existsSync(subDir)) {
295
+ process.stderr.write(`Error: subdirectory "${subDir}" does not exist.\n`);
296
+ process.exit(1);
297
+ }
298
+
299
+ const dr = detectProjectType(subDir);
300
+
301
+ const pkgRoot = packageRoot();
302
+ const srcClaude = path.join(pkgRoot, '.claude');
303
+ const srcClaudeMd = path.join(pkgRoot, 'CLAUDE.md');
304
+ const destClaude = path.join(subDir, '.claude');
305
+ const destClaudeMd = path.join(subDir, 'CLAUDE.md');
306
+
307
+ // Item 21 — self-destruction guard.
308
+ // If install runs inside the harness source tree, the backup step would
309
+ // `mv srcClaude → srcClaude.bak`, removing the very source about to be
310
+ // copied. Abort early with a clear message before any destructive op.
311
+ const pkgRootResolved = path.resolve(pkgRoot);
312
+ const targetResolved = path.resolve(subDir);
313
+ if (targetResolved === pkgRootResolved) {
314
+ process.stderr.write('Error: cannot install harness into its own source tree.\n');
315
+ process.stderr.write(`Target directory equals the harness package root:\n ${pkgRootResolved}\n`);
316
+ process.stderr.write('Run "duk install" from a separate project directory.\n');
317
+ process.exit(1);
318
+ }
319
+ // Secondary check: target has package.json declaring the harness itself.
320
+ // Catches cases where the path equality misses (symlinks, normalised differently).
321
+ const targetPkgJsonPath = path.join(targetResolved, 'package.json');
322
+ if (fs.existsSync(targetPkgJsonPath)) {
323
+ try {
324
+ const targetPkg = JSON.parse(fs.readFileSync(targetPkgJsonPath, 'utf8'));
325
+ if (targetPkg && targetPkg.name === '@eltonssouza/development-utility-kit') {
326
+ process.stderr.write('Error: cannot install harness into its own source tree.\n');
327
+ process.stderr.write(`Target package.json declares: ${targetPkg.name}\n`);
328
+ process.stderr.write('Run "duk install" from a separate project directory.\n');
329
+ process.exit(1);
330
+ }
331
+ } catch {
332
+ // Ignore parse errors — only the secondary check, primary covers cwd === pkgRoot.
333
+ }
334
+ }
335
+
336
+ // Drift detection (only if .claude/ already exists and not forced)
337
+ if (!dryRun && fs.existsSync(destClaude) && !force) {
338
+ const drift = manifestLib.detectDrift(destClaude);
339
+ if (drift.hasManifest && (drift.drifted.length > 0 || drift.removed.length > 0)) {
340
+ process.stderr.write('\nLocal drift detected in .claude/:\n');
341
+ for (const f of drift.drifted) process.stderr.write(` - ${f} (sha mismatch)\n`);
342
+ for (const f of drift.removed) process.stderr.write(` - ${f} (removed locally)\n`);
343
+ process.stderr.write('\nOptions:\n');
344
+ process.stderr.write(' 1. duk install --force # overwrite + backup (drift preserved in .claude.bak)\n');
345
+ process.stderr.write(' 2. Move customisations to .claude/local/ # never touched by install\n');
346
+ process.stderr.write(' 3. Open PR on the harness repo if your change is canonical\n\n');
347
+ process.stderr.write('Aborted. No changes written.\n');
348
+ process.exit(1);
349
+ }
350
+ }
351
+
352
+ if (dryRun) {
353
+ process.stdout.write('[dry-run] No files will be written.\n');
354
+ process.stdout.write(`Detected type : ${dr.type}\n`);
355
+ process.stdout.write(`Stack hints : ${dr.stackHints.join(', ') || '(none)'}\n`);
356
+ process.stdout.write(`Target dir : ${subDir}\n`);
357
+ process.stdout.write(`Harness version: ${harnessVersion()}\n`);
358
+ if (fs.existsSync(destClaude)) {
359
+ process.stdout.write(' [dry-run] would backup .claude/ -> .claude.bak/\n');
360
+ }
361
+ process.stdout.write(' [dry-run] would write .claude/ from package\n');
362
+ process.stdout.write(' [dry-run] would write .claude/HARNESS_VERSION\n');
363
+ process.stdout.write(' [dry-run] would write .claude/.MANIFEST (sha256 baseline)\n');
364
+ localDirLib.ensureLocalDir(destClaude, true);
365
+ process.stdout.write(` [dry-run] would write CLAUDE.md${fs.existsSync(destClaudeMd) ? ' (preserving ## Project Identity)' : ' (fresh)'}\n`);
366
+ process.exit(0);
367
+ }
368
+
369
+ // Backup existing .claude/
370
+ backup.backupClaudeDir(subDir, false);
371
+
372
+ // Copy package .claude/ into target
373
+ if (!fs.existsSync(srcClaude)) {
374
+ process.stderr.write(`Error: package .claude/ directory not found at ${srcClaude}\n`);
375
+ process.exit(1);
376
+ }
377
+ backup.copyDir(srcClaude, destClaude);
378
+
379
+ // Provision .claude/local/
380
+ localDirLib.ensureLocalDir(destClaude, false);
381
+
382
+ // Write HARNESS_VERSION
383
+ const version = harnessVersion();
384
+ manifestLib.writeHarnessVersion(destClaude, version);
385
+
386
+ // Generate + write .MANIFEST AFTER everything else is in place
387
+ manifestLib.writeManifest(destClaude, version);
388
+
389
+ // Read template CLAUDE.md
390
+ let templateContent = '';
391
+ if (fs.existsSync(srcClaudeMd)) {
392
+ templateContent = fs.readFileSync(srcClaudeMd, 'utf8');
393
+ }
394
+
395
+ // Determine identity section
396
+ let identitySection;
397
+ if (initialIdentity) {
398
+ identitySection = initialIdentity;
399
+ } else if (fs.existsSync(destClaudeMd)) {
400
+ const existingContent = fs.readFileSync(destClaudeMd, 'utf8');
401
+ const preserved = identity.readIdentitySection(existingContent);
402
+ identitySection = preserved !== null ? preserved : identity.generateIdentitySection(dr);
403
+ } else {
404
+ identitySection = identity.generateIdentitySection(dr);
405
+ }
406
+
407
+ const merged = identity.mergeClaudeMd('', templateContent, identitySection);
408
+ fs.writeFileSync(destClaudeMd, merged, 'utf8');
409
+
410
+ // Confirmation output
411
+ process.stdout.write('\nProject injection complete.\n');
412
+ process.stdout.write(`Detected type : ${dr.type}\n`);
413
+ process.stdout.write(`Stack hints : ${dr.stackHints.join(', ') || '(none)'}\n`);
414
+ process.stdout.write(`Target dir : ${subDir}\n`);
415
+ process.stdout.write(`Harness version: ${version}\n`);
416
+ process.stdout.write('Written : .claude/ (+ HARNESS_VERSION, .MANIFEST, local/)\n');
417
+ process.stdout.write('Written : CLAUDE.md\n');
418
+ }
419
+
420
+ // ─── newProject (duk new <name>) ──────────────────────────────────────────────
421
+
422
+ /**
423
+ * Validate a project folder name (kebab-case-ish, no path separators).
424
+ * @param {string} name
425
+ * @returns {string|null} error message or null if valid
426
+ */
427
+ function validateProjectName(name) {
428
+ if (!name) return 'Project name is required.';
429
+ if (name.length > 80) return 'Project name is too long (max 80 chars).';
430
+ if (/[\s/\\:*?"<>|]/.test(name)) return 'Project name contains invalid characters.';
431
+ if (name.startsWith('.') || name.startsWith('-')) return 'Project name must not start with "." or "-".';
432
+ return null;
433
+ }
434
+
435
+ /**
436
+ * Scaffold a new project: mkdir + git init + CLAUDE.md placeholder + install.
437
+ * @param {string} name
438
+ * @param {{ dryRun: boolean }} options
439
+ */
440
+ function newProject(name, options) {
441
+ const err = validateProjectName(name);
442
+ if (err) {
443
+ process.stderr.write(`Error: ${err}\n`);
444
+ process.exit(1);
445
+ }
446
+
447
+ const target = path.resolve(process.cwd(), name);
448
+ if (fs.existsSync(target)) {
449
+ process.stderr.write(`Error: folder "${target}" already exists.\n`);
450
+ process.exit(1);
451
+ }
452
+
453
+ if (options.dryRun) {
454
+ process.stdout.write('[dry-run] No files will be written.\n');
455
+ process.stdout.write(` [dry-run] would mkdir ${target}\n`);
456
+ process.stdout.write(' [dry-run] would run "git init"\n');
457
+ process.stdout.write(' [dry-run] would write CLAUDE.md with empty Project Identity\n');
458
+ process.stdout.write(' [dry-run] would run "duk install" inside the new folder\n');
459
+ process.exit(0);
460
+ }
461
+
462
+ // 1. mkdir
463
+ fs.mkdirSync(target, { recursive: true });
464
+
465
+ // 2. git init (non-fatal if git not installed; warn and continue)
466
+ const gitResult = spawnSync('git', ['init', '-q'], {
467
+ cwd: target,
468
+ stdio: 'inherit',
469
+ shell: process.platform === 'win32',
470
+ });
471
+ if (gitResult.status !== 0) {
472
+ process.stdout.write('Warning: "git init" failed or git is not in PATH. Continuing without git repo.\n');
473
+ }
474
+
475
+ // 3. Run install with initialIdentity = empty placeholder
476
+ const placeholderIdentity = identity.emptyIdentitySection(name);
477
+ adoptProject({
478
+ cwd: target,
479
+ sub: null,
480
+ dryRun: false,
481
+ force: true, // fresh folder — no drift possible
482
+ initialIdentity: placeholderIdentity,
483
+ });
484
+
485
+ // 4. Next-steps banner
486
+ process.stdout.write('\n');
487
+ process.stdout.write(`Project "${name}" created.\n\n`);
488
+ process.stdout.write('Next steps:\n');
489
+ process.stdout.write(` 1. cd ${name}\n`);
490
+ process.stdout.write(' 2. Open Cowork or Claude Code in this folder\n');
491
+ process.stdout.write(' 3. First message: "sabatina pra projeto novo"\n');
492
+ process.stdout.write(' → stack-discovery skill walks you through 8 questions\n');
493
+ process.stdout.write(' and fills the Project Identity block.\n');
494
+ }
495
+
496
+ // ─── checkOnly (duk install --check-only) ────────────────────────────────────
497
+
498
+ /**
499
+ * Best-effort lazy loader for an optional sibling module. Returns null if
500
+ * the module is missing; logs a one-liner about it.
501
+ * @param {string} relPath relative to bin/lib (e.g. "./detect-stack")
502
+ * @returns {object|null}
503
+ */
504
+ function tryRequire(relPath) {
505
+ try {
506
+ return require(relPath);
507
+ } catch (e) {
508
+ if (e && e.code === 'MODULE_NOT_FOUND' && String(e.message).includes(relPath.replace('./', ''))) {
509
+ return null;
510
+ }
511
+ // Some other error (syntax, runtime) — re-throw with context
512
+ if (e && e.code === 'MODULE_NOT_FOUND') return null;
513
+ throw e;
514
+ }
515
+ }
516
+
517
+ function checkOnly(opts) {
518
+ const subDir = opts.sub ? path.join(opts.cwd, opts.sub) : opts.cwd;
519
+ if (opts.sub && !fs.existsSync(subDir)) {
520
+ process.stderr.write(`Error: subdirectory "${subDir}" does not exist.\n`);
521
+ process.exit(1);
522
+ }
523
+
524
+ process.stdout.write('Detecting stack...\n');
525
+
526
+ // Prefer the richer detector (other agent — Wave 5 metade B) if present.
527
+ const detectStackMod = tryRequire('./lib/detect-stack');
528
+ const versionCheckMod = tryRequire('./lib/version-check');
529
+
530
+ let stackInfo = null;
531
+ if (detectStackMod && typeof detectStackMod.detectStack === 'function') {
532
+ try {
533
+ stackInfo = detectStackMod.detectStack(subDir);
534
+ } catch (e) {
535
+ process.stderr.write(`Warning: detect-stack failed: ${e.message}\n`);
536
+ }
537
+ }
538
+
539
+ if (!stackInfo) {
540
+ // Fallback: use legacy detector
541
+ const dr = detectProjectType(subDir);
542
+ stackInfo = {
543
+ type: dr.type,
544
+ language: null,
545
+ framework: null,
546
+ version: null,
547
+ detected_from: dr.stackHints,
548
+ };
549
+ process.stdout.write(' (detect-stack module not available — using legacy detector)\n');
550
+ }
551
+
552
+ // detect-stack uses `language`; legacy used `lang`. Either is fine.
553
+ const langValue = stackInfo.language || stackInfo.lang || null;
554
+ const frameworkValue = stackInfo.framework && stackInfo.framework !== 'unknown'
555
+ ? stackInfo.framework
556
+ : null;
557
+
558
+ process.stdout.write(` Detected type : ${stackInfo.type || '(unknown)'}\n`);
559
+ if (langValue && langValue !== 'unknown') {
560
+ const langVer = stackInfo.java_version || stackInfo.node_version
561
+ || stackInfo.python_version || stackInfo.go_version || '';
562
+ process.stdout.write(` Language : ${langValue}${langVer ? ' ' + langVer : ''}\n`);
563
+ }
564
+ if (frameworkValue) {
565
+ process.stdout.write(` Framework : ${frameworkValue}${stackInfo.version ? ' ' + stackInfo.version : ''}\n`);
566
+ }
567
+ if (stackInfo.ui_lib) {
568
+ process.stdout.write(` UI library : ${stackInfo.ui_lib}\n`);
569
+ }
570
+ if (stackInfo.database && stackInfo.database.length > 0) {
571
+ process.stdout.write(` Database : ${stackInfo.database.join(', ')}\n`);
572
+ }
573
+ if (stackInfo.detected_from && stackInfo.detected_from.length > 0) {
574
+ process.stdout.write(` Detected from : ${stackInfo.detected_from.join(', ')}\n`);
575
+ }
576
+
577
+ // Harness version comparison
578
+ const destClaude = path.join(subDir, '.claude');
579
+ const localVersion = manifestLib.readHarnessVersion(destClaude);
580
+ const currentVersion = harnessVersion();
581
+ process.stdout.write('\n');
582
+ process.stdout.write(`Local harness : ${localVersion || '(not installed)'}\n`);
583
+ process.stdout.write(`Current harness : ${currentVersion}\n`);
584
+
585
+ // Latest versions (if the API module is available)
586
+ if (versionCheckMod && typeof versionCheckMod.getLatestVersions === 'function') {
587
+ try {
588
+ const stackKey = stackInfo.framework || stackInfo.lang || stackInfo.type;
589
+ const latest = versionCheckMod.getLatestVersions(stackKey);
590
+ if (latest && Object.keys(latest).length > 0) {
591
+ process.stdout.write('\nLatest stack versions (from registries / fallback JSON):\n');
592
+ for (const [k, v] of Object.entries(latest)) {
593
+ if (k.startsWith('_')) continue;
594
+ process.stdout.write(` ${k.padEnd(15)} : ${v}\n`);
595
+ }
596
+ }
597
+ } catch (e) {
598
+ process.stderr.write(`Warning: getLatestVersions failed: ${e.message}\n`);
599
+ }
600
+ } else {
601
+ process.stdout.write(' (version-check module not available — latest versions skipped)\n');
602
+ }
603
+
604
+ // Drift report (no-op write)
605
+ if (fs.existsSync(destClaude)) {
606
+ const drift = manifestLib.detectDrift(destClaude);
607
+ if (drift.hasManifest) {
608
+ const total = drift.drifted.length + drift.added.length + drift.removed.length;
609
+ process.stdout.write(`\nDrift : ${total === 0 ? 'clean' : `${total} file(s)`}\n`);
610
+ if (total > 0) {
611
+ for (const f of drift.drifted) process.stdout.write(` drift : ${f}\n`);
612
+ for (const f of drift.added) process.stdout.write(` added locally : ${f}\n`);
613
+ for (const f of drift.removed) process.stdout.write(` removed locally: ${f}\n`);
614
+ }
615
+ } else {
616
+ process.stdout.write('\nDrift : (no MANIFEST baseline — run install to create one)\n');
617
+ }
618
+ }
619
+
620
+ process.stdout.write('\nNothing written. Run `duk install` to apply updates.\n');
621
+ process.exit(0);
622
+ }
623
+
624
+ // ─── dashboard ────────────────────────────────────────────────────────────────
625
+
626
+ /**
627
+ * Ensure dashboard dependencies are installed, then spawn the server.
628
+ * @param {string[]} passthroughArgs
629
+ */
630
+ function dashboard(passthroughArgs) {
631
+ const pkgRoot = packageRoot();
632
+ const dashboardDir = path.join(pkgRoot, 'dashboard');
633
+ const nodeModulesDir = path.join(dashboardDir, 'node_modules');
634
+ const serverScript = path.join(dashboardDir, 'server.js');
635
+
636
+ if (!fs.existsSync(dashboardDir)) {
637
+ process.stderr.write('Error: dashboard/ directory not found at ' + dashboardDir + '\n');
638
+ process.exit(1);
639
+ }
640
+
641
+ if (!fs.existsSync(nodeModulesDir)) {
642
+ process.stdout.write('Installing dashboard dependencies (first run)...\n');
643
+ const result = spawnSync('npm', ['install', '--prefix', dashboardDir], {
644
+ stdio: 'inherit',
645
+ shell: process.platform === 'win32',
646
+ });
647
+ if (result.status !== 0) {
648
+ process.stderr.write('Error: npm install failed for dashboard dependencies.\n');
649
+ process.exit(result.status !== null ? result.status : 1);
650
+ }
651
+ }
652
+
653
+ // FR-008: print rtk gain output before starting the server
654
+ const rtkResult = spawnSync('rtk', ['gain'], {
655
+ shell: process.platform === 'win32',
656
+ stdio: ['ignore', 'pipe', 'ignore'],
657
+ });
658
+ if (rtkResult.status === 0 && rtkResult.stdout && rtkResult.stdout.length > 0) {
659
+ process.stdout.write(rtkResult.stdout);
660
+ } else {
661
+ process.stdout.write('Note: rtk not found or returned no output — skipping gain report.\n');
662
+ }
663
+
664
+ const child = spawn(process.execPath, [serverScript, ...passthroughArgs], {
665
+ stdio: 'inherit',
666
+ cwd: dashboardDir,
667
+ });
668
+
669
+ child.on('error', (err) => {
670
+ process.stderr.write('Error starting dashboard server: ' + err.message + '\n');
671
+ process.exit(1);
672
+ });
673
+
674
+ child.on('close', (code) => {
675
+ process.exit(code !== null ? code : 0);
676
+ });
677
+ }
678
+
679
+ // ─── sync-all dispatcher ──────────────────────────────────────────────────────
680
+
681
+ /**
682
+ * Delegate to bin/lib/sync-all.js (created by Wave 5 metade B). Falls back
683
+ * to a clear error if the module is missing.
684
+ *
685
+ * @param {string} dir
686
+ * @param {{ filters: string[], excludes: string[], apply: boolean, dryRun: boolean }} options
687
+ */
688
+ function dispatchSyncAll(dir, options) {
689
+ const mod = tryRequire('./lib/sync-all');
690
+ if (!mod || typeof mod.runSyncAll !== 'function') {
691
+ process.stderr.write(
692
+ 'Error: sync-all command is unavailable.\n' +
693
+ ' The bin/lib/sync-all.js module was not found.\n' +
694
+ ' This part of the CLI is delivered by Wave 5 metade B —\n' +
695
+ ' re-run `npm install` or update the harness.\n'
696
+ );
697
+ process.exit(2);
698
+ }
699
+ try {
700
+ return mod.runSyncAll(dir, options);
701
+ } catch (e) {
702
+ process.stderr.write(`Error running sync-all: ${e.message}\n`);
703
+ process.exit(1);
704
+ }
705
+ }
706
+
707
+ // ─── main ─────────────────────────────────────────────────────────────────────
708
+
709
+ function main() {
710
+ // Early dispatch for commands with their own flag parsers (bypass parseArgs).
711
+ if (process.argv[2] === 'adr') {
712
+ const code = adr.runAdr(process.argv.slice(3));
713
+ process.exit(code);
714
+ }
715
+
716
+ const parsed = parseArgs(process.argv);
717
+ const {
718
+ command, positional, sub, dryRun, help: helpFlag, version,
719
+ checkOnly: checkOnlyFlag, force, apply, filters, excludes,
720
+ dashboardPassthrough, json, strict, categories,
721
+ } = parsed;
722
+
723
+ if (version) {
724
+ process.stdout.write(harnessVersion() + '\n');
725
+ process.exit(0);
726
+ }
727
+
728
+ // `duk help [command]`
729
+ if (command === 'help') {
730
+ if (positional.length === 0) {
731
+ help.printGeneralHelp();
732
+ } else {
733
+ help.printCommandHelp(positional[0]);
734
+ }
735
+ process.exit(process.exitCode || 0);
736
+ }
737
+
738
+ // `duk --help` (no command) → general help
739
+ if (helpFlag && command === null) {
740
+ help.printGeneralHelp();
741
+ process.exit(0);
742
+ }
743
+
744
+ // `duk <command> --help` → command-specific
745
+ if (helpFlag && command !== null) {
746
+ // command was already normalised update→install; pass original alias too
747
+ help.printCommandHelp(command);
748
+ process.exit(process.exitCode || 0);
749
+ }
750
+
751
+ // No command at all → short help
752
+ if (command === null) {
753
+ help.printShortHelp();
754
+ process.exit(0);
755
+ }
756
+
757
+ // Routing
758
+ if (command === 'install') {
759
+ if (checkOnlyFlag) {
760
+ checkOnly({ cwd: process.cwd(), sub: sub || null });
761
+ return;
762
+ }
763
+ adoptProject({
764
+ cwd: process.cwd(),
765
+ sub: sub || null,
766
+ dryRun,
767
+ force,
768
+ });
769
+ return;
770
+ }
771
+
772
+ if (command === 'new') {
773
+ const name = positional[0];
774
+ if (!name) {
775
+ process.stderr.write('Error: `duk new` requires a project name. Run `duk help new`.\n');
776
+ process.exit(1);
777
+ }
778
+ newProject(name, { dryRun });
779
+ return;
780
+ }
781
+
782
+ if (command === 'sync-all') {
783
+ const dir = positional[0];
784
+ if (!dir) {
785
+ process.stderr.write('Error: `duk sync-all` requires a directory argument. Run `duk help sync-all`.\n');
786
+ process.exit(1);
787
+ }
788
+ dispatchSyncAll(dir, {
789
+ filters,
790
+ excludes,
791
+ apply,
792
+ dryRun: !apply,
793
+ });
794
+ return;
795
+ }
796
+
797
+ if (command === 'dashboard') {
798
+ dashboard(dashboardPassthrough);
799
+ return;
800
+ }
801
+
802
+ if (command === 'doctor') {
803
+ const code = doctor.runDoctor({ json, strict });
804
+ process.exit(code);
805
+ }
806
+
807
+ if (command === 'lint') {
808
+ const code = lint.runLint({ json, categories });
809
+ process.exit(code);
810
+ }
811
+
812
+ if (command === 'adr') {
813
+ // Pass raw argv (skip 'node' + 'cli.js' + 'adr') so --by flag is preserved.
814
+ const code = adr.runAdr(process.argv.slice(3));
815
+ process.exit(code);
816
+ }
817
+
818
+ // Unknown command — show short help
819
+ process.stderr.write(`Unknown command: ${command}\n\n`);
820
+ help.printShortHelp();
821
+ process.exit(1);
822
+ }
823
+
824
+ if (require.main === module) {
825
+ main();
826
+ }
827
+
828
+ // Export functions for testing and for other modules to consume.
829
+ module.exports = {
830
+ parseArgs,
831
+ detectProjectType,
832
+ // identity helpers re-exported for back-compat with old tests
833
+ generateIdentitySection: identity.generateIdentitySection,
834
+ emptyIdentitySection: identity.emptyIdentitySection,
835
+ readIdentitySection: identity.readIdentitySection,
836
+ mergeClaudeMd: identity.mergeClaudeMd,
837
+ // backup helpers
838
+ backupClaudeDir: backup.backupClaudeDir,
839
+ copyDir: backup.copyDir,
840
+ // command entrypoints
841
+ adoptProject,
842
+ newProject,
843
+ checkOnly,
844
+ // misc
845
+ packageRoot,
846
+ harnessVersion,
847
+ validateProjectName,
848
+ };