@axiomatic-labs/claudeflow 2.13.59 → 2.13.61
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/lib/doctor.js +29 -6
- package/lib/panel.js +46 -6
- package/package.json +1 -1
package/lib/doctor.js
CHANGED
|
@@ -149,13 +149,36 @@ function checkStaleLockfiles(cwd) {
|
|
|
149
149
|
// the configured port, the daemon is up.
|
|
150
150
|
function readObserverState(cwd) {
|
|
151
151
|
const observerJsonPath = path.join(cwd, '.claudeflow', 'tmp', 'error-observer.json');
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
152
|
+
// Defense-in-depth retry: even though the daemon now writes atomically
|
|
153
|
+
// (rename-after-write), older daemon versions and any future non-atomic
|
|
154
|
+
// writer would expose a 0-byte window during overwrite. We retry up to
|
|
155
|
+
// 3 times with a tiny pause between attempts so a transient empty read
|
|
156
|
+
// doesn't flip the panel UI between running/stopped every refresh.
|
|
157
|
+
let meta = null;
|
|
158
|
+
let lastErr = null;
|
|
159
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
160
|
+
try {
|
|
161
|
+
const raw = fs.readFileSync(observerJsonPath, 'utf8');
|
|
162
|
+
if (raw.trim()) {
|
|
163
|
+
meta = JSON.parse(raw);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
lastErr = new Error('empty file');
|
|
167
|
+
} catch (e) {
|
|
168
|
+
lastErr = e;
|
|
169
|
+
if (e.code === 'ENOENT') break; // file genuinely missing — no retry helps
|
|
170
|
+
}
|
|
171
|
+
if (attempt < 2) {
|
|
172
|
+
// Sync sleep (~30ms) via spawnSync — sufficient to outlast a writeFile race.
|
|
173
|
+
const { spawnSync } = require('child_process');
|
|
174
|
+
spawnSync('sleep', ['0.03']);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!meta) {
|
|
178
|
+
const source = (lastErr && lastErr.code === 'ENOENT') ? 'no-observer-json' : 'observer-json-malformed';
|
|
179
|
+
return { running: false, pid: null, port: null, source, observerJsonPath };
|
|
157
180
|
}
|
|
158
|
-
if (
|
|
181
|
+
if (typeof meta.port !== 'number') {
|
|
159
182
|
return { running: false, pid: null, port: null, source: 'observer-json-malformed', observerJsonPath };
|
|
160
183
|
}
|
|
161
184
|
const port = meta.port;
|
package/lib/panel.js
CHANGED
|
@@ -1839,14 +1839,47 @@ function openInBrowser(url) {
|
|
|
1839
1839
|
} catch {}
|
|
1840
1840
|
}
|
|
1841
1841
|
|
|
1842
|
-
|
|
1842
|
+
// Derive a stable port from the cwd. Without this, every panel restart picks
|
|
1843
|
+
// a different random port and any open browser tab pointing at the OLD port
|
|
1844
|
+
// becomes a dead bookmark — confusingly, the bookmark keeps rendering the
|
|
1845
|
+
// last-known UI state because /api/status fails silently. Range 9300-9399
|
|
1846
|
+
// (above Chrome CDP's 9200-9299).
|
|
1847
|
+
function derivePanelPort(projectPath) {
|
|
1848
|
+
const crypto = require('crypto');
|
|
1849
|
+
const hash = crypto.createHash('sha1').update(projectPath).digest();
|
|
1850
|
+
return 9300 + (hash.readUInt16BE(0) % 100);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function start({ cwd = process.cwd(), port = null, openBrowser = true } = {}) {
|
|
1843
1854
|
const server = http.createServer(handler(cwd));
|
|
1844
1855
|
let lastActivity = Date.now();
|
|
1845
1856
|
server.on('request', () => { lastActivity = Date.now(); });
|
|
1846
1857
|
|
|
1858
|
+
// Port resolution:
|
|
1859
|
+
// - explicit port arg (e.g. --port=9999) → use as-is
|
|
1860
|
+
// - port === 0 → OS-assigned random (legacy behavior, retained for tests)
|
|
1861
|
+
// - default (null) → derive from cwd; on EADDRINUSE fall back to OS-assigned
|
|
1862
|
+
const explicitPort = typeof port === 'number' && port > 0;
|
|
1863
|
+
const derivedPort = port === null ? derivePanelPort(cwd) : null;
|
|
1864
|
+
const initialPort = explicitPort ? port : (derivedPort !== null ? derivedPort : 0);
|
|
1865
|
+
|
|
1847
1866
|
return new Promise((resolve, reject) => {
|
|
1848
|
-
|
|
1849
|
-
server.
|
|
1867
|
+
let usingFallback = false;
|
|
1868
|
+
server.on('error', (err) => {
|
|
1869
|
+
if (err.code === 'EADDRINUSE' && !usingFallback && !explicitPort) {
|
|
1870
|
+
// Another process holds the derived port. Fall back to OS-assigned
|
|
1871
|
+
// so the panel can still start, but warn the user — they may have
|
|
1872
|
+
// another panel running for this project, or unrelated process took
|
|
1873
|
+
// the port.
|
|
1874
|
+
usingFallback = true;
|
|
1875
|
+
console.error(`\n [panel] derived port ${initialPort} is in use — falling back to OS-assigned port.`);
|
|
1876
|
+
console.error(` [panel] If another \`claudeflow panel\` is running for this project, kill it first to recover URL stability.\n`);
|
|
1877
|
+
server.listen(0, '127.0.0.1');
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
reject(err);
|
|
1881
|
+
});
|
|
1882
|
+
server.on('listening', () => {
|
|
1850
1883
|
const addr = server.address();
|
|
1851
1884
|
const url = `http://127.0.0.1:${addr.port}`;
|
|
1852
1885
|
const idleTimer = setInterval(() => {
|
|
@@ -1858,8 +1891,9 @@ function start({ cwd = process.cwd(), port = 0, openBrowser = true } = {}) {
|
|
|
1858
1891
|
}, 60 * 1000);
|
|
1859
1892
|
idleTimer.unref();
|
|
1860
1893
|
if (openBrowser) openInBrowser(url);
|
|
1861
|
-
resolve({ url, server });
|
|
1894
|
+
resolve({ url, server, derivedPort, usedFallback: usingFallback });
|
|
1862
1895
|
});
|
|
1896
|
+
server.listen(initialPort, '127.0.0.1');
|
|
1863
1897
|
});
|
|
1864
1898
|
}
|
|
1865
1899
|
|
|
@@ -1867,13 +1901,18 @@ async function run(argv = []) {
|
|
|
1867
1901
|
const cwd = process.cwd();
|
|
1868
1902
|
const noOpen = argv.includes('--no-open');
|
|
1869
1903
|
const portArg = argv.find((a) => a.startsWith('--port='));
|
|
1870
|
-
|
|
1904
|
+
// Default port (null) → derive from cwd so the URL is stable across restarts.
|
|
1905
|
+
// --port=N overrides; --port=0 keeps the legacy OS-assigned behavior.
|
|
1906
|
+
const port = portArg ? parseInt(portArg.split('=')[1], 10) : null;
|
|
1871
1907
|
|
|
1872
1908
|
ui.banner();
|
|
1873
1909
|
console.log(` Starting panel for ${ui.CYAN}${cwd}${ui.RESET}`);
|
|
1874
|
-
const { url } = await start({ cwd, port, openBrowser: !noOpen });
|
|
1910
|
+
const { url, derivedPort, usedFallback } = await start({ cwd, port, openBrowser: !noOpen });
|
|
1875
1911
|
console.log('');
|
|
1876
1912
|
console.log(` ${ui.GREEN}▸${ui.RESET} ${ui.CYAN}${url}${ui.RESET}`);
|
|
1913
|
+
if (derivedPort !== null && !usedFallback) {
|
|
1914
|
+
console.log(` ${ui.DIM}Port ${derivedPort} is derived from the cwd — same URL across restarts.${ui.RESET}`);
|
|
1915
|
+
}
|
|
1877
1916
|
console.log(` ${ui.DIM}Ctrl+C to stop. Auto-shutdown after 30 min idle.${ui.RESET}`);
|
|
1878
1917
|
console.log('');
|
|
1879
1918
|
return new Promise(() => {}); // keep alive
|
|
@@ -1882,6 +1921,7 @@ async function run(argv = []) {
|
|
|
1882
1921
|
module.exports = run;
|
|
1883
1922
|
module.exports.start = start;
|
|
1884
1923
|
module.exports.createPanelServer = handler;
|
|
1924
|
+
module.exports.derivePanelPort = derivePanelPort;
|
|
1885
1925
|
module.exports.collectStatus = collectStatus;
|
|
1886
1926
|
module.exports.getClaudeMdInfo = getClaudeMdInfo;
|
|
1887
1927
|
module.exports.getHooksInfo = getHooksInfo;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiomatic-labs/claudeflow",
|
|
3
|
-
"version": "2.13.
|
|
3
|
+
"version": "2.13.61",
|
|
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"
|