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.
- package/LICENSE +21 -0
- package/README.md +93 -4
- package/blank-template/README.md +3 -0
- package/blank-template/SPEC.yaml +1 -0
- package/blank-template/index.html +12 -0
- package/blank-template/lemmafit/.vibe/config.json +5 -0
- package/blank-template/lemmafit/dafny/Domain.dfy +5 -0
- package/blank-template/lemmafit/dafny/Replay.dfy +147 -0
- package/blank-template/package.json +25 -0
- package/blank-template/src/App.css +3 -0
- package/blank-template/src/App.tsx +10 -0
- package/blank-template/src/dafny/.gitkeep +0 -0
- package/blank-template/src/index.css +29 -0
- package/blank-template/src/main.tsx +10 -0
- package/blank-template/src/vite-env.d.ts +6 -0
- package/blank-template/template.gitignore +3 -0
- package/blank-template/tsconfig.json +21 -0
- package/blank-template/tsconfig.node.json +11 -0
- package/blank-template/vite.config.js +9 -0
- package/cli/context-hook.js +103 -0
- package/cli/daemon.js +24 -0
- package/cli/download-dafny2js.js +136 -0
- package/cli/generate-guarantees-md.js +223 -0
- package/cli/lemmafit.js +385 -0
- package/cli/session-hook.js +74 -0
- package/cli/sync.js +168 -0
- package/cli/verify-hook.js +221 -0
- package/commands/guarantees.md +138 -0
- package/docs/CLAUDE_INSTRUCTIONS.md +137 -0
- package/kernels/Replay.dfy +147 -0
- package/lib/daemon-client.js +54 -0
- package/lib/daemon.js +990 -0
- package/lib/download-dafny.js +130 -0
- package/lib/log.js +32 -0
- package/lib/spawn-claude.js +51 -0
- package/package.json +49 -5
- package/skills/lemmafit-dafny/SKILL.md +101 -0
- package/skills/lemmafit-post-react-audit/SKILL.md +46 -0
- package/skills/lemmafit-pre-react-audits/SKILL.md +67 -0
- package/skills/lemmafit-proofs/SKILL.md +24 -0
- package/skills/lemmafit-react-pattern/SKILL.md +62 -0
- package/skills/lemmafit-spec/SKILL.md +71 -0
- package/index.js +0 -5
package/cli/lemmafit.js
ADDED
|
@@ -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
|
+
}
|