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