@axiomatic-labs/claudeflow 2.32.21 → 2.33.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 +0 -27
- package/lib/doctor.js +14 -42
- package/lib/install.js +121 -1359
- package/lib/panel.js +10 -26
- package/package.json +1 -1
- package/lib/hook-overrides.js +0 -262
- package/lib/preview.js +0 -360
package/lib/panel.js
CHANGED
|
@@ -230,32 +230,16 @@ function getSetupContextInfo(cwd) {
|
|
|
230
230
|
try { ctx = JSON.parse(fs.readFileSync(p, 'utf8')); }
|
|
231
231
|
catch { return { exists: false, path: p }; }
|
|
232
232
|
|
|
233
|
-
|
|
234
|
-
// types + the 2 observational `_verification` types. (benchmark /
|
|
235
|
-
// exploratory_test are conditional — triggered only by PERFORMANCE_CHANGE or
|
|
236
|
-
// unmeasurable correctness — so they are NOT required for "complete".)
|
|
237
|
-
// Source of truth: .claude/skills/claudeflow-init/references/test-tooling-schema.md.
|
|
238
|
-
const canonicalToolingTypes = [
|
|
239
|
-
'unit_test',
|
|
240
|
-
'integration_test',
|
|
241
|
-
'api_contract_test',
|
|
242
|
-
'e2e_test',
|
|
243
|
-
'security_test',
|
|
244
|
-
'browser_verification',
|
|
245
|
-
'visual_verification',
|
|
246
|
-
];
|
|
233
|
+
const canonicalToolingTypes = ['unit_test', 'integration_test', 'api_contract_test', 'e2e_test', 'security_test'];
|
|
247
234
|
const tooling = ctx.test_tooling || {};
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
e
|
|
254
|
-
typeof e === 'object' &&
|
|
255
|
-
|
|
256
|
-
(typeof e.invocation === 'string' && e.invocation.trim()));
|
|
257
|
-
const toolingComplete = canonicalToolingTypes.every((t) => isToolingDefined(tooling[t]));
|
|
258
|
-
const missingTooling = canonicalToolingTypes.filter((t) => !isToolingDefined(tooling[t]));
|
|
235
|
+
const toolingComplete = canonicalToolingTypes.every((t) => {
|
|
236
|
+
const e = tooling[t];
|
|
237
|
+
return e && typeof e === 'object' && typeof e.command_pattern === 'string' && e.command_pattern.trim();
|
|
238
|
+
});
|
|
239
|
+
const missingTooling = canonicalToolingTypes.filter((t) => {
|
|
240
|
+
const e = tooling[t];
|
|
241
|
+
return !(e && typeof e === 'object' && e.command_pattern && e.command_pattern.trim());
|
|
242
|
+
});
|
|
259
243
|
|
|
260
244
|
return {
|
|
261
245
|
exists: true,
|
|
@@ -850,7 +834,7 @@ details[open] summary { padding-bottom: 8px; border-bottom: 1px solid var(--bord
|
|
|
850
834
|
<span class="version" id="version">…</span>
|
|
851
835
|
<span class="cwd" id="cwd">…</span>
|
|
852
836
|
<span class="actions">
|
|
853
|
-
<span class="enforce-pill" id="enforce-pill" title="Master switch — toggles every blocking enforcement hook (freeze-step-contract,
|
|
837
|
+
<span class="enforce-pill" id="enforce-pill" title="Master switch — toggles every blocking enforcement hook (freeze-step-contract, enforce-task-creation, stack-debt-guard, ecosystem-artifacts, workflow-enforcement). Validators and reminders are unaffected.">
|
|
854
838
|
<span class="enforce-label">Enforcement</span>
|
|
855
839
|
<label class="enforce-switch"><input type="checkbox" id="enforce-toggle" /><span class="enforce-slider"></span></label>
|
|
856
840
|
<span class="enforce-state" id="enforce-state">…</span>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiomatic-labs/claudeflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.33.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"
|
package/lib/hook-overrides.js
DELETED
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
// User hook overrides — toggle individual claudeflow hook handlers without
|
|
2
|
-
// editing the template-managed `.claude/settings.json` directly.
|
|
3
|
-
//
|
|
4
|
-
// File: `.claudeflow/config/user-hook-overrides.json`
|
|
5
|
-
// Shape:
|
|
6
|
-
// { "version": 1,
|
|
7
|
-
// "disabledHandlers": [
|
|
8
|
-
// { "event": "SessionStart", "matcher": "", "handler": "SessionStart/foo.js",
|
|
9
|
-
// "spec": { "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/SessionStart/foo.js\"" } } ] }
|
|
10
|
-
//
|
|
11
|
-
// The `spec` field captures the original hook entry verbatim so the panel
|
|
12
|
-
// can re-enable a handler later without consulting an external snapshot.
|
|
13
|
-
//
|
|
14
|
-
// Two operations:
|
|
15
|
-
// - `applyHookOverrides(settings, overrides)`: pure function, returns a
|
|
16
|
-
// new settings object with disabled handlers removed. Called by
|
|
17
|
-
// install.js right after writing `.claude/settings.json` so the
|
|
18
|
-
// effective config respects the overrides on every install/update.
|
|
19
|
-
// - `toggleHookOverride(...)`: stateful, used by the panel server.
|
|
20
|
-
// Reads / writes the overrides file and the resolved settings file.
|
|
21
|
-
|
|
22
|
-
const fs = require('fs');
|
|
23
|
-
const path = require('path');
|
|
24
|
-
|
|
25
|
-
const OVERRIDES_REL = path.join('.claudeflow', 'config', 'user-hook-overrides.json');
|
|
26
|
-
const SETTINGS_REL = path.join('.claude', 'settings.json');
|
|
27
|
-
// Capture every `.claude/hooks/<path>.js` token in a command string. Wrapped
|
|
28
|
-
// commands include both `run-with-override.js` (the wrapper) and the actual
|
|
29
|
-
// hook path; we want the LAST match — that's the real handler.
|
|
30
|
-
const HOOK_PATH_REGEX_GLOBAL = /\.claude\/hooks\/([^"'\s]+\.js)/g;
|
|
31
|
-
|
|
32
|
-
function readJsonSafe(absPath) {
|
|
33
|
-
try { return JSON.parse(fs.readFileSync(absPath, 'utf8')); } catch { return null; }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function writeJson(absPath, data) {
|
|
37
|
-
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
38
|
-
fs.writeFileSync(absPath, JSON.stringify(data, null, 2) + '\n');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function normalizeHandlerPath(command) {
|
|
42
|
-
if (!command) return '';
|
|
43
|
-
const matches = [...command.matchAll(HOOK_PATH_REGEX_GLOBAL)];
|
|
44
|
-
if (matches.length === 0) {
|
|
45
|
-
return command.replace(/^node\s+/, '').replace(/^["']|["']$/g, '').trim();
|
|
46
|
-
}
|
|
47
|
-
// Skip the wrapper itself when present; pick the last non-wrapper match.
|
|
48
|
-
const real = matches.filter((m) => m[1] !== 'run-with-override.js');
|
|
49
|
-
const chosen = real.length ? real[real.length - 1] : matches[matches.length - 1];
|
|
50
|
-
return chosen[1];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function defaultOverrides() {
|
|
54
|
-
return {
|
|
55
|
-
version: 1,
|
|
56
|
-
disabledHandlers: [],
|
|
57
|
-
disabledReminders: [],
|
|
58
|
-
enforcementDisabled: false,
|
|
59
|
-
runToCompletionReminderEnabled: false,
|
|
60
|
-
cwdLockEnabled: false,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function readOverrides(projectRoot) {
|
|
65
|
-
const data = readJsonSafe(path.join(projectRoot, OVERRIDES_REL));
|
|
66
|
-
if (!data || typeof data !== 'object') return defaultOverrides();
|
|
67
|
-
return {
|
|
68
|
-
version: data.version || 1,
|
|
69
|
-
disabledHandlers: Array.isArray(data.disabledHandlers) ? data.disabledHandlers : [],
|
|
70
|
-
disabledReminders: Array.isArray(data.disabledReminders) ? data.disabledReminders : [],
|
|
71
|
-
enforcementDisabled: data.enforcementDisabled === true,
|
|
72
|
-
runToCompletionReminderEnabled: data.runToCompletionReminderEnabled === true,
|
|
73
|
-
cwdLockEnabled: data.cwdLockEnabled === true,
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function writeOverrides(projectRoot, overrides) {
|
|
78
|
-
writeJson(path.join(projectRoot, OVERRIDES_REL), overrides);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function matchesOverride(override, event, matcher, handlerCommand) {
|
|
82
|
-
if (override.event !== event) return false;
|
|
83
|
-
if ((override.matcher || '') !== (matcher || '')) return false;
|
|
84
|
-
return normalizeHandlerPath(handlerCommand) === override.handler;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Returns a fresh settings object with handlers disabled by `overrides` removed.
|
|
88
|
-
// Empty rules are pruned. The shape of `settings.hooks` is preserved otherwise.
|
|
89
|
-
function applyHookOverrides(settings, overrides) {
|
|
90
|
-
const disabled = (overrides && overrides.disabledHandlers) || [];
|
|
91
|
-
if (!disabled.length || !settings || !settings.hooks) return settings;
|
|
92
|
-
const next = JSON.parse(JSON.stringify(settings));
|
|
93
|
-
const hooks = next.hooks;
|
|
94
|
-
for (const event of Object.keys(hooks)) {
|
|
95
|
-
const rules = Array.isArray(hooks[event]) ? hooks[event] : [];
|
|
96
|
-
for (let i = rules.length - 1; i >= 0; i--) {
|
|
97
|
-
const rule = rules[i] || {};
|
|
98
|
-
const matcher = rule.matcher || '';
|
|
99
|
-
const remaining = (rule.hooks || []).filter(
|
|
100
|
-
(h) => !disabled.some((d) => matchesOverride(d, event, matcher, h.command || '')),
|
|
101
|
-
);
|
|
102
|
-
if (remaining.length === 0) rules.splice(i, 1);
|
|
103
|
-
else rule.hooks = remaining;
|
|
104
|
-
}
|
|
105
|
-
hooks[event] = rules;
|
|
106
|
-
}
|
|
107
|
-
return next;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Apply on disk: read settings.json + overrides, write the pruned settings
|
|
111
|
-
// back. Idempotent — calling twice produces the same file.
|
|
112
|
-
function applyOverridesToFile(projectRoot) {
|
|
113
|
-
const settingsPath = path.join(projectRoot, SETTINGS_REL);
|
|
114
|
-
const settings = readJsonSafe(settingsPath);
|
|
115
|
-
if (!settings) return false;
|
|
116
|
-
const overrides = readOverrides(projectRoot);
|
|
117
|
-
if (!overrides.disabledHandlers.length) return false;
|
|
118
|
-
const next = applyHookOverrides(settings, overrides);
|
|
119
|
-
fs.writeFileSync(settingsPath, JSON.stringify(next, null, 2) + '\n');
|
|
120
|
-
return true;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Find a hook entry in current settings.json by event/matcher/handler.
|
|
124
|
-
// Used when disabling: we capture the spec before removing it.
|
|
125
|
-
function findHandlerInSettings(settings, event, matcher, handler) {
|
|
126
|
-
const rules = (settings.hooks && settings.hooks[event]) || [];
|
|
127
|
-
for (const rule of rules) {
|
|
128
|
-
if ((rule.matcher || '') !== (matcher || '')) continue;
|
|
129
|
-
for (const h of rule.hooks || []) {
|
|
130
|
-
if (normalizeHandlerPath(h.command || '') === handler) return h;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Add an entry back to the live settings under the right event/matcher.
|
|
137
|
-
function restoreHandlerInSettings(settings, event, matcher, spec) {
|
|
138
|
-
if (!settings.hooks) settings.hooks = {};
|
|
139
|
-
if (!settings.hooks[event]) settings.hooks[event] = [];
|
|
140
|
-
const rules = settings.hooks[event];
|
|
141
|
-
let rule = rules.find((r) => (r.matcher || '') === (matcher || ''));
|
|
142
|
-
if (!rule) {
|
|
143
|
-
rule = { matcher, hooks: [] };
|
|
144
|
-
rules.push(rule);
|
|
145
|
-
}
|
|
146
|
-
// Avoid duplicate restoration if the spec is already present
|
|
147
|
-
const already = rule.hooks.some((h) => normalizeHandlerPath(h.command || '') === normalizeHandlerPath(spec.command || ''));
|
|
148
|
-
if (!already) rule.hooks.push(spec);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Stateful toggle used by the panel. Only edits overrides.json — settings.json
|
|
152
|
-
// is never mutated. The wrapper (.claude/hooks/run-with-override.js) reads
|
|
153
|
-
// overrides on every invocation, so disable/enable takes effect on the next
|
|
154
|
-
// hook fire (no Claude Code restart required).
|
|
155
|
-
//
|
|
156
|
-
// Settings.json is also consulted to verify the handler exists at all
|
|
157
|
-
// (refuses to disable a phantom handler), but it is never written.
|
|
158
|
-
function toggleHookOverride(projectRoot, { event, matcher = '', handler, disable }) {
|
|
159
|
-
const settingsPath = path.join(projectRoot, SETTINGS_REL);
|
|
160
|
-
const settings = readJsonSafe(settingsPath) || { hooks: {} };
|
|
161
|
-
const overrides = readOverrides(projectRoot);
|
|
162
|
-
const matcherKey = matcher || '';
|
|
163
|
-
|
|
164
|
-
if (disable) {
|
|
165
|
-
const already = overrides.disabledHandlers.find(
|
|
166
|
-
(d) => d.event === event && (d.matcher || '') === matcherKey && d.handler === handler,
|
|
167
|
-
);
|
|
168
|
-
if (already) return { state: 'noop', reason: 'already-disabled' };
|
|
169
|
-
if (!findHandlerInSettings(settings, event, matcherKey, handler)) {
|
|
170
|
-
return { state: 'error', reason: 'handler-not-found-in-settings' };
|
|
171
|
-
}
|
|
172
|
-
overrides.disabledHandlers.push({ event, matcher: matcherKey, handler });
|
|
173
|
-
writeOverrides(projectRoot, overrides);
|
|
174
|
-
return { state: 'disabled' };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const idx = overrides.disabledHandlers.findIndex(
|
|
178
|
-
(d) => d.event === event && (d.matcher || '') === matcherKey && d.handler === handler,
|
|
179
|
-
);
|
|
180
|
-
if (idx === -1) return { state: 'noop', reason: 'not-disabled' };
|
|
181
|
-
overrides.disabledHandlers.splice(idx, 1);
|
|
182
|
-
writeOverrides(projectRoot, overrides);
|
|
183
|
-
return { state: 'enabled' };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// Master enforcement switch. When OFF, the wrapper silences every hook
|
|
187
|
-
// listed in `.claude/hooks/shared/enforcement-catalog.js` regardless of
|
|
188
|
-
// per-handler manual toggles. Per-handler toggles outside the catalog
|
|
189
|
-
// remain in effect — they are independent layers.
|
|
190
|
-
function toggleEnforcementMode(projectRoot, { disable }) {
|
|
191
|
-
if (typeof disable !== 'boolean') return { state: 'error', reason: 'disable-required' };
|
|
192
|
-
const overrides = readOverrides(projectRoot);
|
|
193
|
-
if (overrides.enforcementDisabled === disable) {
|
|
194
|
-
return { state: 'noop', reason: disable ? 'already-off' : 'already-on' };
|
|
195
|
-
}
|
|
196
|
-
overrides.enforcementDisabled = disable;
|
|
197
|
-
writeOverrides(projectRoot, overrides);
|
|
198
|
-
return { state: disable ? 'off' : 'on' };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Toggle the run-to-completion reminder. Independent of the master
|
|
202
|
-
// enforcement switch — it's a soft nudge, not a gate. Default OFF.
|
|
203
|
-
function toggleRunToCompletionReminder(projectRoot, { enable }) {
|
|
204
|
-
if (typeof enable !== 'boolean') return { state: 'error', reason: 'enable-required' };
|
|
205
|
-
const overrides = readOverrides(projectRoot);
|
|
206
|
-
if (overrides.runToCompletionReminderEnabled === enable) {
|
|
207
|
-
return { state: 'noop', reason: enable ? 'already-on' : 'already-off' };
|
|
208
|
-
}
|
|
209
|
-
overrides.runToCompletionReminderEnabled = enable;
|
|
210
|
-
writeOverrides(projectRoot, overrides);
|
|
211
|
-
return { state: enable ? 'on' : 'off' };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Toggle the cwd-lock hook. Independent of the master enforcement switch —
|
|
215
|
-
// the CwdChanged hook is not in the enforcement-catalog and ignores the
|
|
216
|
-
// master switch by design (you can lock cwd even with enforcement off).
|
|
217
|
-
function toggleCwdLock(projectRoot, { enable }) {
|
|
218
|
-
if (typeof enable !== 'boolean') return { state: 'error', reason: 'enable-required' };
|
|
219
|
-
const overrides = readOverrides(projectRoot);
|
|
220
|
-
if (overrides.cwdLockEnabled === enable) {
|
|
221
|
-
return { state: 'noop', reason: enable ? 'already-on' : 'already-off' };
|
|
222
|
-
}
|
|
223
|
-
overrides.cwdLockEnabled = enable;
|
|
224
|
-
writeOverrides(projectRoot, overrides);
|
|
225
|
-
return { state: enable ? 'on' : 'off' };
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Toggle a single reminder by id. Same persistence file, same hot-reload
|
|
229
|
-
// semantics — `reload-reminder.js` re-reads overrides on every invocation.
|
|
230
|
-
function toggleReminderOverride(projectRoot, { id, disable }) {
|
|
231
|
-
if (typeof id !== 'string' || !id.trim()) return { state: 'error', reason: 'id-required' };
|
|
232
|
-
const overrides = readOverrides(projectRoot);
|
|
233
|
-
const list = overrides.disabledReminders;
|
|
234
|
-
const idx = list.indexOf(id);
|
|
235
|
-
|
|
236
|
-
if (disable) {
|
|
237
|
-
if (idx !== -1) return { state: 'noop', reason: 'already-disabled' };
|
|
238
|
-
list.push(id);
|
|
239
|
-
writeOverrides(projectRoot, overrides);
|
|
240
|
-
return { state: 'disabled' };
|
|
241
|
-
}
|
|
242
|
-
if (idx === -1) return { state: 'noop', reason: 'not-disabled' };
|
|
243
|
-
list.splice(idx, 1);
|
|
244
|
-
writeOverrides(projectRoot, overrides);
|
|
245
|
-
return { state: 'enabled' };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
module.exports = {
|
|
249
|
-
OVERRIDES_REL,
|
|
250
|
-
SETTINGS_REL,
|
|
251
|
-
applyHookOverrides,
|
|
252
|
-
applyOverridesToFile,
|
|
253
|
-
readOverrides,
|
|
254
|
-
writeOverrides,
|
|
255
|
-
toggleHookOverride,
|
|
256
|
-
toggleReminderOverride,
|
|
257
|
-
toggleEnforcementMode,
|
|
258
|
-
toggleRunToCompletionReminder,
|
|
259
|
-
toggleCwdLock,
|
|
260
|
-
normalizeHandlerPath,
|
|
261
|
-
matchesOverride,
|
|
262
|
-
};
|
package/lib/preview.js
DELETED
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
// `claudeflow preview` — standing, decoupled live-preview server for prototypes.
|
|
2
|
-
//
|
|
3
|
-
// Serves .claudeflow/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/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/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", "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 (
|
|
235
|
-
(urlPath === "/" || urlPath === "") &&
|
|
236
|
-
!fs.existsSync(path.join(root, "index.html"))
|
|
237
|
-
) {
|
|
238
|
-
const body = listingPage(root);
|
|
239
|
-
res.writeHead(200, {
|
|
240
|
-
"Content-Type": MIME[".html"],
|
|
241
|
-
"Cache-Control": "no-store",
|
|
242
|
-
});
|
|
243
|
-
res.end(injectLiveClient(body));
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
let file = resolveFile(root, urlPath);
|
|
248
|
-
if (!file) {
|
|
249
|
-
res.writeHead(403);
|
|
250
|
-
res.end("Forbidden");
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
try {
|
|
254
|
-
if (fs.statSync(file).isDirectory()) file = path.join(file, "index.html");
|
|
255
|
-
} catch {
|
|
256
|
-
/* fall through to 404 below */
|
|
257
|
-
}
|
|
258
|
-
let buf;
|
|
259
|
-
try {
|
|
260
|
-
buf = fs.readFileSync(file);
|
|
261
|
-
} catch {
|
|
262
|
-
res.writeHead(404, { "Content-Type": MIME[".html"] });
|
|
263
|
-
res.end(
|
|
264
|
-
injectLiveClient(
|
|
265
|
-
'<!doctype html><body style="font-family:system-ui;padding:40px">404 — not found</body>',
|
|
266
|
-
),
|
|
267
|
-
);
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
const ext = path.extname(file).toLowerCase();
|
|
271
|
-
const type = MIME[ext] || "application/octet-stream";
|
|
272
|
-
if (ext === ".html") {
|
|
273
|
-
res.writeHead(200, { "Content-Type": type, "Cache-Control": "no-store" });
|
|
274
|
-
res.end(injectLiveClient(buf.toString("utf8")));
|
|
275
|
-
} else {
|
|
276
|
-
res.writeHead(200, { "Content-Type": type, "Cache-Control": "no-store" });
|
|
277
|
-
res.end(buf);
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
const stopWatch = watch(protosRoot, (rel) => {
|
|
282
|
-
for (const c of clients) {
|
|
283
|
-
try {
|
|
284
|
-
c.write(`data: ${rel}\n\n`);
|
|
285
|
-
} catch {
|
|
286
|
-
/* drop dead client */
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
const onPort = (actualPort) => {
|
|
292
|
-
const url = `http://127.0.0.1:${actualPort}/`;
|
|
293
|
-
console.log("");
|
|
294
|
-
console.log(
|
|
295
|
-
` ${ui.GREEN}claudeflow preview${ui.RESET} — live reload for prototypes`,
|
|
296
|
-
);
|
|
297
|
-
console.log(
|
|
298
|
-
` ${ui.CYAN}${url}${ui.RESET} ${ui.DIM}(serving .claudeflow/prototypes/)${ui.RESET}`,
|
|
299
|
-
);
|
|
300
|
-
console.log(
|
|
301
|
-
` ${ui.DIM}CSS edits hot-swap (no flicker); HTML edits reload the changed view. Ctrl-C to stop.${ui.RESET}`,
|
|
302
|
-
);
|
|
303
|
-
console.log("");
|
|
304
|
-
if (open) openBrowser(proto ? url : url);
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
await new Promise((resolve) => {
|
|
308
|
-
const tryListen = (p, allowFallback) => {
|
|
309
|
-
server.once("error", (err) => {
|
|
310
|
-
if (err && err.code === "EADDRINUSE" && allowFallback) {
|
|
311
|
-
tryListen(0, false); // OS-assigned free port
|
|
312
|
-
} else {
|
|
313
|
-
console.error(
|
|
314
|
-
`${ui.RED}preview server error: ${err.message}${ui.RESET}`,
|
|
315
|
-
);
|
|
316
|
-
resolve();
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
server.listen(p, "127.0.0.1", () => {
|
|
320
|
-
onPort(server.address().port);
|
|
321
|
-
resolve();
|
|
322
|
-
});
|
|
323
|
-
};
|
|
324
|
-
// Default to an UNCOMMON port (4793) to avoid squatting a port the project's
|
|
325
|
-
// dev server wants (e.g. 4321=Astro, 5173=Vite, 3000=Next, 4200=Angular).
|
|
326
|
-
// --port is strict (no fallback — the user demanded it); the default + the
|
|
327
|
-
// CLAUDEFLOW_PREVIEW_PORT override fall back to an OS-assigned free port if taken,
|
|
328
|
-
// so preview NEVER collides with a running dev server.
|
|
329
|
-
const envPort = Number(process.env.CLAUDEFLOW_PREVIEW_PORT) || 0;
|
|
330
|
-
const preferred = wantPort || envPort || 4793;
|
|
331
|
-
tryListen(preferred, !wantPort);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
const shutdown = () => {
|
|
335
|
-
try {
|
|
336
|
-
stopWatch();
|
|
337
|
-
} catch {
|
|
338
|
-
/* ignore */
|
|
339
|
-
}
|
|
340
|
-
for (const c of clients) {
|
|
341
|
-
try {
|
|
342
|
-
c.end();
|
|
343
|
-
} catch {
|
|
344
|
-
/* ignore */
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
server.close(() => process.exit(0));
|
|
348
|
-
setTimeout(() => process.exit(0), 300).unref();
|
|
349
|
-
};
|
|
350
|
-
process.on("SIGINT", shutdown);
|
|
351
|
-
process.on("SIGTERM", shutdown);
|
|
352
|
-
|
|
353
|
-
// Keep the process alive (standing server).
|
|
354
|
-
return new Promise(() => {});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
module.exports = preview;
|
|
358
|
-
module.exports.injectLiveClient = injectLiveClient;
|
|
359
|
-
module.exports.resolveFile = resolveFile;
|
|
360
|
-
module.exports.listPrototypes = listPrototypes;
|