lemmafit 0.0.1 → 0.2.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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -4
  3. package/blank-template/README.md +3 -0
  4. package/blank-template/SPEC.yaml +1 -0
  5. package/blank-template/index.html +12 -0
  6. package/blank-template/lemmafit/.vibe/config.json +5 -0
  7. package/blank-template/lemmafit/dafny/Domain.dfy +5 -0
  8. package/blank-template/lemmafit/dafny/Replay.dfy +147 -0
  9. package/blank-template/package.json +25 -0
  10. package/blank-template/src/App.css +3 -0
  11. package/blank-template/src/App.tsx +10 -0
  12. package/blank-template/src/dafny/.gitkeep +0 -0
  13. package/blank-template/src/index.css +29 -0
  14. package/blank-template/src/main.tsx +10 -0
  15. package/blank-template/src/vite-env.d.ts +6 -0
  16. package/blank-template/template.gitignore +3 -0
  17. package/blank-template/tsconfig.json +21 -0
  18. package/blank-template/tsconfig.node.json +11 -0
  19. package/blank-template/vite.config.js +9 -0
  20. package/cli/context-hook.js +103 -0
  21. package/cli/daemon.js +24 -0
  22. package/cli/download-dafny2js.js +136 -0
  23. package/cli/generate-guarantees-md.js +223 -0
  24. package/cli/lemmafit.js +385 -0
  25. package/cli/session-hook.js +74 -0
  26. package/cli/sync.js +168 -0
  27. package/cli/verify-hook.js +221 -0
  28. package/commands/guarantees.md +138 -0
  29. package/docs/CLAUDE_INSTRUCTIONS.md +137 -0
  30. package/kernels/Replay.dfy +147 -0
  31. package/lib/daemon-client.js +54 -0
  32. package/lib/daemon.js +990 -0
  33. package/lib/download-dafny.js +130 -0
  34. package/lib/log.js +32 -0
  35. package/lib/spawn-claude.js +51 -0
  36. package/package.json +49 -5
  37. package/skills/lemmafit-dafny/SKILL.md +101 -0
  38. package/skills/lemmafit-post-react-audit/SKILL.md +46 -0
  39. package/skills/lemmafit-pre-react-audits/SKILL.md +67 -0
  40. package/skills/lemmafit-proofs/SKILL.md +24 -0
  41. package/skills/lemmafit-react-pattern/SKILL.md +62 -0
  42. package/skills/lemmafit-spec/SKILL.md +71 -0
  43. package/index.js +0 -5
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Lemmafit CLI - Verified vibe coding with Claude Code
4
+ *
5
+ * Commands:
6
+ * lemmafit init [dir] - Initialize a new lemmafit project (blank)
7
+ * lemmafit init --template <name> [dir] - Initialize from a named template
8
+ * lemmafit init --server <url|none> [dir] - Use a custom server (default: none)
9
+ * lemmafit add [Name] - Add a verified module (or just bootstrap infrastructure)
10
+ * lemmafit add <Name> --null-options - Add with Option<T> → T | null mapping
11
+ * lemmafit add <Name> --no-json-api - Add without JSON marshalling
12
+ * lemmafit add <Name> --target <target> - Set compilation target (client|node|inline|deno|cloudflare)
13
+ * lemmafit sync [dir] - Sync system files from current package version
14
+ * lemmafit daemon [dir] - Run the verification daemon
15
+ * lemmafit logs [dir] - View the dev log
16
+ * lemmafit logs --clear [dir] - Clear the dev log
17
+ * lemmafit dashboard [dir] - Open the dashboard in a browser
18
+ */
19
+
20
+ const path = require('path');
21
+ const fs = require('fs');
22
+ const crypto = require('crypto');
23
+
24
+ const TEMPLATES_BASE = path.join(__dirname, '..');
25
+ const DEFAULT_TEMPLATE = 'blank-template';
26
+ const DEFAULT_SERVER = 'none';
27
+
28
+ function resolveTemplate(name) {
29
+ const templateDir = path.join(TEMPLATES_BASE, name);
30
+ if (!fs.existsSync(templateDir)) {
31
+ const available = fs.readdirSync(TEMPLATES_BASE)
32
+ .filter(f => f.endsWith('-template') && fs.statSync(path.join(TEMPLATES_BASE, f)).isDirectory());
33
+ console.error(`Error: Unknown template '${name}'`);
34
+ console.error(`Available templates: ${available.join(', ')}`);
35
+ process.exit(1);
36
+ }
37
+ return templateDir;
38
+ }
39
+
40
+ function copyDir(src, dest) {
41
+ fs.mkdirSync(dest, { recursive: true });
42
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
43
+ const srcPath = path.join(src, entry.name);
44
+ const destName = entry.name === 'template.gitignore' ? '.gitignore' : entry.name;
45
+ const destPath = path.join(dest, destName);
46
+ if (entry.isDirectory()) {
47
+ copyDir(srcPath, destPath);
48
+ } else {
49
+ fs.copyFileSync(srcPath, destPath);
50
+ }
51
+ }
52
+ }
53
+
54
+ function syncProject(targetDir) {
55
+ const absTarget = path.resolve(targetDir);
56
+ const { syncProject: sync } = require('./sync');
57
+ sync(absTarget);
58
+ }
59
+
60
+ function initProject(targetDir, templateName, serverBase) {
61
+ const absTarget = path.resolve(targetDir);
62
+ const templateDir = resolveTemplate(templateName);
63
+
64
+ const ignorable = new Set(['.git', '.DS_Store']);
65
+ if (fs.existsSync(absTarget) && fs.readdirSync(absTarget).some(f => !ignorable.has(f))) {
66
+ console.error(`Error: Directory '${absTarget}' is not empty`);
67
+ process.exit(1);
68
+ }
69
+
70
+ console.log(`Creating lemmafit project at ${absTarget} (template: ${templateName})...`);
71
+
72
+ // Copy template (user-owned files only)
73
+ copyDir(templateDir, absTarget);
74
+
75
+ // Rewrite lemmafit dependency to point to local package
76
+ const pkgJsonPath = path.join(absTarget, 'package.json');
77
+ if (fs.existsSync(pkgJsonPath)) {
78
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
79
+ if (pkg.dependencies && pkg.dependencies.lemmafit) {
80
+ const lemmaPackageDir = path.resolve(__dirname, '..');
81
+ const relPath = path.relative(absTarget, lemmaPackageDir);
82
+ pkg.dependencies.lemmafit = `file:${relPath}`;
83
+ fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + '\n');
84
+ }
85
+ }
86
+
87
+ // Sync system files (.claude/settings.json, .claude/CLAUDE.md)
88
+ syncProject(absTarget);
89
+
90
+ // Generate per-project secret and server URL, write into .vibe/config.json
91
+ const secret = 'lf_sk_' + crypto.randomBytes(32).toString('hex');
92
+ const projectName = path.basename(absTarget);
93
+ const vibeDir = path.join(absTarget, 'lemmafit', '.vibe');
94
+ const configPath = path.join(vibeDir, 'config.json');
95
+ fs.mkdirSync(vibeDir, { recursive: true });
96
+ let config = {};
97
+ if (fs.existsSync(configPath)) {
98
+ try { config = JSON.parse(fs.readFileSync(configPath, 'utf8')); } catch {}
99
+ }
100
+ config.secret = secret;
101
+ if (serverBase.toLowerCase() !== 'none') {
102
+ const serverWsUrl = `${serverBase}/ws?project=${encodeURIComponent(projectName)}`;
103
+ config.server = serverWsUrl;
104
+ }
105
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
106
+
107
+ console.log('Done! Next steps:');
108
+ console.log('');
109
+ console.log(` cd ${targetDir}`);
110
+ console.log(' npm install # Downloads Dafny, installs deps, syncs hooks');
111
+ console.log(' npm run daemon # In one terminal, start the verification daemon');
112
+ console.log(' npm run dev # In another terminal, start the Vite dev server');
113
+ if (serverBase.toLowerCase() !== 'none') {
114
+ console.log(' lemmafit dashboard # Open the dashboard');
115
+ }
116
+ console.log('');
117
+ if (serverBase.toLowerCase() !== 'none') {
118
+ console.log(`Server: ${serverBase}`);
119
+ }
120
+ console.log('Then open Claude Code in the project directory.');
121
+ console.log('');
122
+ }
123
+
124
+ function addModule(targetDir, moduleName, options = {}) {
125
+ const absTarget = path.resolve(targetDir);
126
+
127
+ const lemmafitDir = path.join(absTarget, 'lemmafit');
128
+ const vibeDir = path.join(lemmafitDir, '.vibe');
129
+ const dafnyDir = path.join(lemmafitDir, 'dafny');
130
+ const configPath = path.join(vibeDir, 'config.json');
131
+ const modulesPath = path.join(vibeDir, 'modules.json');
132
+ const isFirstRun = !fs.existsSync(lemmafitDir);
133
+
134
+ // First run: bootstrap lemmafit infrastructure
135
+ if (isFirstRun) {
136
+ console.log('First lemmafit module — bootstrapping infrastructure...');
137
+ fs.mkdirSync(dafnyDir, { recursive: true });
138
+ fs.mkdirSync(vibeDir, { recursive: true });
139
+
140
+ // Minimal config (no entry/appCore since we use modules.json)
141
+ fs.writeFileSync(configPath, JSON.stringify({}, null, 2) + '\n');
142
+
143
+ // Empty modules array
144
+ fs.writeFileSync(modulesPath, JSON.stringify([], null, 2) + '\n');
145
+
146
+ // Sync .claude/ system files
147
+ syncProject(absTarget);
148
+
149
+ // Add lemmafit as devDependency and daemon script to package.json
150
+ // Create a minimal package.json if one doesn't exist
151
+ const pkgJsonPath = path.join(absTarget, 'package.json');
152
+ if (!fs.existsSync(pkgJsonPath)) {
153
+ const dirName = path.basename(absTarget);
154
+ fs.writeFileSync(pkgJsonPath, JSON.stringify({
155
+ name: dirName,
156
+ private: true
157
+ }, null, 2) + '\n');
158
+ console.log(' Created package.json');
159
+ }
160
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
161
+ if (!pkg.devDependencies) pkg.devDependencies = {};
162
+ if (!pkg.devDependencies.lemmafit && !(pkg.dependencies && pkg.dependencies.lemmafit)) {
163
+ const lemmaPackageDir = path.resolve(__dirname, '..');
164
+ const relPath = path.relative(absTarget, lemmaPackageDir);
165
+ pkg.devDependencies.lemmafit = `file:${relPath}`;
166
+ }
167
+ if (!pkg.scripts) pkg.scripts = {};
168
+ if (!pkg.scripts.daemon) {
169
+ pkg.scripts.daemon = 'lemmafit daemon';
170
+ }
171
+ fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2) + '\n');
172
+
173
+ console.log(' Created lemmafit/dafny/');
174
+ console.log(' Created lemmafit/.vibe/config.json');
175
+ console.log(' Created lemmafit/.vibe/modules.json');
176
+ console.log(' Synced .claude/ system files');
177
+ console.log('');
178
+ }
179
+
180
+ if (!moduleName) return;
181
+
182
+ // Load existing modules
183
+ let modules = [];
184
+ if (fs.existsSync(modulesPath)) {
185
+ try { modules = JSON.parse(fs.readFileSync(modulesPath, 'utf8')); } catch {}
186
+ }
187
+
188
+ // Check for duplicate
189
+ if (modules.some(m => m.outputName === moduleName)) {
190
+ console.error(`Error: Module '${moduleName}' already exists in modules.json`);
191
+ process.exit(1);
192
+ }
193
+
194
+ // Scaffold the Dafny file
195
+ const dafnyFile = path.join(dafnyDir, `${moduleName}.dfy`);
196
+ if (fs.existsSync(dafnyFile)) {
197
+ console.error(`Error: ${path.relative(absTarget, dafnyFile)} already exists`);
198
+ process.exit(1);
199
+ }
200
+
201
+ const dafnyContent = `module ${moduleName} {
202
+ // Your verified logic here
203
+ }
204
+ `;
205
+ fs.mkdirSync(dafnyDir, { recursive: true });
206
+ fs.writeFileSync(dafnyFile, dafnyContent);
207
+
208
+ // Add entry to modules.json
209
+ const moduleEntry = {
210
+ entry: `lemmafit/dafny/${moduleName}.dfy`,
211
+ appCore: moduleName,
212
+ outputName: moduleName,
213
+ jsonApi: options.jsonApi !== false,
214
+ nullOptions: options.nullOptions || false
215
+ };
216
+ if (options.target) moduleEntry.target = options.target;
217
+ modules.push(moduleEntry);
218
+ fs.writeFileSync(modulesPath, JSON.stringify(modules, null, 2) + '\n');
219
+
220
+ // Print results
221
+ console.log(` Created lemmafit/dafny/${moduleName}.dfy`);
222
+ console.log(` Added to lemmafit/.vibe/modules.json`);
223
+ console.log('');
224
+ console.log(` Next: write your verified logic in ${moduleName}.dfy`);
225
+ console.log(` The daemon will compile it to src/dafny/${moduleName}.ts`);
226
+ console.log(` Import with: import ${moduleName} from './src/dafny/${moduleName}.ts'`);
227
+ console.log('');
228
+ if (modules.length > 1) {
229
+ console.log(` Modules: ${modules.map(m => m.outputName).join(', ')}`);
230
+ console.log('');
231
+ }
232
+ }
233
+
234
+ function showLogs(targetDir, clear) {
235
+ const absTarget = path.resolve(targetDir);
236
+ const logPath = path.join(absTarget, 'lemmafit', 'logs', 'lemmafit.log');
237
+
238
+ if (clear) {
239
+ try {
240
+ fs.unlinkSync(logPath);
241
+ console.log('Cleared lemmafit/logs/lemmafit.log');
242
+ } catch {
243
+ console.log('No log file to clear.');
244
+ }
245
+ return;
246
+ }
247
+
248
+ try {
249
+ const contents = fs.readFileSync(logPath, 'utf8');
250
+ process.stdout.write(contents);
251
+ } catch {
252
+ console.log('No log file found. Logs will appear after hooks run.');
253
+ }
254
+ }
255
+
256
+ function openDashboard(targetDir) {
257
+ const absTarget = path.resolve(targetDir);
258
+ const configPath = path.join(absTarget, 'lemmafit', '.vibe', 'config.json');
259
+
260
+ if (!fs.existsSync(configPath)) {
261
+ console.error('Error: No lemmafit config found. Run "lemmafit init" first.');
262
+ process.exit(1);
263
+ }
264
+
265
+ let config;
266
+ try {
267
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
268
+ } catch {
269
+ console.error('Error: Could not read lemmafit/.vibe/config.json');
270
+ process.exit(1);
271
+ }
272
+
273
+ if (!config.server) {
274
+ console.error('Error: No "server" field in config. Add a server URL to lemmafit/.vibe/config.json');
275
+ process.exit(1);
276
+ }
277
+
278
+ // Derive HTTP dashboard URL from the WS server URL
279
+ // e.g. ws://localhost:8787/ws?project=foo -> http://localhost:8787/
280
+ // No secret in URL — user authenticates via Supabase in the browser
281
+ const wsUrl = new URL(config.server);
282
+ const dashboardUrl = new URL(`${wsUrl.protocol === 'wss:' ? 'https' : 'http'}://${wsUrl.host}/`);
283
+
284
+ const project = wsUrl.searchParams.get('project') || 'default';
285
+ dashboardUrl.hash = `project=${encodeURIComponent(project)}`;
286
+
287
+ const url = dashboardUrl.toString();
288
+ console.log(`Opening dashboard: ${url}`);
289
+ console.log('');
290
+ console.log('To register this project, use these credentials:');
291
+ console.log(` Project: ${project}`);
292
+ console.log(` Secret: ${config.secret}`);
293
+ console.log('');
294
+
295
+ // Open in default browser
296
+ const { spawn } = require('child_process');
297
+ const openCmd = process.platform === 'darwin' ? 'open'
298
+ : process.platform === 'win32' ? 'start'
299
+ : 'xdg-open';
300
+ spawn(openCmd, [url]);
301
+ }
302
+
303
+ function runDaemon(targetDir) {
304
+ const absTarget = path.resolve(targetDir);
305
+ const { Daemon } = require('../lib/daemon');
306
+ const daemon = new Daemon(absTarget);
307
+ daemon.watch();
308
+ }
309
+
310
+ // Parse arguments
311
+ const args = process.argv.slice(2);
312
+ const command = args[0];
313
+ const clearFlag = args.includes('--clear');
314
+ const nullOptionsFlag = args.includes('--null-options');
315
+ const noJsonApiFlag = args.includes('--no-json-api');
316
+ const targetIdx = args.indexOf('--target');
317
+ const targetFlag = targetIdx !== -1 ? args[targetIdx + 1] : null;
318
+ const templateIdx = args.indexOf('--template');
319
+ const templateName = templateIdx !== -1 ? args[templateIdx + 1] : DEFAULT_TEMPLATE;
320
+ const serverIdx = args.indexOf('--server');
321
+ const serverBase = serverIdx !== -1 ? args[serverIdx + 1] : DEFAULT_SERVER;
322
+ const positionalArgs = args.filter((a, i) =>
323
+ a !== '--clear' && a !== '--template' && a !== '--server' &&
324
+ a !== '--null-options' && a !== '--no-json-api' && a !== '--target' &&
325
+ (targetIdx === -1 || i !== targetIdx + 1) &&
326
+ (templateIdx === -1 || i !== templateIdx + 1) &&
327
+ (serverIdx === -1 || i !== serverIdx + 1)
328
+ ).slice(1);
329
+ let addModuleName = null;
330
+ let target;
331
+ if (command === 'add') {
332
+ addModuleName = positionalArgs[0] || null;
333
+ target = '.';
334
+ } else {
335
+ target = positionalArgs[0] || '.';
336
+ }
337
+
338
+ switch (command) {
339
+ case 'init':
340
+ initProject(target, templateName, serverBase);
341
+ break;
342
+ case 'add':
343
+ addModule(target, addModuleName, {
344
+ jsonApi: !noJsonApiFlag,
345
+ nullOptions: nullOptionsFlag,
346
+ target: targetFlag
347
+ });
348
+ break;
349
+ case 'sync':
350
+ syncProject(target);
351
+ break;
352
+ case 'daemon':
353
+ runDaemon(target);
354
+ break;
355
+ case 'dashboard':
356
+ openDashboard(target);
357
+ break;
358
+ case 'logs':
359
+ showLogs(target, clearFlag);
360
+ break;
361
+ case undefined:
362
+ case '--help':
363
+ case '-h':
364
+ console.log('Lemmafit - Verified vibe coding with Claude Code');
365
+ console.log('');
366
+ console.log('Usage:');
367
+ console.log(' lemmafit init [dir] - Create a new project (blank template)');
368
+ console.log(' lemmafit init --template <name> [dir] - Create from a named template');
369
+ console.log(' lemmafit init --server <url> [dir] - Use a custom server (default: none)');
370
+ console.log(' lemmafit add [Name] - Add a verified module (or just bootstrap infrastructure)');
371
+ console.log(' lemmafit add <Name> --null-options - Add with Option<T> → T | null mapping');
372
+ console.log(' lemmafit add <Name> --no-json-api - Add without JSON marshalling');
373
+ console.log(' lemmafit add <Name> --target <target> - Set compilation target (client|node|inline|deno|cloudflare)');
374
+ console.log(' lemmafit sync [dir] - Sync system files from package');
375
+ console.log(' lemmafit daemon [dir] - Run the verification daemon');
376
+ console.log(' lemmafit dashboard [dir] - Open the dashboard in a browser');
377
+ console.log(' lemmafit logs [dir] - View the dev log');
378
+ console.log(' lemmafit logs --clear [dir] - Clear the dev log');
379
+ console.log('');
380
+ break;
381
+ default:
382
+ console.error(`Unknown command: ${command}`);
383
+ console.error('Run "lemmafit --help" for usage');
384
+ process.exit(1);
385
+ }
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Claude Code SessionStart hook for lemmafit.
4
+ *
5
+ * Fires once at the start of each session. Tells Claude to read
6
+ * the lemmafit instructions file from the installed package.
7
+ *
8
+ * Hook receives JSON on stdin with { "cwd": "..." }
9
+ */
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const { initLog, log } = require('../lib/log');
14
+
15
+ async function readStdin() {
16
+ const chunks = [];
17
+ for await (const chunk of process.stdin) {
18
+ chunks.push(chunk);
19
+ }
20
+ return Buffer.concat(chunks).toString('utf8');
21
+ }
22
+
23
+ function findProjectRoot(dir) {
24
+ let current = dir;
25
+ while (current !== path.dirname(current)) {
26
+ if (fs.existsSync(path.join(current, 'lemmafit'))) {
27
+ return current;
28
+ }
29
+ current = path.dirname(current);
30
+ }
31
+ return null;
32
+ }
33
+
34
+ async function main() {
35
+ const input = await readStdin();
36
+
37
+ let hookData;
38
+ try {
39
+ hookData = JSON.parse(input);
40
+ } catch {
41
+ process.exit(0);
42
+ }
43
+
44
+ const cwd = hookData.cwd;
45
+ if (!cwd) {
46
+ process.exit(0);
47
+ }
48
+
49
+ const projectDir = findProjectRoot(cwd);
50
+ if (!projectDir) {
51
+ process.exit(0);
52
+ }
53
+
54
+ initLog(projectDir);
55
+ log('session', 'Session started');
56
+
57
+ const instructionsPath = path.join(projectDir, 'node_modules', 'lemmafit', 'docs', 'CLAUDE_INSTRUCTIONS.md');
58
+ if (fs.existsSync(instructionsPath)) {
59
+ log('session', `Injecting instructions from ${instructionsPath}`);
60
+ const context = `<lemmafit-instructions>\nRead and follow the project instructions at: ${instructionsPath}\n</lemmafit-instructions>`;
61
+ console.log(JSON.stringify({
62
+ hookSpecificOutput: {
63
+ hookEventName: 'SessionStart',
64
+ additionalContext: context
65
+ }
66
+ }));
67
+ } else {
68
+ log('session', 'Instructions file not found (node_modules/lemmafit not installed?)');
69
+ }
70
+ }
71
+
72
+ main().catch(() => {
73
+ process.exit(0);
74
+ });
package/cli/sync.js ADDED
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Lemmafit sync - writes system-owned files into a lemmafit project.
4
+ *
5
+ * Runs automatically as postinstall when lemmafit is installed/updated.
6
+ * Also available as `lemmafit sync [dir]`.
7
+ *
8
+ * System files written:
9
+ * .claude/settings.json - Claude Code hook configuration
10
+ * .claude/CLAUDE.md - Pointer to package instructions (append-safe)
11
+ */
12
+
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+ const { initLog, log } = require('../lib/log');
16
+
17
+ const POINTER_LINE =
18
+ `============================== LEMMAFIT SYSTEM PROMPT - DO NOT EDIT ====================================
19
+ This project uses lemmafit to formally verify logic.
20
+ You MUST read the lemmafit project instructions at node_modules/lemmafit/docs/CLAUDE_INSTRUCTIONS.md before writing any code.
21
+ ===============================================================================================
22
+ `;
23
+
24
+ const SETTINGS = {
25
+ permissions: {
26
+ allow: [
27
+ // Lemmafit project files Claude needs to read/write without prompting
28
+ "Read(SPEC.yaml)",
29
+ "Read(lemmafit/**)",
30
+ "Read(src/dafny/**)",
31
+ "Read(node_modules/lemmafit/docs/**)",
32
+ "Edit(SPEC.yaml)",
33
+ "Edit(lemmafit/dafny/*.dfy)",
34
+ "Write(SPEC.yaml)",
35
+ "Write(lemmafit/dafny/*.dfy)",
36
+ // Common build/dev commands
37
+ "Bash(npm run build:*)",
38
+ "Bash(npm run dev:*)",
39
+ "Bash(npx tsc:*)"
40
+ ]
41
+ },
42
+ hooks: {
43
+ PostToolUse: [
44
+ {
45
+ matcher: "Write|Edit",
46
+ hooks: [
47
+ { type: "command", command: "lemmafit-verify-hook" }
48
+ ]
49
+ }
50
+ ],
51
+ UserPromptSubmit: [
52
+ {
53
+ hooks: [
54
+ { type: "command", command: "lemmafit-context-hook" }
55
+ ]
56
+ }
57
+ ],
58
+ SessionStart: [
59
+ {
60
+ hooks: [
61
+ { type: "command", command: "lemmafit-session-hook" }
62
+ ]
63
+ }
64
+ ]
65
+ }
66
+ };
67
+
68
+ function findProjectRoot() {
69
+ // When run as postinstall, CWD is inside node_modules/lemmafit.
70
+ // Walk up to find the project root (has package.json but is not this package).
71
+ let dir = process.cwd();
72
+ while (dir !== path.dirname(dir)) {
73
+ const pkgPath = path.join(dir, 'package.json');
74
+ if (fs.existsSync(pkgPath)) {
75
+ try {
76
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
77
+ if (pkg.name !== 'lemmafit') {
78
+ return dir;
79
+ }
80
+ } catch {}
81
+ }
82
+ dir = path.dirname(dir);
83
+ }
84
+ return null;
85
+ }
86
+
87
+ function syncProject(targetDir) {
88
+ const absTarget = path.resolve(targetDir);
89
+
90
+ initLog(absTarget);
91
+ log('sync', 'Starting sync');
92
+
93
+ // Write .claude/settings.json
94
+ const claudeDir = path.join(absTarget, '.claude');
95
+ fs.mkdirSync(claudeDir, { recursive: true });
96
+
97
+ fs.writeFileSync(
98
+ path.join(claudeDir, 'settings.json'),
99
+ JSON.stringify(SETTINGS, null, 2) + '\n'
100
+ );
101
+
102
+ // Write or append .claude/CLAUDE.md
103
+ const claudeMdPath = path.join(claudeDir, 'CLAUDE.md');
104
+ if (fs.existsSync(claudeMdPath)) {
105
+ const existing = fs.readFileSync(claudeMdPath, 'utf8');
106
+ if (!existing.includes(POINTER_LINE)) {
107
+ fs.appendFileSync(claudeMdPath, '\n' + POINTER_LINE + '\n');
108
+ }
109
+ } else {
110
+ fs.writeFileSync(claudeMdPath, POINTER_LINE + '\n');
111
+ }
112
+
113
+ // Sync .claude/commands/ from package
114
+ const srcCommands = path.join(__dirname, '..', 'commands');
115
+ if (fs.existsSync(srcCommands)) {
116
+ const commandsDir = path.join(claudeDir, 'commands');
117
+ fs.mkdirSync(commandsDir, { recursive: true });
118
+ for (const file of fs.readdirSync(srcCommands)) {
119
+ fs.copyFileSync(
120
+ path.join(srcCommands, file),
121
+ path.join(commandsDir, file)
122
+ );
123
+ }
124
+ }
125
+
126
+ // Sync .claude/skills/ from package
127
+ const srcSkills = path.join(__dirname, '..', 'skills');
128
+ if (fs.existsSync(srcSkills)) {
129
+ const skillsDir = path.join(claudeDir, 'skills');
130
+ fs.mkdirSync(skillsDir, { recursive: true });
131
+ for (const skillFolder of fs.readdirSync(srcSkills)) {
132
+ const srcSkillDir = path.join(srcSkills, skillFolder);
133
+ if (!fs.statSync(srcSkillDir).isDirectory()) continue;
134
+ const destSkillDir = path.join(skillsDir, skillFolder);
135
+ fs.mkdirSync(destSkillDir, { recursive: true });
136
+ for (const file of fs.readdirSync(srcSkillDir)) {
137
+ fs.copyFileSync(
138
+ path.join(srcSkillDir, file),
139
+ path.join(destSkillDir, file)
140
+ );
141
+ }
142
+ }
143
+ }
144
+
145
+ log('sync', 'Synced system files to .claude/');
146
+ console.log('lemmafit: synced system files to .claude/');
147
+ }
148
+
149
+ module.exports = { syncProject };
150
+
151
+ // Run as script when invoked directly
152
+ if (require.main === module) {
153
+
154
+ // Determine target directory
155
+ const explicitTarget = process.argv[2];
156
+
157
+ if (explicitTarget) {
158
+ // Called as `lemmafit sync <dir>` or `node cli/sync.js <dir>`
159
+ syncProject(explicitTarget);
160
+ } else {
161
+ // Called as postinstall or `lemmafit sync` (no arg)
162
+ const projectRoot = findProjectRoot();
163
+ if (projectRoot) {
164
+ syncProject(projectRoot);
165
+ }
166
+ // If no project root found (e.g. installing lemmafit globally), silently do nothing.
167
+ }
168
+ }