@axiomatic-labs/claudeflow 2.30.0 → 2.32.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/bin/cli.js +7 -1
- package/lib/preview.js +335 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
const fs = require('fs');
|
|
20
20
|
const path = require('path');
|
|
21
21
|
|
|
22
|
-
const SUBCOMMANDS = new Set(['install', 'version', '--version', '-v', 'help', '--help', '-h', 'doctor', 'panel']);
|
|
22
|
+
const SUBCOMMANDS = new Set(['install', 'version', '--version', '-v', 'help', '--help', '-h', 'doctor', 'panel', 'preview']);
|
|
23
23
|
|
|
24
24
|
function inClaudeflowProject(startDir) {
|
|
25
25
|
let dir = path.resolve(startDir);
|
|
@@ -57,6 +57,11 @@ async function runSubcommand(command) {
|
|
|
57
57
|
await panel(process.argv.slice(3));
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
|
+
case 'preview': {
|
|
61
|
+
const preview = require('../lib/preview.js');
|
|
62
|
+
await preview(process.argv.slice(3));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
60
65
|
case 'help':
|
|
61
66
|
case '--help':
|
|
62
67
|
case '-h':
|
|
@@ -69,6 +74,7 @@ async function runSubcommand(command) {
|
|
|
69
74
|
console.log(` ${ui.CYAN}install${ui.RESET} Install or update Claudeflow in the current project`);
|
|
70
75
|
console.log(` ${ui.CYAN}doctor${ui.RESET} Diagnose local issues (CDP port, stale lockfiles); add --fix to repair`);
|
|
71
76
|
console.log(` ${ui.CYAN}panel${ui.RESET} Open the local web dashboard for hooks, CLAUDE.md, and run state`);
|
|
77
|
+
console.log(` ${ui.CYAN}preview${ui.RESET} Live-reload server for prototypes (.claudeflow/tmp/prototypes/) — run once, leave it`);
|
|
72
78
|
console.log(` ${ui.CYAN}version${ui.RESET} Show version info`);
|
|
73
79
|
console.log(` ${ui.CYAN}help${ui.RESET} Show this message`);
|
|
74
80
|
console.log('');
|
package/lib/preview.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// `claudeflow preview` — standing, decoupled live-preview server for prototypes.
|
|
2
|
+
//
|
|
3
|
+
// Serves .claudeflow/tmp/prototypes/ over http://127.0.0.1:<port>, watches the
|
|
4
|
+
// files, and pushes live updates over SSE — CSS edits hot-swap (no flicker),
|
|
5
|
+
// HTML edits reload only the changed document / the content iframe (the shell
|
|
6
|
+
// stays). The user runs this ONCE and leaves it; the /claudeflow-prototype flow
|
|
7
|
+
// just writes files into the watched dir and never touches this process. That
|
|
8
|
+
// decoupling is the whole point: the fragility the project hit came from servers
|
|
9
|
+
// the BUILD/observer flow spawned + killed — this one is user-owned (start it,
|
|
10
|
+
// Ctrl-C to stop), so the flow adds no steps and manages no process.
|
|
11
|
+
//
|
|
12
|
+
// Zero runtime dependencies — Node built-ins only. Opened directly as file://,
|
|
13
|
+
// a prototype is still a static snapshot; live-reload is purely this server's job
|
|
14
|
+
// (the live client is INJECTED into served HTML, so prototype files stay pure).
|
|
15
|
+
|
|
16
|
+
const http = require('http');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { spawn } = require('child_process');
|
|
20
|
+
|
|
21
|
+
const ui = require('./ui.js');
|
|
22
|
+
|
|
23
|
+
const MIME = {
|
|
24
|
+
'.html': 'text/html; charset=utf-8',
|
|
25
|
+
'.css': 'text/css; charset=utf-8',
|
|
26
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
27
|
+
'.json': 'application/json; charset=utf-8',
|
|
28
|
+
'.svg': 'image/svg+xml',
|
|
29
|
+
'.png': 'image/png',
|
|
30
|
+
'.jpg': 'image/jpeg',
|
|
31
|
+
'.jpeg': 'image/jpeg',
|
|
32
|
+
'.gif': 'image/gif',
|
|
33
|
+
'.webp': 'image/webp',
|
|
34
|
+
'.ico': 'image/x-icon',
|
|
35
|
+
'.woff': 'font/woff',
|
|
36
|
+
'.woff2': 'font/woff2',
|
|
37
|
+
'.ttf': 'font/ttf',
|
|
38
|
+
'.map': 'application/json; charset=utf-8',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const SSE_PATH = '/__livereload';
|
|
42
|
+
|
|
43
|
+
// Injected into every served .html. Connects to the SSE stream; CSS changes
|
|
44
|
+
// hot-swap the <link>s (flicker-free), other changes reload the changed
|
|
45
|
+
// document — or, if this doc is the shell (has an <iframe>), just its iframe
|
|
46
|
+
// so navigation + the shell survive.
|
|
47
|
+
const LIVE_CLIENT = `
|
|
48
|
+
<script data-claudeflow-livereload>
|
|
49
|
+
(function () {
|
|
50
|
+
try {
|
|
51
|
+
var es = new EventSource(${JSON.stringify(SSE_PATH)});
|
|
52
|
+
es.onmessage = function (e) {
|
|
53
|
+
var p = (e.data || '').trim();
|
|
54
|
+
if (!p) return;
|
|
55
|
+
if (/\\.css($|\\?)/.test(p)) {
|
|
56
|
+
document.querySelectorAll('link[rel="stylesheet"]').forEach(function (l) {
|
|
57
|
+
var u = l.href.split('?')[0];
|
|
58
|
+
l.href = u + '?t=' + Date.now();
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
var here = decodeURIComponent(location.pathname.replace(/^\\//, ''));
|
|
63
|
+
if (p && here && (p === here || here.endsWith(p) || p.endsWith(here))) {
|
|
64
|
+
location.reload();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
var f = document.querySelector('iframe');
|
|
68
|
+
if (f) { try { f.contentWindow.location.reload(); return; } catch (_) {} }
|
|
69
|
+
location.reload();
|
|
70
|
+
};
|
|
71
|
+
} catch (_) {}
|
|
72
|
+
})();
|
|
73
|
+
</script>`;
|
|
74
|
+
|
|
75
|
+
function listPrototypes(root) {
|
|
76
|
+
let entries = [];
|
|
77
|
+
try {
|
|
78
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
79
|
+
} catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
return entries
|
|
83
|
+
.filter((e) => e.isDirectory())
|
|
84
|
+
.map((e) => e.name)
|
|
85
|
+
.filter((name) => fs.existsSync(path.join(root, name, 'index.html')))
|
|
86
|
+
.sort();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function listingPage(root) {
|
|
90
|
+
const protos = listPrototypes(root);
|
|
91
|
+
const items = protos.length
|
|
92
|
+
? protos
|
|
93
|
+
.map((p) => `<li><a href="/${p}/index.html">${p}</a></li>`)
|
|
94
|
+
.join('\n')
|
|
95
|
+
: '<li class="muted">No prototypes yet — assemble one with /claudeflow-prototype (writes into .claudeflow/tmp/prototypes/).</li>';
|
|
96
|
+
return `<!doctype html><html><head><meta charset="utf-8"><title>claudeflow preview</title>
|
|
97
|
+
<style>body{font:15px/1.5 -apple-system,system-ui,sans-serif;max-width:640px;margin:48px auto;padding:0 20px;color:#1c1c22}
|
|
98
|
+
h1{font-size:18px}a{color:#5050d4}.muted{color:#717180}li{margin:6px 0}</style></head>
|
|
99
|
+
<body><h1>claudeflow preview</h1><p class="muted">Serving <code>.claudeflow/tmp/prototypes/</code> with live reload.</p>
|
|
100
|
+
<ul>${items}</ul></body></html>`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Resolve the requested URL to a file under root, blocking path traversal.
|
|
104
|
+
function resolveFile(root, urlPath) {
|
|
105
|
+
const clean = decodeURIComponent(urlPath.split('?')[0]).replace(/\/+$/, '');
|
|
106
|
+
const abs = path.normalize(path.join(root, clean));
|
|
107
|
+
if (abs !== root && !abs.startsWith(root + path.sep)) return null; // traversal guard
|
|
108
|
+
return abs;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function injectLiveClient(html) {
|
|
112
|
+
const marker = /<\/body>/i;
|
|
113
|
+
return marker.test(html)
|
|
114
|
+
? html.replace(marker, LIVE_CLIENT + '\n</body>')
|
|
115
|
+
: html + LIVE_CLIENT;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Debounced recursive watcher with a polling fallback (recursive fs.watch is not
|
|
119
|
+
// supported on every platform/Node). Calls onChange(relPath) on edits.
|
|
120
|
+
function watch(root, onChange) {
|
|
121
|
+
let timer = null;
|
|
122
|
+
let lastPath = '';
|
|
123
|
+
const fire = (rel) => {
|
|
124
|
+
lastPath = rel || lastPath;
|
|
125
|
+
clearTimeout(timer);
|
|
126
|
+
timer = setTimeout(() => onChange(lastPath), 120);
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
const w = fs.watch(root, { recursive: true }, (_evt, filename) => {
|
|
130
|
+
if (filename) fire(String(filename).split(path.sep).join('/'));
|
|
131
|
+
});
|
|
132
|
+
w.on('error', () => {});
|
|
133
|
+
return () => w.close();
|
|
134
|
+
} catch {
|
|
135
|
+
// Polling fallback: scan mtimes every second.
|
|
136
|
+
const seen = new Map();
|
|
137
|
+
const scan = (dir, base) => {
|
|
138
|
+
let ents = [];
|
|
139
|
+
try {
|
|
140
|
+
ents = fs.readdirSync(dir, { withFileTypes: true });
|
|
141
|
+
} catch {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
for (const e of ents) {
|
|
145
|
+
const abs = path.join(dir, e.name);
|
|
146
|
+
const rel = base ? base + '/' + e.name : e.name;
|
|
147
|
+
if (e.isDirectory()) {
|
|
148
|
+
scan(abs, rel);
|
|
149
|
+
} else {
|
|
150
|
+
let m = 0;
|
|
151
|
+
try {
|
|
152
|
+
m = fs.statSync(abs).mtimeMs;
|
|
153
|
+
} catch {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (seen.has(rel) && seen.get(rel) !== m) fire(rel);
|
|
157
|
+
seen.set(rel, m);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
scan(root, '');
|
|
162
|
+
const iv = setInterval(() => scan(root, ''), 1000);
|
|
163
|
+
return () => clearInterval(iv);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function openBrowser(url) {
|
|
168
|
+
const cmd =
|
|
169
|
+
process.platform === 'darwin'
|
|
170
|
+
? { c: 'open', a: [url] }
|
|
171
|
+
: process.platform === 'win32'
|
|
172
|
+
? { c: 'cmd', a: ['/c', 'start', '', url] }
|
|
173
|
+
: { c: 'xdg-open', a: [url] };
|
|
174
|
+
try {
|
|
175
|
+
const child = spawn(cmd.c, cmd.a, { stdio: 'ignore', detached: true });
|
|
176
|
+
child.on('error', () => {});
|
|
177
|
+
child.unref();
|
|
178
|
+
} catch {
|
|
179
|
+
/* best-effort */
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function parseArgs(argv) {
|
|
184
|
+
const out = { proto: null, port: null, open: true };
|
|
185
|
+
for (let i = 0; i < argv.length; i++) {
|
|
186
|
+
const a = argv[i];
|
|
187
|
+
if (a === '--port' || a === '-p') out.port = Number(argv[++i]);
|
|
188
|
+
else if (a === '--no-open') out.open = false;
|
|
189
|
+
else if (!a.startsWith('-')) out.proto = a;
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function preview(argv = []) {
|
|
195
|
+
const { proto, port: wantPort, open } = parseArgs(argv);
|
|
196
|
+
const cwd = process.cwd();
|
|
197
|
+
const protosRoot = path.join(cwd, '.claudeflow', 'tmp', 'prototypes');
|
|
198
|
+
|
|
199
|
+
// Root to serve: a specific prototype subdir, or the prototypes parent (listing).
|
|
200
|
+
let root = protosRoot;
|
|
201
|
+
if (proto) {
|
|
202
|
+
const sub = path.join(protosRoot, proto);
|
|
203
|
+
if (!fs.existsSync(sub)) {
|
|
204
|
+
console.error(
|
|
205
|
+
`${ui.RED}No prototype "${proto}" under ${protosRoot}${ui.RESET}`,
|
|
206
|
+
);
|
|
207
|
+
return 1;
|
|
208
|
+
}
|
|
209
|
+
root = sub;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
fs.mkdirSync(protosRoot, { recursive: true });
|
|
213
|
+
} catch {
|
|
214
|
+
/* best-effort — listing/watch handle a missing dir */
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const clients = new Set();
|
|
218
|
+
const server = http.createServer((req, res) => {
|
|
219
|
+
const urlPath = (req.url || '/').split('?')[0];
|
|
220
|
+
|
|
221
|
+
if (urlPath === SSE_PATH) {
|
|
222
|
+
res.writeHead(200, {
|
|
223
|
+
'Content-Type': 'text/event-stream',
|
|
224
|
+
'Cache-Control': 'no-cache',
|
|
225
|
+
Connection: 'keep-alive',
|
|
226
|
+
});
|
|
227
|
+
res.write(': connected\n\n');
|
|
228
|
+
clients.add(res);
|
|
229
|
+
req.on('close', () => clients.delete(res));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Listing at root when serving the prototypes parent and no index there.
|
|
234
|
+
if ((urlPath === '/' || urlPath === '') && !fs.existsSync(path.join(root, 'index.html'))) {
|
|
235
|
+
const body = listingPage(root);
|
|
236
|
+
res.writeHead(200, { 'Content-Type': MIME['.html'], 'Cache-Control': 'no-store' });
|
|
237
|
+
res.end(injectLiveClient(body));
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let file = resolveFile(root, urlPath);
|
|
242
|
+
if (!file) {
|
|
243
|
+
res.writeHead(403);
|
|
244
|
+
res.end('Forbidden');
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
if (fs.statSync(file).isDirectory()) file = path.join(file, 'index.html');
|
|
249
|
+
} catch {
|
|
250
|
+
/* fall through to 404 below */
|
|
251
|
+
}
|
|
252
|
+
let buf;
|
|
253
|
+
try {
|
|
254
|
+
buf = fs.readFileSync(file);
|
|
255
|
+
} catch {
|
|
256
|
+
res.writeHead(404, { 'Content-Type': MIME['.html'] });
|
|
257
|
+
res.end(injectLiveClient('<!doctype html><body style="font-family:system-ui;padding:40px">404 — not found</body>'));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const ext = path.extname(file).toLowerCase();
|
|
261
|
+
const type = MIME[ext] || 'application/octet-stream';
|
|
262
|
+
if (ext === '.html') {
|
|
263
|
+
res.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'no-store' });
|
|
264
|
+
res.end(injectLiveClient(buf.toString('utf8')));
|
|
265
|
+
} else {
|
|
266
|
+
res.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'no-store' });
|
|
267
|
+
res.end(buf);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const stopWatch = watch(protosRoot, (rel) => {
|
|
272
|
+
for (const c of clients) {
|
|
273
|
+
try {
|
|
274
|
+
c.write(`data: ${rel}\n\n`);
|
|
275
|
+
} catch {
|
|
276
|
+
/* drop dead client */
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const onPort = (actualPort) => {
|
|
282
|
+
const url = `http://127.0.0.1:${actualPort}/`;
|
|
283
|
+
console.log('');
|
|
284
|
+
console.log(` ${ui.GREEN}claudeflow preview${ui.RESET} — live reload for prototypes`);
|
|
285
|
+
console.log(` ${ui.CYAN}${url}${ui.RESET} ${ui.DIM}(serving .claudeflow/tmp/prototypes/)${ui.RESET}`);
|
|
286
|
+
console.log(` ${ui.DIM}CSS edits hot-swap (no flicker); HTML edits reload the changed view. Ctrl-C to stop.${ui.RESET}`);
|
|
287
|
+
console.log('');
|
|
288
|
+
if (open) openBrowser(proto ? url : url);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
await new Promise((resolve) => {
|
|
292
|
+
const tryListen = (p, allowFallback) => {
|
|
293
|
+
server.once('error', (err) => {
|
|
294
|
+
if (err && err.code === 'EADDRINUSE' && allowFallback) {
|
|
295
|
+
tryListen(0, false); // OS-assigned free port
|
|
296
|
+
} else {
|
|
297
|
+
console.error(`${ui.RED}preview server error: ${err.message}${ui.RESET}`);
|
|
298
|
+
resolve();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
server.listen(p, '127.0.0.1', () => {
|
|
302
|
+
onPort(server.address().port);
|
|
303
|
+
resolve();
|
|
304
|
+
});
|
|
305
|
+
};
|
|
306
|
+
tryListen(wantPort || 4321, !wantPort);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const shutdown = () => {
|
|
310
|
+
try {
|
|
311
|
+
stopWatch();
|
|
312
|
+
} catch {
|
|
313
|
+
/* ignore */
|
|
314
|
+
}
|
|
315
|
+
for (const c of clients) {
|
|
316
|
+
try {
|
|
317
|
+
c.end();
|
|
318
|
+
} catch {
|
|
319
|
+
/* ignore */
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
server.close(() => process.exit(0));
|
|
323
|
+
setTimeout(() => process.exit(0), 300).unref();
|
|
324
|
+
};
|
|
325
|
+
process.on('SIGINT', shutdown);
|
|
326
|
+
process.on('SIGTERM', shutdown);
|
|
327
|
+
|
|
328
|
+
// Keep the process alive (standing server).
|
|
329
|
+
return new Promise(() => {});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
module.exports = preview;
|
|
333
|
+
module.exports.injectLiveClient = injectLiveClient;
|
|
334
|
+
module.exports.resolveFile = resolveFile;
|
|
335
|
+
module.exports.listPrototypes = listPrototypes;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiomatic-labs/claudeflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.32.0",
|
|
4
4
|
"description": "Claudeflow — AI-powered development toolkit for Claude Code. Skills, agents, hooks, and quality gates that ship production apps.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"claudeflow": "./bin/cli.js"
|