specrails-desktop 2.28.0 → 2.28.1
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.
|
@@ -38,6 +38,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
38
38
|
// ---------------------------------------------------------------------------
|
|
39
39
|
const DEFAULT_PORT = 4200;
|
|
40
40
|
const DETECTION_TIMEOUT_MS = 500;
|
|
41
|
+
const HTTP_REQUEST_TIMEOUT_MS = 15_000;
|
|
41
42
|
exports.KNOWN_VERBS = new Set([
|
|
42
43
|
'implement',
|
|
43
44
|
'batch-implement',
|
|
@@ -242,7 +243,10 @@ function loadDesktopToken() {
|
|
|
242
243
|
}
|
|
243
244
|
return null;
|
|
244
245
|
}
|
|
245
|
-
function
|
|
246
|
+
function requestTimeoutError(url, timeoutMs) {
|
|
247
|
+
return new Error(`Request to ${url} timed out after ${timeoutMs}ms`);
|
|
248
|
+
}
|
|
249
|
+
function httpGet(url, timeoutMs = HTTP_REQUEST_TIMEOUT_MS) {
|
|
246
250
|
return new Promise((resolve, reject) => {
|
|
247
251
|
const token = loadDesktopToken();
|
|
248
252
|
const parsed = new URL(url);
|
|
@@ -260,10 +264,13 @@ function httpGet(url) {
|
|
|
260
264
|
res.on('data', (chunk) => { body += chunk; });
|
|
261
265
|
res.on('end', () => resolve({ status: res.statusCode ?? 0, body }));
|
|
262
266
|
});
|
|
267
|
+
req.setTimeout(timeoutMs, () => {
|
|
268
|
+
req.destroy(requestTimeoutError(url, timeoutMs));
|
|
269
|
+
});
|
|
263
270
|
req.on('error', reject);
|
|
264
271
|
});
|
|
265
272
|
}
|
|
266
|
-
function httpPost(url, payload) {
|
|
273
|
+
function httpPost(url, payload, timeoutMs = HTTP_REQUEST_TIMEOUT_MS) {
|
|
267
274
|
return new Promise((resolve, reject) => {
|
|
268
275
|
const data = JSON.stringify(payload);
|
|
269
276
|
const urlObj = new URL(url);
|
|
@@ -286,6 +293,9 @@ function httpPost(url, payload) {
|
|
|
286
293
|
res.on('data', (chunk) => { body += chunk; });
|
|
287
294
|
res.on('end', () => resolve({ status: res.statusCode ?? 0, body }));
|
|
288
295
|
});
|
|
296
|
+
req.setTimeout(timeoutMs, () => {
|
|
297
|
+
req.destroy(requestTimeoutError(url, timeoutMs));
|
|
298
|
+
});
|
|
289
299
|
req.on('error', reject);
|
|
290
300
|
req.write(data);
|
|
291
301
|
req.end();
|
|
@@ -1138,5 +1148,5 @@ exports._internal = {
|
|
|
1138
1148
|
httpGet, httpPost, formatJobDuration, formatJobStarted, printVersion, printHelp,
|
|
1139
1149
|
handleStatus, handleJobs, handleDesktop, desktopStart, desktopStop, desktopStatus, desktopAdd, desktopRemove, desktopList, desktopServerPath,
|
|
1140
1150
|
resolveProjectFromCwd, runViaWebManager, runDirect, isPortInUse, readPid, isProcessRunning, main,
|
|
1141
|
-
isTTY, DESKTOP_PID_FILE, DESKTOP_LOG_FILE, EXIT_PATTERN, DEFAULT_PORT, DETECTION_TIMEOUT_MS,
|
|
1151
|
+
isTTY, DESKTOP_PID_FILE, DESKTOP_LOG_FILE, EXIT_PATTERN, DEFAULT_PORT, DETECTION_TIMEOUT_MS, HTTP_REQUEST_TIMEOUT_MS,
|
|
1142
1152
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specrails-desktop",
|
|
3
|
-
"version": "2.28.
|
|
3
|
+
"version": "2.28.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"check-core-compat": "tsx scripts/check-core-compat.ts",
|
|
38
38
|
"test": "vitest run && tsx scripts/check-core-compat.ts",
|
|
39
39
|
"test:watch": "vitest",
|
|
40
|
-
"test:coverage": "vitest run --coverage",
|
|
40
|
+
"test:coverage": "vitest run --coverage --maxWorkers=1 --reporter=dot --bail=1",
|
|
41
41
|
"test:client": "cd client && npx vitest run",
|
|
42
42
|
"test:all": "vitest run && cd client && npx vitest run",
|
|
43
43
|
"ci": "npm run typecheck && npm run check-core-compat && npm run test:coverage && cd client && npm run test:coverage"
|
|
@@ -163,17 +163,35 @@ function readRefineChildOutput(child, timeoutMs, kill = tree_kill_1.default) {
|
|
|
163
163
|
resolve({ fullText, resultEvent, code: -1, timedOut: false });
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
166
|
+
const stdout = child.stdout;
|
|
166
167
|
let stderrBuf = '';
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
168
|
+
const onStderrData = (chunk) => {
|
|
169
|
+
const s = typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
|
|
170
|
+
stderrBuf += s;
|
|
171
|
+
if (stderrBuf.length > 8192)
|
|
172
|
+
stderrBuf = stderrBuf.slice(-8192);
|
|
173
|
+
};
|
|
174
|
+
if (child.stderr)
|
|
175
|
+
child.stderr.on('data', onStderrData);
|
|
175
176
|
let killEscalation = null;
|
|
176
|
-
const reader = (0, node_readline_1.createInterface)({ input:
|
|
177
|
+
const reader = (0, node_readline_1.createInterface)({ input: stdout, crlfDelay: Infinity });
|
|
178
|
+
let outputClosed = false;
|
|
179
|
+
const closeOutputReaders = () => {
|
|
180
|
+
if (outputClosed)
|
|
181
|
+
return;
|
|
182
|
+
outputClosed = true;
|
|
183
|
+
reader.removeAllListeners('line');
|
|
184
|
+
try {
|
|
185
|
+
reader.close();
|
|
186
|
+
}
|
|
187
|
+
catch { /* already closed */ }
|
|
188
|
+
if (child.stderr)
|
|
189
|
+
child.stderr.off('data', onStderrData);
|
|
190
|
+
if (!stdout.destroyed)
|
|
191
|
+
stdout.destroy();
|
|
192
|
+
if (child.stderr && !child.stderr.destroyed)
|
|
193
|
+
child.stderr.destroy();
|
|
194
|
+
};
|
|
177
195
|
reader.on('line', (line) => {
|
|
178
196
|
let parsed = null;
|
|
179
197
|
try {
|
|
@@ -202,6 +220,7 @@ function readRefineChildOutput(child, timeoutMs, kill = tree_kill_1.default) {
|
|
|
202
220
|
// BUG-PARSER-01: treeKill the whole subtree (SIGTERM) and SIGKILL-escalate
|
|
203
221
|
// after a grace window so a signal-swallowing CLI tree is force-killed.
|
|
204
222
|
killEscalation = escalateKill(child.pid, kill);
|
|
223
|
+
closeOutputReaders();
|
|
205
224
|
if (!settled) {
|
|
206
225
|
settled = true;
|
|
207
226
|
resolve({ fullText, resultEvent, code: null, timedOut: true });
|
|
@@ -215,6 +234,7 @@ function readRefineChildOutput(child, timeoutMs, kill = tree_kill_1.default) {
|
|
|
215
234
|
clearTimeout(killEscalation);
|
|
216
235
|
killEscalation = null;
|
|
217
236
|
}
|
|
237
|
+
closeOutputReaders();
|
|
218
238
|
if (settled)
|
|
219
239
|
return;
|
|
220
240
|
settled = true;
|
|
@@ -229,6 +249,7 @@ function readRefineChildOutput(child, timeoutMs, kill = tree_kill_1.default) {
|
|
|
229
249
|
clearTimeout(killEscalation);
|
|
230
250
|
killEscalation = null;
|
|
231
251
|
}
|
|
252
|
+
closeOutputReaders();
|
|
232
253
|
console.log(`[contract-refine-runner] child error: ${err.message}; stderr=${JSON.stringify(stderrBuf.slice(-2000))}`);
|
|
233
254
|
if (settled)
|
|
234
255
|
return;
|
|
@@ -20,7 +20,7 @@ const providers_1 = require("./providers");
|
|
|
20
20
|
* provider behaviour (instructions file is adapter-driven: CLAUDE.md for
|
|
21
21
|
* claude, AGENTS.md for codex).
|
|
22
22
|
*
|
|
23
|
-
* Layout under
|
|
23
|
+
* Layout under `<specrails-home>/.specrails/projects/<slug>/explore-cwd/`:
|
|
24
24
|
* ├── <adapter.instructionsFilename> — embedded mini-prompt, app-owned
|
|
25
25
|
* ├── project — symlink/junction → <project.path>
|
|
26
26
|
* └── project-path.txt — fallback when symlink creation fails
|
|
@@ -87,12 +87,15 @@ function renderExploreInstructions(projectName) {
|
|
|
87
87
|
function renderExploreClaudeMd(projectName) {
|
|
88
88
|
return renderExploreInstructions(projectName);
|
|
89
89
|
}
|
|
90
|
+
function homeDir() {
|
|
91
|
+
return process.env.SPECRAILS_REGISTRY_HOME || os_1.default.homedir();
|
|
92
|
+
}
|
|
90
93
|
/**
|
|
91
94
|
* Compute the explore-cwd path for a project without touching the filesystem.
|
|
92
95
|
* Used by the legacy-cwd env-var short-circuit and by tests.
|
|
93
96
|
*/
|
|
94
97
|
function exploreCwdPathFor(slug, baseDir) {
|
|
95
|
-
const base = baseDir ?? path_1.default.join(
|
|
98
|
+
const base = baseDir ?? path_1.default.join(homeDir(), '.specrails', 'projects');
|
|
96
99
|
return path_1.default.join(base, slug, 'explore-cwd');
|
|
97
100
|
}
|
|
98
101
|
/**
|
|
@@ -102,7 +105,7 @@ function exploreCwdPathFor(slug, baseDir) {
|
|
|
102
105
|
* When SPECRAILS_EXPLORE_LEGACY_CWD=1 is set, returns `projectPath` directly
|
|
103
106
|
* and performs no filesystem IO — used as the rollback escape hatch.
|
|
104
107
|
*
|
|
105
|
-
* Pass `baseDir` for tests
|
|
108
|
+
* Pass `baseDir` for tests that need a specific projects directory.
|
|
106
109
|
*/
|
|
107
110
|
function ensureExploreCwd(input, baseDir) {
|
|
108
111
|
if (process.env.SPECRAILS_EXPLORE_LEGACY_CWD === '1') {
|
|
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.HeadroomManager = void 0;
|
|
6
|
+
exports.HeadroomManager = exports.HEADROOM_MANAGED_PYTHON_VERSION = void 0;
|
|
7
|
+
exports.getHeadroomManagedInstallPlan = getHeadroomManagedInstallPlan;
|
|
7
8
|
const child_process_1 = require("child_process");
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
9
10
|
const os_1 = __importDefault(require("os"));
|
|
@@ -12,11 +13,15 @@ const desktop_db_1 = require("./desktop-db");
|
|
|
12
13
|
const headroom_routing_1 = require("./headroom-routing");
|
|
13
14
|
const STATE_KEY = 'plugins.headroom.state';
|
|
14
15
|
const DEFAULT_PORT = 8787;
|
|
16
|
+
exports.HEADROOM_MANAGED_PYTHON_VERSION = '3.12';
|
|
15
17
|
function nowIso() {
|
|
16
18
|
return new Date().toISOString();
|
|
17
19
|
}
|
|
20
|
+
function specrailsHome() {
|
|
21
|
+
return process.env.SPECRAILS_REGISTRY_HOME || os_1.default.homedir();
|
|
22
|
+
}
|
|
18
23
|
function toolRoot() {
|
|
19
|
-
return path_1.default.join(
|
|
24
|
+
return path_1.default.join(specrailsHome(), '.specrails', 'tools');
|
|
20
25
|
}
|
|
21
26
|
function uvToolDir() {
|
|
22
27
|
return path_1.default.join(toolRoot(), 'uv', 'tools');
|
|
@@ -24,6 +29,15 @@ function uvToolDir() {
|
|
|
24
29
|
function uvCacheDir() {
|
|
25
30
|
return path_1.default.join(toolRoot(), 'uv', 'cache');
|
|
26
31
|
}
|
|
32
|
+
function uvPythonInstallDir() {
|
|
33
|
+
return path_1.default.join(toolRoot(), 'uv', 'python');
|
|
34
|
+
}
|
|
35
|
+
function uvPythonCacheDir() {
|
|
36
|
+
return path_1.default.join(toolRoot(), 'uv', 'python-cache');
|
|
37
|
+
}
|
|
38
|
+
function uvPythonBinDir() {
|
|
39
|
+
return path_1.default.join(toolRoot(), 'uv', 'python-bin');
|
|
40
|
+
}
|
|
27
41
|
function uvBinDir() {
|
|
28
42
|
return path_1.default.join(toolRoot(), 'bin');
|
|
29
43
|
}
|
|
@@ -152,6 +166,41 @@ function classifyInstallFailure(output, command) {
|
|
|
152
166
|
}
|
|
153
167
|
return issue('unknown', output, command);
|
|
154
168
|
}
|
|
169
|
+
function getHeadroomManagedInstallPlan() {
|
|
170
|
+
return {
|
|
171
|
+
pythonVersion: exports.HEADROOM_MANAGED_PYTHON_VERSION,
|
|
172
|
+
pythonInstallArgs: [
|
|
173
|
+
'python',
|
|
174
|
+
'install',
|
|
175
|
+
exports.HEADROOM_MANAGED_PYTHON_VERSION,
|
|
176
|
+
'--managed-python',
|
|
177
|
+
],
|
|
178
|
+
toolInstallArgs: [
|
|
179
|
+
'tool',
|
|
180
|
+
'install',
|
|
181
|
+
'--python',
|
|
182
|
+
exports.HEADROOM_MANAGED_PYTHON_VERSION,
|
|
183
|
+
'--managed-python',
|
|
184
|
+
'--force',
|
|
185
|
+
'headroom-ai[all]',
|
|
186
|
+
],
|
|
187
|
+
env: {
|
|
188
|
+
UV_TOOL_DIR: uvToolDir(),
|
|
189
|
+
UV_TOOL_BIN_DIR: uvBinDir(),
|
|
190
|
+
UV_CACHE_DIR: uvCacheDir(),
|
|
191
|
+
UV_PYTHON: exports.HEADROOM_MANAGED_PYTHON_VERSION,
|
|
192
|
+
UV_MANAGED_PYTHON: 'true',
|
|
193
|
+
UV_PYTHON_DOWNLOADS: 'automatic',
|
|
194
|
+
UV_PYTHON_PREFERENCE: 'only-managed',
|
|
195
|
+
UV_PYTHON_INSTALL_DIR: uvPythonInstallDir(),
|
|
196
|
+
UV_PYTHON_CACHE_DIR: uvPythonCacheDir(),
|
|
197
|
+
UV_PYTHON_BIN_DIR: uvPythonBinDir(),
|
|
198
|
+
UV_PYTHON_NO_REGISTRY: 'true',
|
|
199
|
+
UV_PYTHON_INSTALL_REGISTRY: 'false',
|
|
200
|
+
UV_NO_PROGRESS: '1',
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
155
204
|
function classifyProxyFailure(output, command) {
|
|
156
205
|
const lower = output.toLowerCase();
|
|
157
206
|
if (lower.includes('address already in use') ||
|
|
@@ -334,18 +383,32 @@ class HeadroomManager {
|
|
|
334
383
|
fs_1.default.mkdirSync(uvToolDir(), { recursive: true });
|
|
335
384
|
fs_1.default.mkdirSync(uvCacheDir(), { recursive: true });
|
|
336
385
|
fs_1.default.mkdirSync(uvBinDir(), { recursive: true });
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
386
|
+
fs_1.default.mkdirSync(uvPythonInstallDir(), { recursive: true });
|
|
387
|
+
fs_1.default.mkdirSync(uvPythonCacheDir(), { recursive: true });
|
|
388
|
+
fs_1.default.mkdirSync(uvPythonBinDir(), { recursive: true });
|
|
389
|
+
const plan = getHeadroomManagedInstallPlan();
|
|
390
|
+
const pythonCommand = `${uv} ${plan.pythonInstallArgs.join(' ')}`;
|
|
391
|
+
const installCommand = `${uv} ${plan.toolInstallArgs.join(' ')}`;
|
|
392
|
+
const logs = [
|
|
393
|
+
`Ensuring Headroom Python runtime (CPython ${plan.pythonVersion})`,
|
|
394
|
+
`Running ${pythonCommand}`,
|
|
395
|
+
];
|
|
340
396
|
this.emit('installing', logs[0]);
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
397
|
+
this.emit('installing', logs[1]);
|
|
398
|
+
const pythonResult = await this.runCommand(uv, plan.pythonInstallArgs, plan.env, logs);
|
|
399
|
+
if (pythonResult.code !== 0) {
|
|
400
|
+
const failure = classifyInstallFailure(logs.join('\n'), pythonCommand);
|
|
401
|
+
this.updatePersisted({ lastIssue: failure });
|
|
402
|
+
this.emit('failed', failure.title);
|
|
403
|
+
return { ok: false, state: this.getState(), issue: failure, logs };
|
|
404
|
+
}
|
|
405
|
+
logs.push(`Installing Headroom with managed CPython ${plan.pythonVersion}`);
|
|
406
|
+
logs.push(`Running ${installCommand}`);
|
|
407
|
+
this.emit('installing', logs[logs.length - 2]);
|
|
408
|
+
this.emit('installing', logs[logs.length - 1]);
|
|
409
|
+
const result = await this.runCommand(uv, plan.toolInstallArgs, plan.env, logs);
|
|
347
410
|
if (result.code !== 0) {
|
|
348
|
-
const failure = classifyInstallFailure(logs.join('\n'),
|
|
411
|
+
const failure = classifyInstallFailure(logs.join('\n'), installCommand);
|
|
349
412
|
this.updatePersisted({ lastIssue: failure });
|
|
350
413
|
this.emit('failed', failure.title);
|
|
351
414
|
return { ok: false, state: this.getState(), issue: failure, logs };
|
|
@@ -353,7 +416,7 @@ class HeadroomManager {
|
|
|
353
416
|
const exe = this.resolveHeadroomInstall(null, false)?.path ?? null;
|
|
354
417
|
const version = exe ? this.readHeadroomVersion(exe) : null;
|
|
355
418
|
if (!exe || !version) {
|
|
356
|
-
const failure = issue('headroom_not_found_after_install', logs.join('\n'),
|
|
419
|
+
const failure = issue('headroom_not_found_after_install', logs.join('\n'), installCommand);
|
|
357
420
|
this.updatePersisted({ lastIssue: failure });
|
|
358
421
|
this.emit('failed', failure.title);
|
|
359
422
|
return { ok: false, state: this.getState(), issue: failure, logs };
|
|
@@ -494,6 +557,9 @@ class HeadroomManager {
|
|
|
494
557
|
toolDir: uvToolDir(),
|
|
495
558
|
binDir: uvBinDir(),
|
|
496
559
|
cacheDir: uvCacheDir(),
|
|
560
|
+
pythonInstallDir: uvPythonInstallDir(),
|
|
561
|
+
pythonBinDir: uvPythonBinDir(),
|
|
562
|
+
pythonCacheDir: uvPythonCacheDir(),
|
|
497
563
|
proxyTail: this.proxyTail.slice(-4000),
|
|
498
564
|
installSource: state.installSource,
|
|
499
565
|
detectedRoutes: state.detectedRoutes,
|
|
@@ -62,8 +62,11 @@ function warnShimMissingOnce(name) {
|
|
|
62
62
|
_warnedShims.add(name);
|
|
63
63
|
console.warn(`[terminal-shell-integration] bundled shim not found: ${name} — shell integration disabled for this shell`);
|
|
64
64
|
}
|
|
65
|
+
function specrailsHome() {
|
|
66
|
+
return process.env.SPECRAILS_REGISTRY_HOME || os_1.default.homedir();
|
|
67
|
+
}
|
|
65
68
|
function projectsRoot() {
|
|
66
|
-
return path_1.default.join(
|
|
69
|
+
return path_1.default.join(specrailsHome(), '.specrails', 'projects');
|
|
67
70
|
}
|
|
68
71
|
function shimDirFor(projectSlug, sessionId) {
|
|
69
72
|
return path_1.default.join(projectsRoot(), projectSlug, 'terminals', sessionId);
|