@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.
- package/.claude/agents/analyst.md +198 -0
- package/.claude/agents/backend-developer.md +126 -0
- package/.claude/agents/brain-keeper.md +229 -0
- package/.claude/agents/code-reviewer.md +181 -0
- package/.claude/agents/database-engineer.md +94 -0
- package/.claude/agents/devops-engineer.md +141 -0
- package/.claude/agents/frontend-developer.md +97 -0
- package/.claude/agents/gate-keeper.md +118 -0
- package/.claude/agents/migrator.md +291 -0
- package/.claude/agents/mobile-developer.md +80 -0
- package/.claude/agents/n8n-specialist.md +94 -0
- package/.claude/agents/product-owner.md +115 -0
- package/.claude/agents/qa-engineer.md +232 -0
- package/.claude/agents/release-engineer.md +204 -0
- package/.claude/agents/scaffold.md +87 -0
- package/.claude/agents/security-engineer.md +199 -0
- package/.claude/agents/sprint-runner.md +44 -0
- package/.claude/agents/stack-resolver.md +84 -0
- package/.claude/agents/tech-lead.md +182 -0
- package/.claude/agents/update-template.md +54 -0
- package/.claude/agents/ux-designer.md +118 -0
- package/.claude/settings.json +44 -0
- package/.claude/skills/README.md +332 -0
- package/.claude/skills/active-project/SKILL.md +129 -0
- package/.claude/skills/api-integration-test/SKILL.md +64 -0
- package/.claude/skills/auto-test-guard/SKILL.md +237 -0
- package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
- package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
- package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
- package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
- package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
- package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
- package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
- package/.claude/skills/brain-keeper/SKILL.md +60 -0
- package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
- package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
- package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
- package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
- package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
- package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
- package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
- package/.claude/skills/brain-keeper/templates/README.md +51 -0
- package/.claude/skills/brain-keeper/templates/adr.md +40 -0
- package/.claude/skills/brain-keeper/templates/bug.md +35 -0
- package/.claude/skills/brain-keeper/templates/daily.md +38 -0
- package/.claude/skills/brain-keeper/templates/feature.md +62 -0
- package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
- package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
- package/.claude/skills/caveman/SKILL.md +187 -0
- package/.claude/skills/create-stack-pack/SKILL.md +281 -0
- package/.claude/skills/grill-me/SKILL.md +79 -0
- package/.claude/skills/honcho-memory/SKILL.md +207 -0
- package/.claude/skills/honcho-memory/docs/api-endpoints-verified.md +75 -0
- package/.claude/skills/honcho-memory/hooks/on-prompt-submit.js +221 -0
- package/.claude/skills/honcho-memory/hooks/on-stop.js +193 -0
- package/.claude/skills/honcho-memory/lib/honcho-client.js +363 -0
- package/.claude/skills/honcho-memory/lib/memory-injector.js +93 -0
- package/.claude/skills/honcho-memory/package.json +32 -0
- package/.claude/skills/honcho-memory/scripts/cli.js +370 -0
- package/.claude/skills/honcho-memory/scripts/setup.js +109 -0
- package/.claude/skills/honcho-memory/tests/t001-api-endpoints-verified.test.js +89 -0
- package/.claude/skills/honcho-memory/tests/t002-structure.test.js +97 -0
- package/.claude/skills/honcho-memory/tests/t003-honcho-client.test.js +162 -0
- package/.claude/skills/honcho-memory/tests/t004-soft-delete.test.js +259 -0
- package/.claude/skills/honcho-memory/tests/t005-memory-injector.test.js +175 -0
- package/.claude/skills/honcho-memory/tests/t006-on-prompt-submit.test.js +215 -0
- package/.claude/skills/honcho-memory/tests/t007-on-stop.test.js +165 -0
- package/.claude/skills/honcho-memory/tests/t008-cli.test.js +214 -0
- package/.claude/skills/honcho-memory/tests/t009-setup.test.js +232 -0
- package/.claude/skills/honcho-memory/tests/t010-skill-md.test.js +114 -0
- package/.claude/skills/honcho-memory/tests/t011-settings-hooks.test.js +105 -0
- package/.claude/skills/honcho-memory/tests/t012-docs-update.test.js +106 -0
- package/.claude/skills/honcho-memory/tests/t013-smoke-e2e.test.js +90 -0
- package/.claude/skills/pair-debug/SKILL.md +288 -0
- package/.claude/skills/prd-ready-check/SKILL.md +58 -0
- package/.claude/skills/project-manager/SKILL.md +167 -0
- package/.claude/skills/quality-standards/SKILL.md +201 -0
- package/.claude/skills/quick-feature/SKILL.md +264 -0
- package/.claude/skills/run-sprint/SKILL.md +342 -0
- package/.claude/skills/scaffold/SKILL.md +58 -0
- package/.claude/skills/stack-discovery/SKILL.md +159 -0
- package/.claude/skills/test-coverage-auditor/SKILL.md +59 -0
- package/.claude/skills/to-issues/SKILL.md +163 -0
- package/.claude/skills/to-prd/SKILL.md +130 -0
- package/.claude/skills/update-template/SKILL.md +254 -0
- package/.claude/stacks/CODEOWNERS +30 -0
- package/.claude/stacks/README.md +88 -0
- package/.claude/stacks/_template.md +116 -0
- package/.claude/stacks/java/spring-boot-3.md +376 -0
- package/.claude/stacks/java/spring-boot-4.md +438 -0
- package/.claude/stacks/typescript/angular-18.md +420 -0
- package/.claude/stacks/typescript/angular-19.md +397 -0
- package/.claude/stacks/typescript/angular-21.md +494 -0
- package/CLAUDE.md +453 -0
- package/README.md +391 -0
- package/bin/cli.js +773 -0
- package/bin/lib/backup.js +62 -0
- package/bin/lib/detect-stack.js +476 -0
- package/bin/lib/help.js +233 -0
- package/bin/lib/identity.js +108 -0
- package/bin/lib/local-dir.js +69 -0
- package/bin/lib/manifest.js +236 -0
- package/bin/lib/sync-all.js +394 -0
- package/bin/lib/version-check.js +398 -0
- package/dashboard/db.js +199 -0
- package/dashboard/package.json +22 -0
- package/dashboard/public/app.js +709 -0
- package/dashboard/public/content/docs/agents-reference.en.md +911 -0
- package/dashboard/public/content/docs/architecture-overview.en.md +260 -0
- package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
- package/dashboard/public/content/docs/git-flow.en.md +525 -0
- package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
- package/dashboard/public/content/docs/hooks-reference.en.md +420 -0
- package/dashboard/public/content/docs/pipeline.en.md +400 -0
- package/dashboard/public/content/docs/quality-gate.en.md +315 -0
- package/dashboard/public/content/docs/skills-reference.en.md +500 -0
- package/dashboard/public/content/docs/stack-rules.en.md +362 -0
- package/dashboard/public/content/docs/troubleshooting.en.md +637 -0
- package/dashboard/public/content/manifest.json +102 -0
- package/dashboard/public/content/manual/backend.en.md +1138 -0
- package/dashboard/public/content/manual/existing-project.en.md +831 -0
- package/dashboard/public/content/manual/frontend.en.md +1065 -0
- package/dashboard/public/content/manual/fullstack.en.md +1508 -0
- package/dashboard/public/content/manual/mobile.en.md +866 -0
- package/dashboard/public/index.html +108 -0
- package/dashboard/public/style.css +610 -0
- package/dashboard/public/vendor/marked.min.js +69 -0
- package/dashboard/rtk.js +143 -0
- package/dashboard/server-app.js +403 -0
- package/dashboard/server.js +104 -0
- package/dashboard/test/sprint1.test.js +406 -0
- package/dashboard/test/sprint2.test.js +571 -0
- package/dashboard/test/sprint3.test.js +560 -0
- package/package.json +33 -0
- package/scripts/hooks/subagent-telemetry.sh +14 -0
- package/scripts/hooks/telemetry-writer.js +250 -0
- package/scripts/latest-versions.json +56 -0
package/bin/lib/help.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Help text for the duk CLI.
|
|
5
|
+
*
|
|
6
|
+
* Three modes:
|
|
7
|
+
* - printGeneralHelp() → full usage page (`duk help`)
|
|
8
|
+
* - printCommandHelp(cmd) → detailed help for one command
|
|
9
|
+
* - printShortHelp() → terse usage shown when called with no args
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const GENERAL_HELP = `\
|
|
13
|
+
duk — development-utility-kit CLI
|
|
14
|
+
|
|
15
|
+
Usage: duk <command> [options]
|
|
16
|
+
|
|
17
|
+
COMMANDS
|
|
18
|
+
new <name> Scaffold new project + open Cowork for conversational stack discovery
|
|
19
|
+
install Inject harness into CWD (idempotent). Detects existing stack
|
|
20
|
+
update Alias for install
|
|
21
|
+
sync-all <dir> Update harness in multiple projects under <dir>. Default dry-run
|
|
22
|
+
dashboard Start local telemetry dashboard (Express + Chart.js)
|
|
23
|
+
help [command] Show this message, or detailed help for <command>
|
|
24
|
+
|
|
25
|
+
COMMON OPTIONS
|
|
26
|
+
--help, -h Show help (this or command-specific)
|
|
27
|
+
--version, -v Show duk version
|
|
28
|
+
|
|
29
|
+
COMMAND-SPECIFIC OPTIONS
|
|
30
|
+
|
|
31
|
+
install / update:
|
|
32
|
+
--sub <dir> Install in subdirectory instead of CWD
|
|
33
|
+
--dry-run Print actions without writing
|
|
34
|
+
--check-only Show "local vs latest harness", do not update
|
|
35
|
+
--force Skip drift detection (overwrite local modifications)
|
|
36
|
+
|
|
37
|
+
sync-all:
|
|
38
|
+
--apply Execute updates (default: dry-run)
|
|
39
|
+
--filter <expr> Filter projects (repeat for AND logic):
|
|
40
|
+
stack:<lang|framework> e.g. stack:java
|
|
41
|
+
type:<value> e.g. type:fullstack
|
|
42
|
+
age:<duration> e.g. age:30d, age:6m
|
|
43
|
+
harness-version:<semver> e.g. harness-version:<0.2
|
|
44
|
+
--exclude <name> Exclude project by folder name (repeat)
|
|
45
|
+
|
|
46
|
+
dashboard:
|
|
47
|
+
--port <n> Port to bind (default: 4242)
|
|
48
|
+
--no-open Do not open browser automatically
|
|
49
|
+
|
|
50
|
+
EXAMPLES
|
|
51
|
+
duk new my-app
|
|
52
|
+
duk install
|
|
53
|
+
duk install --check-only
|
|
54
|
+
duk sync-all C:\\development\\source\\projects --filter stack:java --apply
|
|
55
|
+
duk dashboard --port 4243
|
|
56
|
+
|
|
57
|
+
Run \`duk help <command>\` for detailed information.`;
|
|
58
|
+
|
|
59
|
+
const SHORT_HELP = `\
|
|
60
|
+
duk — development-utility-kit CLI
|
|
61
|
+
|
|
62
|
+
Usage: duk <command> [options]
|
|
63
|
+
|
|
64
|
+
Commands: new | install | update | sync-all | dashboard | help
|
|
65
|
+
|
|
66
|
+
Run \`duk help\` for full usage, or \`duk help <command>\` for command details.`;
|
|
67
|
+
|
|
68
|
+
const COMMAND_HELP = {
|
|
69
|
+
new: `\
|
|
70
|
+
duk new <name> — Scaffold a new project
|
|
71
|
+
|
|
72
|
+
Creates a folder <name> in the CWD, initialises a git repo, writes a
|
|
73
|
+
CLAUDE.md template with an empty Project Identity, and injects the
|
|
74
|
+
harness (.claude/, CLAUDE.md, HARNESS_VERSION, MANIFEST, local/).
|
|
75
|
+
|
|
76
|
+
Usage:
|
|
77
|
+
duk new <name>
|
|
78
|
+
|
|
79
|
+
Arguments:
|
|
80
|
+
<name> Project folder name (kebab-case recommended).
|
|
81
|
+
Must not already exist.
|
|
82
|
+
|
|
83
|
+
After running:
|
|
84
|
+
1. cd <name>
|
|
85
|
+
2. Open Cowork or Claude Code in the new folder
|
|
86
|
+
3. First message: "sabatina pra projeto novo"
|
|
87
|
+
→ stack-discovery skill walks you through 8 questions and fills
|
|
88
|
+
the Project Identity block.
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
duk new my-app
|
|
92
|
+
duk new payment-gateway`,
|
|
93
|
+
|
|
94
|
+
install: `\
|
|
95
|
+
duk install — Inject the harness into a project
|
|
96
|
+
|
|
97
|
+
Detects the existing stack (pom.xml, package.json, etc.) and writes
|
|
98
|
+
.claude/, CLAUDE.md (preserving ## Project Identity if present),
|
|
99
|
+
HARNESS_VERSION, MANIFEST, and a .claude/local/ placeholder.
|
|
100
|
+
|
|
101
|
+
Usage:
|
|
102
|
+
duk install [options]
|
|
103
|
+
|
|
104
|
+
Options:
|
|
105
|
+
--sub <dir> Install into <dir> instead of the CWD.
|
|
106
|
+
--dry-run Print what would be done without writing.
|
|
107
|
+
--check-only Detect stack + report local-vs-latest harness;
|
|
108
|
+
do not write anything.
|
|
109
|
+
--force Skip drift detection (overwrite local changes).
|
|
110
|
+
A backup is still created.
|
|
111
|
+
--help, -h Show this help.
|
|
112
|
+
|
|
113
|
+
Drift detection:
|
|
114
|
+
install reads .claude/.MANIFEST (sha256 of every file from the prior
|
|
115
|
+
install). If any file was modified locally, install aborts and asks
|
|
116
|
+
you to either:
|
|
117
|
+
1. Move customisations to .claude/local/ (never touched), or
|
|
118
|
+
2. Run with --force to overwrite (backup is preserved), or
|
|
119
|
+
3. Open a PR on the harness repo if the change is canonical.
|
|
120
|
+
|
|
121
|
+
Examples:
|
|
122
|
+
duk install
|
|
123
|
+
duk install --sub backend
|
|
124
|
+
duk install --check-only
|
|
125
|
+
duk install --force
|
|
126
|
+
duk install --dry-run`,
|
|
127
|
+
|
|
128
|
+
update: `\
|
|
129
|
+
duk update — Alias for "duk install"
|
|
130
|
+
|
|
131
|
+
Identical to \`duk install\`. Provided for ergonomics when you are
|
|
132
|
+
updating an already-adopted project.
|
|
133
|
+
|
|
134
|
+
See \`duk help install\` for full options.`,
|
|
135
|
+
|
|
136
|
+
'sync-all': `\
|
|
137
|
+
duk sync-all <dir> — Batch update the harness in many projects
|
|
138
|
+
|
|
139
|
+
Iterates over every folder under <dir> that contains a .claude/
|
|
140
|
+
directory and runs duk install. Default is dry-run; pass --apply to
|
|
141
|
+
execute.
|
|
142
|
+
|
|
143
|
+
Usage:
|
|
144
|
+
duk sync-all <dir> [options]
|
|
145
|
+
|
|
146
|
+
Options:
|
|
147
|
+
--apply Execute the updates (default: dry-run preview).
|
|
148
|
+
--filter <expr> Filter projects (repeat for AND logic):
|
|
149
|
+
stack:<lang|framework> e.g. stack:java
|
|
150
|
+
type:<value> e.g. type:fullstack
|
|
151
|
+
age:<duration> e.g. age:30d, age:6m
|
|
152
|
+
harness-version:<semver> e.g. harness-version:<0.2
|
|
153
|
+
--exclude <name> Skip project by folder name (repeat for multiple).
|
|
154
|
+
--help, -h Show this help.
|
|
155
|
+
|
|
156
|
+
Examples:
|
|
157
|
+
duk sync-all C:\\development\\source\\projects
|
|
158
|
+
duk sync-all . --filter stack:java --apply
|
|
159
|
+
duk sync-all . --filter age:60d
|
|
160
|
+
duk sync-all . --exclude prod-critical --apply
|
|
161
|
+
duk sync-all . --filter harness-version:<0.2 --apply`,
|
|
162
|
+
|
|
163
|
+
dashboard: `\
|
|
164
|
+
duk dashboard — Start the local telemetry dashboard
|
|
165
|
+
|
|
166
|
+
Boots an Express server + Chart.js UI that visualises hook telemetry
|
|
167
|
+
from the local harness.
|
|
168
|
+
|
|
169
|
+
Usage:
|
|
170
|
+
duk dashboard [options]
|
|
171
|
+
|
|
172
|
+
Options:
|
|
173
|
+
--port <n> Port to bind to. Default: 4242.
|
|
174
|
+
--no-open Do not open the browser automatically.
|
|
175
|
+
--help, -h Show this help.
|
|
176
|
+
|
|
177
|
+
Examples:
|
|
178
|
+
duk dashboard
|
|
179
|
+
duk dashboard --port 4243
|
|
180
|
+
duk dashboard --no-open`,
|
|
181
|
+
|
|
182
|
+
help: `\
|
|
183
|
+
duk help [command] — Show help
|
|
184
|
+
|
|
185
|
+
Usage:
|
|
186
|
+
duk help Show full usage page.
|
|
187
|
+
duk help <command> Show detailed help for <command>.
|
|
188
|
+
duk <command> --help Same as \`duk help <command>\`.
|
|
189
|
+
|
|
190
|
+
Commands with detailed help:
|
|
191
|
+
new, install, update, sync-all, dashboard, help`,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Write the full general help to stdout.
|
|
196
|
+
*/
|
|
197
|
+
function printGeneralHelp() {
|
|
198
|
+
process.stdout.write(GENERAL_HELP + '\n');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Write the short usage page to stdout (used when called with no args).
|
|
203
|
+
*/
|
|
204
|
+
function printShortHelp() {
|
|
205
|
+
process.stdout.write(SHORT_HELP + '\n');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Write detailed help for a single command. Falls back to general help
|
|
210
|
+
* if the command is unknown.
|
|
211
|
+
* @param {string} command
|
|
212
|
+
*/
|
|
213
|
+
function printCommandHelp(command) {
|
|
214
|
+
if (!command) {
|
|
215
|
+
printGeneralHelp();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const normalised = command === 'update' ? 'update' : command;
|
|
219
|
+
const text = COMMAND_HELP[normalised];
|
|
220
|
+
if (!text) {
|
|
221
|
+
process.stderr.write(`Unknown command: ${command}\n\n`);
|
|
222
|
+
printGeneralHelp();
|
|
223
|
+
process.exitCode = 1;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
process.stdout.write(text + '\n');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = {
|
|
230
|
+
printGeneralHelp,
|
|
231
|
+
printShortHelp,
|
|
232
|
+
printCommandHelp,
|
|
233
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLAUDE.md `## Project Identity` helpers.
|
|
5
|
+
*
|
|
6
|
+
* - generateIdentitySection(dr) build a fresh Project Identity block
|
|
7
|
+
* - readIdentitySection(content) extract the existing block from CLAUDE.md
|
|
8
|
+
* - mergeClaudeMd(...) inject identity into the template body
|
|
9
|
+
* - emptyIdentitySection(name) placeholder identity for `duk new`
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const IDENTITY_HEADING = '## Project Identity';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build the Project Identity block pre-filled with detected values.
|
|
16
|
+
* @param {{ type: string, stackHints: string[] }} dr
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function generateIdentitySection(dr) {
|
|
20
|
+
const stackLine = dr.stackHints && dr.stackHints.length > 0
|
|
21
|
+
? dr.stackHints.join(', ')
|
|
22
|
+
: '<ex: Java 25 + Spring Boot 4 + Angular 21>';
|
|
23
|
+
return `## Project Identity
|
|
24
|
+
|
|
25
|
+
> **Modifique apenas esta seção ao adotar o plugin em um novo projeto.**
|
|
26
|
+
> Todas as demais seções são base do plugin e não devem ser alteradas diretamente
|
|
27
|
+
> — use \`update-template\` para receber atualizações.
|
|
28
|
+
|
|
29
|
+
- **Project name**: \`<project-name>\`
|
|
30
|
+
- **Project type**: \`${dr.type}\`
|
|
31
|
+
- **Primary stack**: \`${stackLine}\`
|
|
32
|
+
- **Database**: \`<ex: PostgreSQL 17 + Redis 7>\`
|
|
33
|
+
- **Domain**: \`<ex: e-commerce, fintech, healthcare>\`
|
|
34
|
+
- **Team size**: \`<ex: 3 backend, 2 frontend>\`
|
|
35
|
+
- **Additional rules**: _(deixe vazio se não houver)_`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Placeholder identity used by `duk new <name>`. All fields are TODO so
|
|
40
|
+
* the stack-discovery skill can fill them in via conversation.
|
|
41
|
+
* @param {string} name
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function emptyIdentitySection(name) {
|
|
45
|
+
return `## Project Identity
|
|
46
|
+
|
|
47
|
+
> **Modifique apenas esta seção ao adotar o plugin em um novo projeto.**
|
|
48
|
+
> Todas as demais seções são base do plugin e não devem ser alteradas diretamente
|
|
49
|
+
> — use \`update-template\` para receber atualizações.
|
|
50
|
+
|
|
51
|
+
- **Project name**: \`${name}\`
|
|
52
|
+
- **Project type**: \`<TODO: backend | frontend | fullstack | mobile | library | cli | data-pipeline>\`
|
|
53
|
+
- **Primary stack**: \`<TODO: declare via stack-discovery skill>\`
|
|
54
|
+
- **Database**: \`<TODO>\`
|
|
55
|
+
- **Domain**: \`<TODO>\`
|
|
56
|
+
- **Team size**: \`<TODO>\`
|
|
57
|
+
- **Additional rules**: _(deixe vazio se não houver)_`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract the existing ## Project Identity section from a CLAUDE.md string.
|
|
62
|
+
* Returns the full section text (from the heading to before the next ## heading),
|
|
63
|
+
* or null if not found.
|
|
64
|
+
* @param {string} content
|
|
65
|
+
* @returns {string|null}
|
|
66
|
+
*/
|
|
67
|
+
function readIdentitySection(content) {
|
|
68
|
+
const startIdx = content.indexOf(IDENTITY_HEADING);
|
|
69
|
+
if (startIdx === -1) return null;
|
|
70
|
+
|
|
71
|
+
const afterHeading = content.indexOf('\n## ', startIdx + IDENTITY_HEADING.length);
|
|
72
|
+
if (afterHeading === -1) {
|
|
73
|
+
return content.slice(startIdx).trimEnd();
|
|
74
|
+
}
|
|
75
|
+
return content.slice(startIdx, afterHeading).trimEnd();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Merge: replace the template's ## Project Identity placeholder with the
|
|
80
|
+
* preserved or generated identity section.
|
|
81
|
+
* @param {string} existing - current file content (may be empty, kept for symmetry)
|
|
82
|
+
* @param {string} template - template content from package
|
|
83
|
+
* @param {string} identity - identity section text to inject
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function mergeClaudeMd(existing, template, identity) {
|
|
87
|
+
const templateStartIdx = template.indexOf(IDENTITY_HEADING);
|
|
88
|
+
if (templateStartIdx === -1) {
|
|
89
|
+
return identity + '\n\n' + template;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const afterHeading = template.indexOf('\n## ', templateStartIdx + IDENTITY_HEADING.length);
|
|
93
|
+
let rest;
|
|
94
|
+
if (afterHeading === -1) {
|
|
95
|
+
rest = '';
|
|
96
|
+
} else {
|
|
97
|
+
rest = template.slice(afterHeading);
|
|
98
|
+
}
|
|
99
|
+
return identity + rest;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
IDENTITY_HEADING,
|
|
104
|
+
generateIdentitySection,
|
|
105
|
+
emptyIdentitySection,
|
|
106
|
+
readIdentitySection,
|
|
107
|
+
mergeClaudeMd,
|
|
108
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Provision `.claude/local/` with a README explaining the override contract
|
|
8
|
+
* (per ADR-032). Idempotent — never overwrites an existing README.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const LOCAL_README = `# .claude/local/ — project-only overrides
|
|
12
|
+
|
|
13
|
+
This directory is **never touched** by \`duk install\`. Anything you put
|
|
14
|
+
here survives every harness update.
|
|
15
|
+
|
|
16
|
+
Use it for customisations that apply ONLY to this project:
|
|
17
|
+
|
|
18
|
+
local/agents/ project-specific agent overrides
|
|
19
|
+
local/skills/ project-specific skills (non-canonical)
|
|
20
|
+
local/stacks/ proprietary or internal stack packs
|
|
21
|
+
|
|
22
|
+
Loader priority (per ADR-032 §3):
|
|
23
|
+
.claude/local/<path> wins over
|
|
24
|
+
.claude/<path>
|
|
25
|
+
|
|
26
|
+
When to use \`.claude/local/\`:
|
|
27
|
+
- Stack proprietária da empresa (ex: \`local/stacks/internal/cobol.md\`)
|
|
28
|
+
- Regra de projeto que NÃO se aplica a outros
|
|
29
|
+
- Skill experimental que pode virar canônica via PR no harness
|
|
30
|
+
|
|
31
|
+
When NOT to use:
|
|
32
|
+
- "Prefiro outro estilo" → Honcho (\`lembra que prefiro X\`)
|
|
33
|
+
- Algo que beneficia todos → PR no harness repo
|
|
34
|
+
|
|
35
|
+
Drift detection: files under \`local/\` are excluded from \`.MANIFEST\`,
|
|
36
|
+
so editing them never triggers a drift abort during \`duk install\`.
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Ensure `.claude/local/` exists with a README. Subdirs (agents/, skills/,
|
|
41
|
+
* stacks/) are NOT pre-created — they are lazy, born when the dev adds an
|
|
42
|
+
* override.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} claudeDir absolute path to .claude/
|
|
45
|
+
* @param {boolean} dryRun
|
|
46
|
+
*/
|
|
47
|
+
function ensureLocalDir(claudeDir, dryRun) {
|
|
48
|
+
const localDir = path.join(claudeDir, 'local');
|
|
49
|
+
const readmePath = path.join(localDir, 'README.md');
|
|
50
|
+
|
|
51
|
+
if (dryRun) {
|
|
52
|
+
if (!fs.existsSync(localDir)) {
|
|
53
|
+
process.stdout.write(' [dry-run] would create .claude/local/ with README.md\n');
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!fs.existsSync(localDir)) {
|
|
59
|
+
fs.mkdirSync(localDir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
if (!fs.existsSync(readmePath)) {
|
|
62
|
+
fs.writeFileSync(readmePath, LOCAL_README, 'utf8');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
ensureLocalDir,
|
|
68
|
+
LOCAL_README,
|
|
69
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `.claude/.MANIFEST` — hash-based drift detection per ADR-032.
|
|
9
|
+
*
|
|
10
|
+
* generateManifest(claudeDir) walk .claude/ and return Map<relPath, sha256>
|
|
11
|
+
* writeManifest(claudeDir, harnessVersion)
|
|
12
|
+
* loadManifest(claudeDir) parse .claude/.MANIFEST or return null
|
|
13
|
+
* detectDrift(claudeDir) compare current files vs stored manifest
|
|
14
|
+
*
|
|
15
|
+
* Paths excluded from the manifest (managed independently):
|
|
16
|
+
* - local/ (per-project overrides — never touched by install)
|
|
17
|
+
* - .MANIFEST (the manifest file itself)
|
|
18
|
+
* - HARNESS_VERSION
|
|
19
|
+
* - Anything under .claude.bak / .claude.backup-* (lives outside .claude/)
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const MANIFEST_FILENAME = '.MANIFEST';
|
|
23
|
+
const HARNESS_VERSION_FILENAME = 'HARNESS_VERSION';
|
|
24
|
+
const LOCAL_DIR = 'local';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Walk dir recursively, returning relative POSIX-style paths of files,
|
|
28
|
+
* skipping any directory or filename that matches `shouldSkip(name, relPath)`.
|
|
29
|
+
* @param {string} root
|
|
30
|
+
* @param {(name: string, relPath: string) => boolean} shouldSkip
|
|
31
|
+
* @returns {string[]} sorted relative paths
|
|
32
|
+
*/
|
|
33
|
+
function walkFiles(root, shouldSkip) {
|
|
34
|
+
const out = [];
|
|
35
|
+
|
|
36
|
+
function recur(absDir, relDir) {
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = fs.readdirSync(absDir, { withFileTypes: true });
|
|
40
|
+
} catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
45
|
+
if (shouldSkip(entry.name, relPath)) continue;
|
|
46
|
+
const absPath = path.join(absDir, entry.name);
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
recur(absPath, relPath);
|
|
49
|
+
} else if (entry.isFile()) {
|
|
50
|
+
out.push(relPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
recur(root, '');
|
|
56
|
+
out.sort();
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Return sha256 hex of a file's bytes.
|
|
62
|
+
* @param {string} absPath
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function sha256OfFile(absPath) {
|
|
66
|
+
const buf = fs.readFileSync(absPath);
|
|
67
|
+
return crypto.createHash('sha256').update(buf).digest('hex');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Default skip rule for manifest walks.
|
|
72
|
+
* @param {string} name
|
|
73
|
+
* @param {string} relPath
|
|
74
|
+
* @returns {boolean}
|
|
75
|
+
*/
|
|
76
|
+
function defaultSkip(name, relPath) {
|
|
77
|
+
// Skip top-level local/, MANIFEST, HARNESS_VERSION
|
|
78
|
+
if (relPath === LOCAL_DIR) return true;
|
|
79
|
+
if (relPath.startsWith(`${LOCAL_DIR}/`)) return true;
|
|
80
|
+
if (relPath === MANIFEST_FILENAME) return true;
|
|
81
|
+
if (relPath === HARNESS_VERSION_FILENAME) return true;
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build a Map<relPath, sha256> for every file under .claude/ except the
|
|
87
|
+
* exclusions listed above.
|
|
88
|
+
* @param {string} claudeDir absolute path to the .claude/ directory
|
|
89
|
+
* @returns {Map<string, string>}
|
|
90
|
+
*/
|
|
91
|
+
function generateManifest(claudeDir) {
|
|
92
|
+
const result = new Map();
|
|
93
|
+
if (!fs.existsSync(claudeDir)) return result;
|
|
94
|
+
const files = walkFiles(claudeDir, defaultSkip);
|
|
95
|
+
for (const rel of files) {
|
|
96
|
+
const abs = path.join(claudeDir, rel);
|
|
97
|
+
result.set(rel, sha256OfFile(abs));
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Render a manifest Map to the on-disk format and write it to
|
|
104
|
+
* `<claudeDir>/.MANIFEST`.
|
|
105
|
+
* @param {string} claudeDir
|
|
106
|
+
* @param {string} harnessVersion
|
|
107
|
+
* @param {Map<string, string>} [manifest] - pre-computed; if omitted, regenerates
|
|
108
|
+
*/
|
|
109
|
+
function writeManifest(claudeDir, harnessVersion, manifest) {
|
|
110
|
+
const map = manifest || generateManifest(claudeDir);
|
|
111
|
+
const ts = new Date().toISOString();
|
|
112
|
+
const header = [
|
|
113
|
+
'# .claude/.MANIFEST',
|
|
114
|
+
`# Auto-generated by duk install at ${ts}.`,
|
|
115
|
+
`# Harness version: ${harnessVersion}`,
|
|
116
|
+
'# Format: <relative-path> <sha256>',
|
|
117
|
+
'# Do not edit by hand.',
|
|
118
|
+
];
|
|
119
|
+
const lines = [...map.entries()]
|
|
120
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
121
|
+
.map(([rel, sha]) => `${rel} ${sha}`);
|
|
122
|
+
const body = header.concat(lines).join('\n') + '\n';
|
|
123
|
+
const target = path.join(claudeDir, MANIFEST_FILENAME);
|
|
124
|
+
fs.writeFileSync(target, body, 'utf8');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Read and parse `.claude/.MANIFEST`. Returns null if missing or unreadable.
|
|
129
|
+
* @param {string} claudeDir
|
|
130
|
+
* @returns {Map<string, string> | null}
|
|
131
|
+
*/
|
|
132
|
+
function loadManifest(claudeDir) {
|
|
133
|
+
const target = path.join(claudeDir, MANIFEST_FILENAME);
|
|
134
|
+
if (!fs.existsSync(target)) return null;
|
|
135
|
+
|
|
136
|
+
let content;
|
|
137
|
+
try {
|
|
138
|
+
content = fs.readFileSync(target, 'utf8');
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const map = new Map();
|
|
144
|
+
for (const line of content.split(/\r?\n/)) {
|
|
145
|
+
if (!line || line.startsWith('#')) continue;
|
|
146
|
+
// last whitespace-separated chunk is the sha; rest is path
|
|
147
|
+
const idx = line.lastIndexOf(' ');
|
|
148
|
+
if (idx === -1) continue;
|
|
149
|
+
const rel = line.slice(0, idx).trim();
|
|
150
|
+
const sha = line.slice(idx + 1).trim();
|
|
151
|
+
if (rel && /^[a-f0-9]{64}$/i.test(sha)) {
|
|
152
|
+
map.set(rel, sha);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return map;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Compare current files in .claude/ against the stored manifest. Returns
|
|
160
|
+
* { hasManifest, drifted, added, removed }
|
|
161
|
+
* where each list contains relative paths.
|
|
162
|
+
*
|
|
163
|
+
* - hasManifest=false → no baseline (first install ever) → not blocking.
|
|
164
|
+
* - drifted → file present in both but sha differs.
|
|
165
|
+
* - added → file present on disk but not in manifest.
|
|
166
|
+
* - removed → file in manifest but missing on disk.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} claudeDir
|
|
169
|
+
* @returns {{ hasManifest: boolean, drifted: string[], added: string[], removed: string[] }}
|
|
170
|
+
*/
|
|
171
|
+
function detectDrift(claudeDir) {
|
|
172
|
+
const stored = loadManifest(claudeDir);
|
|
173
|
+
if (stored === null) {
|
|
174
|
+
return { hasManifest: false, drifted: [], added: [], removed: [] };
|
|
175
|
+
}
|
|
176
|
+
const current = generateManifest(claudeDir);
|
|
177
|
+
|
|
178
|
+
const drifted = [];
|
|
179
|
+
const added = [];
|
|
180
|
+
const removed = [];
|
|
181
|
+
|
|
182
|
+
for (const [rel, sha] of current.entries()) {
|
|
183
|
+
if (!stored.has(rel)) {
|
|
184
|
+
added.push(rel);
|
|
185
|
+
} else if (stored.get(rel) !== sha) {
|
|
186
|
+
drifted.push(rel);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const rel of stored.keys()) {
|
|
190
|
+
if (!current.has(rel)) {
|
|
191
|
+
removed.push(rel);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
drifted.sort();
|
|
196
|
+
added.sort();
|
|
197
|
+
removed.sort();
|
|
198
|
+
return { hasManifest: true, drifted, added, removed };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Write the plain-text HARNESS_VERSION file (single line, semver).
|
|
203
|
+
* @param {string} claudeDir
|
|
204
|
+
* @param {string} version
|
|
205
|
+
*/
|
|
206
|
+
function writeHarnessVersion(claudeDir, version) {
|
|
207
|
+
const target = path.join(claudeDir, HARNESS_VERSION_FILENAME);
|
|
208
|
+
fs.writeFileSync(target, version + '\n', 'utf8');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Read HARNESS_VERSION, returning null if missing.
|
|
213
|
+
* @param {string} claudeDir
|
|
214
|
+
* @returns {string | null}
|
|
215
|
+
*/
|
|
216
|
+
function readHarnessVersion(claudeDir) {
|
|
217
|
+
const target = path.join(claudeDir, HARNESS_VERSION_FILENAME);
|
|
218
|
+
if (!fs.existsSync(target)) return null;
|
|
219
|
+
try {
|
|
220
|
+
return fs.readFileSync(target, 'utf8').trim();
|
|
221
|
+
} catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = {
|
|
227
|
+
MANIFEST_FILENAME,
|
|
228
|
+
HARNESS_VERSION_FILENAME,
|
|
229
|
+
LOCAL_DIR,
|
|
230
|
+
generateManifest,
|
|
231
|
+
writeManifest,
|
|
232
|
+
loadManifest,
|
|
233
|
+
detectDrift,
|
|
234
|
+
writeHarnessVersion,
|
|
235
|
+
readHarnessVersion,
|
|
236
|
+
};
|