mixdog 0.7.15 → 0.7.17
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/hooks/session-start.cjs +10 -2
- package/hooks/shim-launcher.cjs +15 -1
- package/lib/keychain-cjs.cjs +32 -5
- package/lib/plugin-paths.cjs +10 -2
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +1 -1
- package/setup/launch-core.mjs +47 -3
- package/setup/locate-claude.mjs +22 -4
- package/setup/setup-server.mjs +22 -0
- package/setup/setup.html +1 -1
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +36 -10
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +13 -4
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +11 -1
- package/src/agent/orchestrator/tools/graph-manifest.json +7 -7
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +11 -1
- package/src/agent/orchestrator/tools/patch-manifest.json +7 -7
- package/src/channels/lib/executor.mjs +59 -24
- package/src/channels/lib/hook-pipe-server.mjs +8 -1
- package/src/memory/lib/runtime-fetcher.mjs +19 -3
- package/src/search/lib/web-tools.mjs +13 -1
- package/src/shared/config.mjs +20 -1
- package/src/shared/open-url.mjs +35 -10
- package/src/shared/user-cwd.mjs +14 -2
- package/src/shared/wsl.mjs +35 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.17",
|
|
4
4
|
"description": "Claude Code all-in-one agent plugin — autonomous agents, continuous memory, cost-aware sub-agents, and syntax-aware code editing.",
|
|
5
5
|
"hooks": "./hooks/hooks.json",
|
|
6
6
|
"mcpServers": {
|
package/hooks/session-start.cjs
CHANGED
|
@@ -1257,11 +1257,19 @@ async function runRulesPart() {
|
|
|
1257
1257
|
// only when it is not already alive and never opens a browser window, so
|
|
1258
1258
|
// this is a cheap no-op once the server is running.
|
|
1259
1259
|
try {
|
|
1260
|
-
spawn('bun', [path.join(PLUGIN_ROOT, 'setup', 'launch.mjs'), '--prewarm'], {
|
|
1260
|
+
const _prewarm = spawn('bun', [path.join(PLUGIN_ROOT, 'setup', 'launch.mjs'), '--prewarm'], {
|
|
1261
1261
|
detached: true,
|
|
1262
1262
|
stdio: 'ignore',
|
|
1263
1263
|
windowsHide: true,
|
|
1264
|
-
})
|
|
1264
|
+
});
|
|
1265
|
+
// ENOENT (bun not on PATH) and other spawn failures surface ASYNC as an
|
|
1266
|
+
// 'error' event, NOT via the try/catch above. Without a listener Node
|
|
1267
|
+
// throws it as an uncaught exception and crashes the hook. Fail soft:
|
|
1268
|
+
// prewarm is a best-effort optimization, never required for a session.
|
|
1269
|
+
_prewarm.on('error', (e) => {
|
|
1270
|
+
teeStderr(`[session-start] prewarm spawn failed (non-fatal): ${(e && e.message) || e}\n`);
|
|
1271
|
+
});
|
|
1272
|
+
_prewarm.unref();
|
|
1265
1273
|
} catch {}
|
|
1266
1274
|
|
|
1267
1275
|
try {
|
package/hooks/shim-launcher.cjs
CHANGED
|
@@ -48,4 +48,18 @@ const child = spawnSync(shimPath, process.argv.slice(2), {
|
|
|
48
48
|
windowsHide: true,
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
// spawnSync reports a failure to even START the child (EACCES on a non-exec
|
|
52
|
+
// bit, ENOEXEC on a corrupt/foreign binary, etc.) via child.error, and leaves
|
|
53
|
+
// child.status === null. The old `child.status ?? 0` collapsed that null to a
|
|
54
|
+
// SUCCESS exit code, masking a broken shim as a clean run. Surface it instead.
|
|
55
|
+
if (child.error) {
|
|
56
|
+
process.stderr.write(`[mixdog-shim] failed to launch ${shimPath}: ${child.error.message}\n`);
|
|
57
|
+
process.exit(126); // 126 = command found but not executable / could not run
|
|
58
|
+
}
|
|
59
|
+
if (child.status === null) {
|
|
60
|
+
// No error object but no exit code either: the child was killed by a signal
|
|
61
|
+
// (child.signal set) or otherwise terminated abnormally. Report non-zero.
|
|
62
|
+
process.stderr.write(`[mixdog-shim] ${shimPath} terminated abnormally${child.signal ? ` (signal ${child.signal})` : ''}\n`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
process.exit(child.status);
|
package/lib/keychain-cjs.cjs
CHANGED
|
@@ -6,7 +6,19 @@ const fs = require('fs');
|
|
|
6
6
|
const { resolvePluginData } = require('./plugin-paths.cjs');
|
|
7
7
|
|
|
8
8
|
const SERVICE = 'mixdog';
|
|
9
|
-
|
|
9
|
+
// Shared bound for every synchronous keychain backend (DPAPI/PowerShell on
|
|
10
|
+
// Windows, security(1) on macOS). A single env override keeps them consistent.
|
|
11
|
+
const KEYCHAIN_TIMEOUT_MS = Number(process.env.MIXDOG_KEYCHAIN_TIMEOUT_MS || 15000);
|
|
12
|
+
const POWERSHELL_TIMEOUT_MS = KEYCHAIN_TIMEOUT_MS;
|
|
13
|
+
|
|
14
|
+
// CommonJS module: cannot import the ESM src/shared/wsl.mjs, so inline an
|
|
15
|
+
// equivalent WSL check (process.platform reports 'linux' inside WSL).
|
|
16
|
+
function isWSL() {
|
|
17
|
+
if (process.platform !== 'linux') return false;
|
|
18
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) return true;
|
|
19
|
+
try { return /microsoft|wsl/i.test(fs.readFileSync('/proc/version', 'utf8')); }
|
|
20
|
+
catch { return false; }
|
|
21
|
+
}
|
|
10
22
|
|
|
11
23
|
// ---------------------------------------------------------------------------
|
|
12
24
|
// Helpers
|
|
@@ -81,19 +93,30 @@ function dpApiFile(account) {
|
|
|
81
93
|
// darwin — security(1)
|
|
82
94
|
// ---------------------------------------------------------------------------
|
|
83
95
|
|
|
96
|
+
// Bound security(1) calls so a stuck Keychain prompt (locked keychain, GUI
|
|
97
|
+
// unlock dialog with no display) cannot block hook/server callers forever —
|
|
98
|
+
// matching the Windows DPAPI and Linux keytar timeouts.
|
|
99
|
+
function darwinRun(args) {
|
|
100
|
+
const r = run('security', args, { timeout: KEYCHAIN_TIMEOUT_MS });
|
|
101
|
+
if (r.error && r.error.code === 'ETIMEDOUT') {
|
|
102
|
+
throw new Error(`[keychain] security command timed out after ${KEYCHAIN_TIMEOUT_MS}ms`);
|
|
103
|
+
}
|
|
104
|
+
return r;
|
|
105
|
+
}
|
|
106
|
+
|
|
84
107
|
function darwinGet(account) {
|
|
85
|
-
const r =
|
|
108
|
+
const r = darwinRun(['find-generic-password', '-a', account, '-s', SERVICE, '-w']);
|
|
86
109
|
if (r.status !== 0) return null;
|
|
87
110
|
return r.stdout.trimEnd();
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
function darwinSet(account, value) {
|
|
91
|
-
const r =
|
|
114
|
+
const r = darwinRun(['add-generic-password', '-a', account, '-s', SERVICE, '-w', value, '-U']);
|
|
92
115
|
if (r.status !== 0) throw new Error(`[keychain] security set failed: ${r.stderr || r.stdout}`);
|
|
93
116
|
}
|
|
94
117
|
|
|
95
118
|
function darwinDelete(account) {
|
|
96
|
-
const r =
|
|
119
|
+
const r = darwinRun(['delete-generic-password', '-a', account, '-s', SERVICE]);
|
|
97
120
|
if (r.status !== 0) throw new Error(`[keychain] security delete failed: ${r.stderr || r.stdout}`);
|
|
98
121
|
}
|
|
99
122
|
|
|
@@ -112,7 +135,11 @@ function loadKeytar() {
|
|
|
112
135
|
throw new Error(
|
|
113
136
|
'[keychain] keytar is not installed — run: npm install keytar\n' +
|
|
114
137
|
' Requires libsecret-dev (Debian/Ubuntu) or libsecret (other distros).\n' +
|
|
115
|
-
' Cannot access credentials on this platform without it.'
|
|
138
|
+
' Cannot access credentials on this platform without it.' +
|
|
139
|
+
(isWSL()
|
|
140
|
+
? '\n Detected WSL: there is usually no Secret Service here; ' +
|
|
141
|
+
'set the relevant MIXDOG_*/PROVIDER_API_KEY env var instead.'
|
|
142
|
+
: '')
|
|
116
143
|
);
|
|
117
144
|
}
|
|
118
145
|
return _keytarMod;
|
package/lib/plugin-paths.cjs
CHANGED
|
@@ -29,6 +29,14 @@ const fs = require('fs');
|
|
|
29
29
|
const DEFAULT_PLUGIN = 'mixdog';
|
|
30
30
|
const DEFAULT_MARKETPLACE = 'trib-plugin';
|
|
31
31
|
|
|
32
|
+
// Claude config base — honours CLAUDE_CONFIG_DIR when set, otherwise the
|
|
33
|
+
// real Claude Code default of ~/.claude (matches settings-loader.cjs,
|
|
34
|
+
// statusline, doctor, install). Only ADDS the env override; never relocates
|
|
35
|
+
// the default base.
|
|
36
|
+
function claudeConfigBase() {
|
|
37
|
+
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
function readPluginManifestName(root) {
|
|
33
41
|
try {
|
|
34
42
|
const manifest = JSON.parse(fs.readFileSync(path.join(root, '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
@@ -46,14 +54,14 @@ function resolvePluginData() {
|
|
|
46
54
|
if (/^\d+\.\d+\.\d+/.test(dirName)) {
|
|
47
55
|
const pluginName = path.basename(path.join(root, '..'));
|
|
48
56
|
const marketplace = path.basename(path.join(root, '..', '..'));
|
|
49
|
-
return path.join(
|
|
57
|
+
return path.join(claudeConfigBase(), 'plugins', 'data', `${pluginName}-${marketplace}`);
|
|
50
58
|
}
|
|
51
59
|
// Marketplace layout: .../marketplaces/{marketplace}/
|
|
52
60
|
// The root dir itself is the marketplace. Plugin name lives in the
|
|
53
61
|
// manifest; fall back to DEFAULT_PLUGIN when it's unreadable.
|
|
54
62
|
const marketplace = dirName;
|
|
55
63
|
const pluginName = readPluginManifestName(root);
|
|
56
|
-
return path.join(
|
|
64
|
+
return path.join(claudeConfigBase(), 'plugins', 'data', `${pluginName}-${marketplace}`);
|
|
57
65
|
}
|
|
58
66
|
throw new Error('[plugin-paths] CLAUDE_PLUGIN_DATA and CLAUDE_PLUGIN_ROOT are both unset — cannot resolve plugin data dir outside of Claude Code.');
|
|
59
67
|
}
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.17",
|
|
4
4
|
"description": "Claude Code all-in-one bridge plugin: role-based bridge workers, continuous memory, and syntax-aware code editing.",
|
|
5
5
|
"author": "mixdog contributors <dev@tribgames.com>",
|
|
6
6
|
"license": "MIT",
|
package/setup/launch-core.mjs
CHANGED
|
@@ -157,6 +157,47 @@ function releaseLaunchLock() {
|
|
|
157
157
|
try { unlinkSync(LAUNCH_LOCK_PATH); } catch {}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
// POSIX listener-pid resolution with graceful degradation across minimal
|
|
161
|
+
// images. Tries lsof → ss → fuser; returns the first listener pid found, or
|
|
162
|
+
// null if none of the tools exist / none report a listener. A missing tool
|
|
163
|
+
// throws ENOENT from execSync, which we swallow and move to the next probe so
|
|
164
|
+
// a bare container without lsof does not wedge port reclaim.
|
|
165
|
+
function resolveListenerPidPosix(trace) {
|
|
166
|
+
// lsof: -ti prints bare pids, -sTCP:LISTEN restricts to the listening socket.
|
|
167
|
+
try {
|
|
168
|
+
const out = execSync(`lsof -ti :${PORT} -sTCP:LISTEN`, { encoding: 'utf8', timeout: 3000 }).trim();
|
|
169
|
+
const n = parseInt(out.split('\n')[0], 10);
|
|
170
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
171
|
+
} catch (e) {
|
|
172
|
+
if (trace) trace(`reclaimPort: lsof unavailable/failed (${e?.code || e?.message || e}) → trying ss`);
|
|
173
|
+
}
|
|
174
|
+
// ss (iproute2): -H no header, -l listening, -t tcp, -n numeric, -p process.
|
|
175
|
+
// The pid lives in the trailing `users:(("name",pid=1234,fd=7))` field.
|
|
176
|
+
try {
|
|
177
|
+
const out = execSync(`ss -Hltnp 'sport = :${PORT}'`, { encoding: 'utf8', timeout: 3000 }).trim();
|
|
178
|
+
const m = out.match(/pid=(\d+)/);
|
|
179
|
+
if (m) {
|
|
180
|
+
const n = parseInt(m[1], 10);
|
|
181
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
182
|
+
}
|
|
183
|
+
} catch (e) {
|
|
184
|
+
if (trace) trace(`reclaimPort: ss unavailable/failed (${e?.code || e?.message || e}) → trying fuser`);
|
|
185
|
+
}
|
|
186
|
+
// fuser: prints the pids holding the tcp port to stdout (diagnostics on
|
|
187
|
+
// stderr). Take the first numeric token.
|
|
188
|
+
try {
|
|
189
|
+
const out = execSync(`fuser ${PORT}/tcp 2>/dev/null`, { encoding: 'utf8', timeout: 3000 }).trim();
|
|
190
|
+
const m = out.match(/\d+/);
|
|
191
|
+
if (m) {
|
|
192
|
+
const n = parseInt(m[0], 10);
|
|
193
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
if (trace) trace(`reclaimPort: fuser unavailable/failed (${e?.code || e?.message || e}) → no listener pid resolved`);
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
160
201
|
// Reclaim PORT from a version-mismatched setup-server: resolve the LISTENER
|
|
161
202
|
// pid, kill it (tree on win32, process-group on unix), wait for the port to
|
|
162
203
|
// free, then spawn the current-version server window-free. Throws LaunchError
|
|
@@ -179,9 +220,12 @@ async function reclaimPort(pluginRoot, pluginData, remoteRoot, trace) {
|
|
|
179
220
|
const n = parseInt(out, 10);
|
|
180
221
|
if (Number.isFinite(n) && n > 0) stalePid = n;
|
|
181
222
|
} else {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
223
|
+
// Resolve the LISTENER pid. lsof is the primary tool, but minimal
|
|
224
|
+
// Linux/WSL/container images often ship without it — falling back to
|
|
225
|
+
// ss (iproute2) then fuser keeps port reclaim working there instead
|
|
226
|
+
// of wedging on a missing-binary ENOENT. Each probe is independent and
|
|
227
|
+
// best-effort; the first that yields a pid wins.
|
|
228
|
+
stalePid = resolveListenerPidPosix(trace);
|
|
185
229
|
}
|
|
186
230
|
} catch (e) { pidResolveError = e; }
|
|
187
231
|
if (stalePid !== null) {
|
package/setup/locate-claude.mjs
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
// locate-claude.mjs — shared PATH scan for the Claude Code CLI executable.
|
|
2
2
|
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import { existsSync } from 'node:fs';
|
|
4
|
+
import { existsSync, statSync, accessSync, constants as fsConstants } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
|
|
7
|
+
// POSIX: a PATH match is only a real candidate if it is a regular file the
|
|
8
|
+
// current user may EXECUTE. existsSync alone would return a non-exec data file
|
|
9
|
+
// (e.g. a `claude` config/dir or a 0644 stray) and the caller would then fail
|
|
10
|
+
// to spawn it. On win32 executability is governed by PATHEXT, not a mode bit,
|
|
11
|
+
// so existence is the right test there.
|
|
12
|
+
function isUsable(p) {
|
|
13
|
+
const win32 = process.platform === 'win32';
|
|
14
|
+
try {
|
|
15
|
+
if (!existsSync(p)) return false;
|
|
16
|
+
if (win32) return true;
|
|
17
|
+
if (!statSync(p).isFile()) return false;
|
|
18
|
+
accessSync(p, fsConstants.X_OK);
|
|
19
|
+
return true;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
export function resolveClaudeExecutable() {
|
|
8
26
|
const win32 = process.platform === 'win32';
|
|
9
27
|
try {
|
|
10
28
|
const cmd = win32 ? 'where' : 'which';
|
|
11
29
|
const out = execFileSync(cmd, ['claude'], { encoding: 'utf8', windowsHide: true }).trim();
|
|
12
30
|
const first = out.split(/\r?\n/).map((l) => l.trim()).find(Boolean);
|
|
13
|
-
if (first &&
|
|
31
|
+
if (first && isUsable(first)) return first;
|
|
14
32
|
} catch { /* PATH scan */ }
|
|
15
33
|
|
|
16
34
|
const pathSep = win32 ? ';' : ':';
|
|
@@ -30,9 +48,9 @@ export function resolveClaudeExecutable() {
|
|
|
30
48
|
if (existsSync(bare)) return bare;
|
|
31
49
|
} else {
|
|
32
50
|
const candidate = join(dir, base);
|
|
33
|
-
if (
|
|
51
|
+
if (isUsable(candidate)) return candidate;
|
|
34
52
|
}
|
|
35
53
|
}
|
|
36
54
|
}
|
|
37
55
|
return null;
|
|
38
|
-
}
|
|
56
|
+
}
|
package/setup/setup-server.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { readSection, writeSection, updateSection, saveSecret, deleteSecret, has
|
|
|
19
19
|
import { applyDefaults as applyChannelsDefaults } from '../src/channels/lib/config.mjs';
|
|
20
20
|
import { validateCronExpression } from '../src/channels/lib/scheduler.mjs';
|
|
21
21
|
import { mergeAgentConfig, mergeMemoryConfig, mergeSearchConfig, mergeConfig, mergeEndpointConfig, mergeWebhookEndpointConfig } from './config-merge.mjs';
|
|
22
|
+
import { isWSL } from '../src/shared/wsl.mjs';
|
|
22
23
|
|
|
23
24
|
// C2 — Origin/Referer guard for mutating routes.
|
|
24
25
|
// Returns true when the request is safe to handle (same-origin loopback UI,
|
|
@@ -1465,6 +1466,27 @@ async function openAppWindowSequence() {
|
|
|
1465
1466
|
return { ...macResult, method: 'macOS open', attempts: [macAttempt] };
|
|
1466
1467
|
}
|
|
1467
1468
|
|
|
1469
|
+
// WSL: process.platform is 'linux' but xdg-open targets a (usually absent)
|
|
1470
|
+
// Linux GUI. Reach the Windows-HOST browser instead, mirroring open-url.mjs:
|
|
1471
|
+
// prefer wslview (wslu), then PowerShell Start-Process (cmd.exe is avoided —
|
|
1472
|
+
// see the note below). Each is a sync spawn through trySyncOpen so a missing
|
|
1473
|
+
// opener advances to the next.
|
|
1474
|
+
if (isWSL()) {
|
|
1475
|
+
if (trySyncOpen('wslview', 'wslview', [appUrl], attempts)) {
|
|
1476
|
+
return { ok: true, method: 'wslview', attempts };
|
|
1477
|
+
}
|
|
1478
|
+
// cmd.exe `start` is intentionally NOT used: cmd re-parses URL
|
|
1479
|
+
// metacharacters, so a query-string `&` is treated as a command separator
|
|
1480
|
+
// and the URL truncates. Start-Process takes the URL as a single argument
|
|
1481
|
+
// and does not reparse `&`; quotePowerShellString single-quotes the URL
|
|
1482
|
+
// (doubling embedded quotes) so it is injection-safe.
|
|
1483
|
+
const wslPsCommand = `Start-Process -FilePath ${quotePowerShellString(appUrl)}`;
|
|
1484
|
+
if (trySyncOpen('PowerShell Start-Process', 'powershell.exe', ['-NoProfile', '-NonInteractive', '-WindowStyle', 'Hidden', '-Command', wslPsCommand], attempts)) {
|
|
1485
|
+
return { ok: true, method: 'PowerShell Start-Process', attempts };
|
|
1486
|
+
}
|
|
1487
|
+
return { ok: false, error: 'Failed to open Windows browser from WSL', attempts };
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1468
1490
|
const xdgResult = await new Promise(resolve => {
|
|
1469
1491
|
const child = spawn('xdg-open', [appUrl], { stdio: 'ignore' });
|
|
1470
1492
|
let timer = setTimeout(() => {
|
package/setup/setup.html
CHANGED
|
@@ -1310,7 +1310,7 @@
|
|
|
1310
1310
|
<span style="font-size:11px;color:var(--text-3)">Description</span>
|
|
1311
1311
|
<span class="file-open" onclick="wfOpenDescription()">Open</span>
|
|
1312
1312
|
</div>
|
|
1313
|
-
<textarea id="wf-description" class="r-input" placeholder="Free-form workflow description (markdown)." style="width:100%;height:
|
|
1313
|
+
<textarea id="wf-description" class="r-input" placeholder="Free-form workflow description (markdown)." style="width:100%;height:330px;resize:vertical;font-family:inherit;padding:10px;line-height:1.5"></textarea>
|
|
1314
1314
|
</div>
|
|
1315
1315
|
<div class="r r-opt">
|
|
1316
1316
|
<div class="opt-body" style="width:100%">
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'os';
|
|
2
2
|
import { isAbsolute, relative, resolve } from 'path';
|
|
3
3
|
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { isWSL } from '../../../../shared/wsl.mjs';
|
|
4
5
|
|
|
5
6
|
// Restore the on-disk casing of a path (win32 only). rg relativizes candidate
|
|
6
7
|
// paths against its process cwd with a CASE-SENSITIVE prefix strip before
|
|
@@ -28,6 +29,21 @@ export function posixPathToWindowsPath(posixPath) {
|
|
|
28
29
|
return posixPath;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
// Reverse of posixPathToWindowsPath, for WSL: a Windows-HOST drive path that
|
|
33
|
+
// arrives from the host (e.g. `C:\Users\x` or `C:/Users/x`) is unreachable as
|
|
34
|
+
// a literal under WSL — the drive surfaces at `/mnt/<letter>/...`. Map
|
|
35
|
+
// `<drive>:` → `/mnt/<lowercase-drive>` and flip backslashes to slashes. Only
|
|
36
|
+
// drive-letter paths are touched; anything else is returned unchanged.
|
|
37
|
+
export function windowsPathToPosixPath(winPath) {
|
|
38
|
+
if (typeof winPath !== 'string') return winPath;
|
|
39
|
+
const m = winPath.match(/^([a-zA-Z]):[\\/](.*)$/);
|
|
40
|
+
if (m) return `/mnt/${m[1].toLowerCase()}/${m[2].replace(/\\/g, '/')}`;
|
|
41
|
+
// Bare `C:` with no separator → drive root.
|
|
42
|
+
const root = winPath.match(/^([a-zA-Z]):$/);
|
|
43
|
+
if (root) return `/mnt/${root[1].toLowerCase()}/`;
|
|
44
|
+
return winPath;
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
export function normalizeInputPath(p) {
|
|
32
48
|
if (typeof p !== 'string') return p;
|
|
33
49
|
// Trim leading/trailing whitespace — LLMs intermittently emit paths with
|
|
@@ -49,6 +65,13 @@ export function normalizeInputPath(p) {
|
|
|
49
65
|
if (looksPosixDrive || looksCygdrive || looksWsl || looksUnc) {
|
|
50
66
|
out = posixPathToWindowsPath(out);
|
|
51
67
|
}
|
|
68
|
+
} else if (isWSL()) {
|
|
69
|
+
// Reverse direction: a Windows-style drive path from the host maps to
|
|
70
|
+
// the /mnt/<letter> mount. Native-Linux behavior is untouched (gated
|
|
71
|
+
// on isWSL()); a path already in /mnt/... POSIX form is left as-is.
|
|
72
|
+
if (/^[a-zA-Z]:([\\/]|$)/.test(out)) {
|
|
73
|
+
out = windowsPathToPosixPath(out);
|
|
74
|
+
}
|
|
52
75
|
}
|
|
53
76
|
try { out = out.normalize('NFC'); } catch {}
|
|
54
77
|
return out;
|
|
@@ -712,7 +712,11 @@ export function adoptForegroundShellJob({ command, cwd, pid, timeoutMs, mergeStd
|
|
|
712
712
|
}
|
|
713
713
|
|
|
714
714
|
function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, shellArg, shellType, clientHostPid }) {
|
|
715
|
-
|
|
715
|
+
// Route ANY PowerShell shell to the PS wrapper, regardless of platform.
|
|
716
|
+
// Gating on win32 sent shell:'powershell' on macOS/Linux down the POSIX
|
|
717
|
+
// path below, which spawns `pwsh <wrapper>.sh` — pwsh then tries to run a
|
|
718
|
+
// bash script. isPowerShellShell already matches pwsh/powershell by stem.
|
|
719
|
+
if (isPowerShellShell(shell, shellType)) {
|
|
716
720
|
return startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr, spawnEnv, shell, clientHostPid });
|
|
717
721
|
}
|
|
718
722
|
|
|
@@ -738,9 +742,11 @@ function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr
|
|
|
738
742
|
// only the setTimeout below enforced; a mixdog restart between spawn
|
|
739
743
|
// and deadline would orphan the runaway. --preserve-status keeps the
|
|
740
744
|
// user command's exit code on success; on timeout the wrapper exits 124.
|
|
741
|
-
// `timeout` ships with GNU coreutils on Linux
|
|
742
|
-
//
|
|
743
|
-
//
|
|
745
|
+
// `timeout` ships with GNU coreutils on Linux. On macOS, Homebrew
|
|
746
|
+
// coreutils installs it as `gtimeout` (the un-prefixed name is NOT created
|
|
747
|
+
// by default), so the wrapper picks `timeout` if present else `gtimeout`.
|
|
748
|
+
// When neither exists it falls through to the inner command (the parent
|
|
749
|
+
// setTimeout still calls refreshShellJob to clean up).
|
|
744
750
|
const userCmdQuoted = shellQuoteSingle(command);
|
|
745
751
|
// P2 fix: invoke the resolved shell (not bash -c) so zsh / dash /
|
|
746
752
|
// alternate shells run snapshot-aware commands correctly. Drop
|
|
@@ -762,7 +768,7 @@ function _startBackgroundShellJobImpl({ command, timeoutMs, workDir, mergeStderr
|
|
|
762
768
|
// status for processes that actually exited 0. `rm -- "$0"` removes
|
|
763
769
|
// the staged wrapper .cmd.sh after donePath is published so a host
|
|
764
770
|
// crash before this point still leaves the file for the sweep to GC.
|
|
765
|
-
const wrapped = `{ if command -v timeout >/dev/null 2>&1; then touch ${shellQuoteSingle(enforcedPath)};
|
|
771
|
+
const wrapped = `{ if command -v timeout >/dev/null 2>&1; then _to=timeout; elif command -v gtimeout >/dev/null 2>&1; then _to=gtimeout; else _to=; fi; if [ -n "$_to" ]; then touch ${shellQuoteSingle(enforcedPath)}; "$_to" ${timeoutSeconds} ${innerShellQ} ${innerArgQ} ${userCmdQuoted}; else ${innerShellQ} ${innerArgQ} ${userCmdQuoted}; fi; rc=$?; printf '%s' "$rc" > ${shellQuoteSingle(exitPath)}; touch ${shellQuoteSingle(donePath)}; rm -- "$0" 2>/dev/null; exit $rc; }`;
|
|
766
772
|
// Stage the wrapped command to a .sh and let the script open its own
|
|
767
773
|
// output files via `exec > … 2> …`. The parent does NOT pass file
|
|
768
774
|
// descriptors via stdio inheritance (`stdio: 'ignore'` for all three).
|
|
@@ -841,9 +847,24 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
|
|
|
841
847
|
'$code = 1',
|
|
842
848
|
'try {',
|
|
843
849
|
" $argList = @('-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', $encoded)",
|
|
844
|
-
|
|
850
|
+
// -WindowStyle is a Windows-only Start-Process parameter; pwsh on
|
|
851
|
+
// macOS/Linux throws "not supported on this platform". Add it only on win32.
|
|
852
|
+
' $spArgs = @{ FilePath = $exe; ArgumentList = $argList; RedirectStandardOutput = $stdoutPath; RedirectStandardError = $stderrPath; PassThru = $true }',
|
|
853
|
+
' if ($IsWindows -or $null -eq $IsWindows) { $spArgs[\'WindowStyle\'] = \'Hidden\' }',
|
|
854
|
+
' $p = Start-Process @spArgs',
|
|
845
855
|
' if ($timeoutMs -gt 0 -and -not $p.WaitForExit($timeoutMs)) {',
|
|
846
|
-
|
|
856
|
+
// Kill the whole process TREE, not just the direct child: Start-Process
|
|
857
|
+
// launches an intermediate pwsh that spawns the user command, so
|
|
858
|
+
// Stop-Process on $p.Id alone orphans the grandchildren. On Windows use
|
|
859
|
+
// `taskkill /T /F` (tree-terminate); on macOS/Linux pwsh, fall back to
|
|
860
|
+
// Stop-Process (no taskkill there).
|
|
861
|
+
' try {',
|
|
862
|
+
' if ($IsWindows -or $null -eq $IsWindows) {',
|
|
863
|
+
' & taskkill.exe /pid $p.Id /t /f 2>$null | Out-Null',
|
|
864
|
+
' } else {',
|
|
865
|
+
' try { Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue } catch {}',
|
|
866
|
+
' }',
|
|
867
|
+
' } catch {}',
|
|
847
868
|
' $code = 124',
|
|
848
869
|
' } else {',
|
|
849
870
|
' try { $p.WaitForExit() } catch {}',
|
|
@@ -875,9 +896,14 @@ function startBackgroundPowerShellJob({ command, timeoutMs, workDir, mergeStderr
|
|
|
875
896
|
}
|
|
876
897
|
|
|
877
898
|
const shellStem = basename(String(shell || '')).toLowerCase().replace(/\.exe$/, '');
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
899
|
+
// `-WindowStyle Hidden` is a Windows-only CLI switch; pwsh on macOS/Linux
|
|
900
|
+
// rejects it. `-ExecutionPolicy` likewise only applies to Windows
|
|
901
|
+
// PowerShell. Build args per-platform so cross-OS pwsh background jobs run.
|
|
902
|
+
const isWin = process.platform === 'win32';
|
|
903
|
+
const wrapperArgs = ['-NoLogo', '-NoProfile', '-NonInteractive'];
|
|
904
|
+
if (isWin) wrapperArgs.push('-WindowStyle', 'Hidden');
|
|
905
|
+
if (isWin && shellStem === 'powershell') wrapperArgs.push('-ExecutionPolicy', 'Bypass');
|
|
906
|
+
wrapperArgs.push('-File', wrappedTempPath);
|
|
881
907
|
// Spawn the staged wrapper directly. detached MUST be false on Windows:
|
|
882
908
|
// a native pwsh launched with detached:true + stdio:'ignore' exits
|
|
883
909
|
// immediately without running -File (verified — the detached child dies
|
|
@@ -19,10 +19,15 @@ function shellTypeFor(shell) {
|
|
|
19
19
|
|
|
20
20
|
function shellSpec(shell, shellType = shellTypeFor(shell)) {
|
|
21
21
|
if (shellType === 'powershell') {
|
|
22
|
+
// `-WindowStyle Hidden` is a Windows-only switch. PowerShell Core
|
|
23
|
+
// (pwsh) on macOS/Linux rejects it, so only append it on win32.
|
|
24
|
+
const psArgs = ['-NoLogo', '-NoProfile', '-NonInteractive'];
|
|
25
|
+
if (process.platform === 'win32') psArgs.push('-WindowStyle', 'Hidden');
|
|
26
|
+
psArgs.push('-Command');
|
|
22
27
|
return {
|
|
23
28
|
shell,
|
|
24
29
|
shellArg: '-Command',
|
|
25
|
-
shellArgs:
|
|
30
|
+
shellArgs: psArgs,
|
|
26
31
|
shellType,
|
|
27
32
|
};
|
|
28
33
|
}
|
|
@@ -60,8 +65,10 @@ function resolveWindowsPowerShell() {
|
|
|
60
65
|
|
|
61
66
|
export function resolveShell() {
|
|
62
67
|
if (_resolvedShell) return _resolvedShell;
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
// Gate on the actual platform, NOT WINDIR/SystemRoot env presence: under
|
|
69
|
+
// WSL those vars can be inherited via interop while process.platform is
|
|
70
|
+
// 'linux', and WSL must resolve /bin/sh — not Windows PowerShell.
|
|
71
|
+
const isWindows = process.platform === 'win32';
|
|
65
72
|
if (!isWindows) {
|
|
66
73
|
_resolvedShell = shellSpec('/bin/sh', 'posix');
|
|
67
74
|
return _resolvedShell;
|
|
@@ -76,7 +83,9 @@ export function resolveShell() {
|
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
function _isWindows() {
|
|
79
|
-
|
|
86
|
+
// Real-platform check only (see resolveShell): env presence would make
|
|
87
|
+
// WSL (process.platform 'linux') mis-resolve to Windows Git Bash.
|
|
88
|
+
return process.platform === 'win32';
|
|
80
89
|
}
|
|
81
90
|
|
|
82
91
|
// Resolve Git Bash on Windows. Strategy (invariant-based, no silent fallback):
|
|
@@ -118,7 +118,17 @@ export function ensureGraphBinary(dataDir) {
|
|
|
118
118
|
const pkey = platformKey();
|
|
119
119
|
const asset = manifest.assets?.[pkey];
|
|
120
120
|
if (!asset || !asset.url || !asset.sha256) {
|
|
121
|
-
|
|
121
|
+
// Unsupported platform/arch (e.g. win32-arm64): the manifest has no
|
|
122
|
+
// downloadable asset for this {os}-{arch}. The code graph has NO JS
|
|
123
|
+
// parsing fallback, so this is terminal — surface a single clear,
|
|
124
|
+
// actionable message instead of a cryptic crash downstream.
|
|
125
|
+
const supported = Object.keys(manifest.assets || {}).join(', ') || '(none)';
|
|
126
|
+
throw new Error(
|
|
127
|
+
`[graph-fetcher] no prebuilt mixdog-graph binary for platform ${pkey} `
|
|
128
|
+
+ `(unsupported platform/arch — there is no JS parsing fallback). `
|
|
129
|
+
+ `Supported platforms: ${supported}. `
|
|
130
|
+
+ `Build it locally: cargo build --release in native/mixdog-graph.`,
|
|
131
|
+
);
|
|
122
132
|
}
|
|
123
133
|
const version = String(manifest.version || '0');
|
|
124
134
|
const dir = graphBinDir(dataDir);
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.7.
|
|
2
|
+
"version": "0.7.17",
|
|
3
3
|
"_comment": "Rewritten by .github/workflows/graph-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-graph binary on the GitHub release. A local cargo build under native/mixdog-graph/target/release always takes precedence at runtime. (v0.5.236 entries were filled manually after CI's commit step hit detached HEAD; the workflow now checks out ref: main so future releases self-update.)",
|
|
4
4
|
"assets": {
|
|
5
5
|
"darwin-arm64": {
|
|
6
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
6
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.17/mixdog-graph-darwin-arm64",
|
|
7
7
|
"sha256": "53f45f8373000fb55ccb49f9b03af0275ca2a663f924b68ee8ded4535e14445a"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
10
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.17/mixdog-graph-darwin-x64",
|
|
11
11
|
"sha256": "347dc5e1b39dfc33ae07e01319b085c1f1ee775a70be1ac09f7b0d2954b36591"
|
|
12
12
|
},
|
|
13
13
|
"linux-arm64": {
|
|
14
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
14
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.17/mixdog-graph-linux-arm64",
|
|
15
15
|
"sha256": "7648be2c4bc89bef837b4843639a9ab38c382ba103518ad52d4439587521542f"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
18
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.17/mixdog-graph-linux-x64",
|
|
19
19
|
"sha256": "6e5a1b548c69c3a51170eb11c769985be7ca7029ede2597b19304c8dc106eded"
|
|
20
20
|
},
|
|
21
21
|
"win32-x64": {
|
|
22
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
23
|
-
"sha256": "
|
|
22
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.17/mixdog-graph-win32-x64.exe",
|
|
23
|
+
"sha256": "cd263b6bfd3d7371857f498fb53d1d67b4b4cc06dd4d9855408337390a56cee2"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -107,7 +107,17 @@ export function ensurePatchBinary(dataDir) {
|
|
|
107
107
|
const pkey = platformKey();
|
|
108
108
|
const asset = manifest.assets?.[pkey];
|
|
109
109
|
if (!asset || !asset.url || !asset.sha256) {
|
|
110
|
-
|
|
110
|
+
// Unsupported platform/arch (e.g. win32-arm64): the manifest has no
|
|
111
|
+
// downloadable asset for this {os}-{arch}. apply_patch is native-only
|
|
112
|
+
// (no JS apply fallback), so this is terminal — surface a single clear,
|
|
113
|
+
// actionable message instead of a cryptic crash downstream.
|
|
114
|
+
const supported = Object.keys(manifest.assets || {}).join(', ') || '(none)';
|
|
115
|
+
throw new Error(
|
|
116
|
+
`[patch-fetcher] no prebuilt mixdog-patch binary for platform ${pkey} `
|
|
117
|
+
+ `(unsupported platform/arch — apply_patch is native-only, no JS apply fallback). `
|
|
118
|
+
+ `Supported platforms: ${supported}. `
|
|
119
|
+
+ `Build it locally: cargo build --release in native/mixdog-patch.`,
|
|
120
|
+
);
|
|
111
121
|
}
|
|
112
122
|
const version = String(manifest.version || '0');
|
|
113
123
|
const dir = patchBinDir(dataDir);
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.7.
|
|
2
|
+
"version": "0.7.17",
|
|
3
3
|
"_comment": "Rewritten by .github/workflows/patch-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-patch binary on the GitHub release. A local cargo build under native/mixdog-patch/target/release always takes precedence; otherwise the binary is fetched per this manifest into the data dir (apply is native-only — no JS apply engine).",
|
|
4
4
|
"assets": {
|
|
5
5
|
"darwin-arm64": {
|
|
6
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
6
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.17/mixdog-patch-darwin-arm64",
|
|
7
7
|
"sha256": "836a0b60a443b0a6a8c1bbe24d15a79ed70ee92c2f0fbc05374c4e9ed2536415"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
10
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.17/mixdog-patch-darwin-x64",
|
|
11
11
|
"sha256": "cab10c4e1e8b72d3958241dffdff764712ed74f4861d105bafa8258961215c98"
|
|
12
12
|
},
|
|
13
13
|
"linux-arm64": {
|
|
14
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
14
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.17/mixdog-patch-linux-arm64",
|
|
15
15
|
"sha256": "a90c32ce3417a7d853f2723f82f3613cf2cd030fe885cf710cfc9f8e4b193264"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
18
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.17/mixdog-patch-linux-x64",
|
|
19
19
|
"sha256": "0fea40ab98acd35bfb47515756024d1882a2abbaddce8a0b51642d20ac405577"
|
|
20
20
|
},
|
|
21
21
|
"win32-x64": {
|
|
22
|
-
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.
|
|
23
|
-
"sha256": "
|
|
22
|
+
"url": "https://github.com/trib-plugin/mixdog/releases/download/v0.7.17/mixdog-patch-win32-x64.exe",
|
|
23
|
+
"sha256": "af92ae25c2434f9b2999df0186e11f6a8802d70bcbc4b91290cfd92f3e143466"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -130,30 +130,65 @@ function runScript(name, scriptName, onResult) {
|
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
132
|
const ext = extname(scriptName).toLowerCase();
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
133
|
+
// Pick interpreter candidates. `python3` does not exist on a default
|
|
134
|
+
// Windows install — the Python launcher `py` (and often `python`) does —
|
|
135
|
+
// so on win32 try py → python → python3, falling through on ENOENT.
|
|
136
|
+
// POSIX keeps python3 → python.
|
|
137
|
+
let candidates;
|
|
138
|
+
if (ext === ".py") {
|
|
139
|
+
candidates = process.platform === "win32"
|
|
140
|
+
? ["py", "python", "python3"]
|
|
141
|
+
: ["python3", "python"];
|
|
142
|
+
} else {
|
|
143
|
+
candidates = ["node"];
|
|
144
|
+
}
|
|
145
|
+
// onResult MUST fire exactly once across the whole candidate chain. A failed
|
|
146
|
+
// ENOENT spawn emits BOTH 'error' (→ we advance) AND 'close', so guard the
|
|
147
|
+
// final callback at the chain level and mark each attempt that advanced so
|
|
148
|
+
// its own 'close' is ignored.
|
|
149
|
+
let resultSent = false;
|
|
150
|
+
const finish = (out, code) => {
|
|
151
|
+
if (resultSent) return;
|
|
152
|
+
resultSent = true;
|
|
153
|
+
onResult(out, code);
|
|
154
|
+
};
|
|
155
|
+
const trySpawn = (idx) => {
|
|
156
|
+
const cmd = candidates[idx];
|
|
157
|
+
let advanced = false;
|
|
158
|
+
const proc = spawn(cmd, [scriptPath], {
|
|
159
|
+
timeout: 3e4,
|
|
160
|
+
env: { ...process.env },
|
|
161
|
+
windowsHide: true
|
|
162
|
+
});
|
|
163
|
+
let stdout = "";
|
|
164
|
+
let stderr = "";
|
|
165
|
+
if (proc.stdout) proc.stdout.on("data", (d) => {
|
|
166
|
+
stdout += d;
|
|
167
|
+
});
|
|
168
|
+
if (proc.stderr) proc.stderr.on("data", (d) => {
|
|
169
|
+
stderr += d;
|
|
170
|
+
});
|
|
171
|
+
proc.on("close", (code) => {
|
|
172
|
+
// This attempt ENOENT'd and handed off to the next candidate — its
|
|
173
|
+
// 'close' is spurious and must not report a result.
|
|
174
|
+
if (advanced) return;
|
|
175
|
+
if (code !== 0) {
|
|
176
|
+
logEvent(`${name}: script exited ${code}: ${stderr.substring(0, 500)}`);
|
|
177
|
+
}
|
|
178
|
+
finish(stdout.substring(0, 2e3), code);
|
|
179
|
+
});
|
|
180
|
+
proc.on("error", (err) => {
|
|
181
|
+
// Interpreter not found → try the next candidate before giving up.
|
|
182
|
+
if (err.code === "ENOENT" && idx + 1 < candidates.length) {
|
|
183
|
+
advanced = true;
|
|
184
|
+
trySpawn(idx + 1);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
logEvent(`${name}: script spawn error: ${err.message}`);
|
|
188
|
+
finish("", null);
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
trySpawn(0);
|
|
157
192
|
}
|
|
158
193
|
export {
|
|
159
194
|
applyParser,
|
|
@@ -35,7 +35,14 @@ const {
|
|
|
35
35
|
// net.createServer().listen() accepts both transparently.
|
|
36
36
|
const PIPE_PATH = moduleRequire('../../../lib/hook-pipe-path.cjs')()
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// Honor MIXDOG_RUNTIME_ROOT consistently with runtime-paths.mjs (the consumer
|
|
39
|
+
// of these tool-exec signals): when the override is set, the signal PRODUCER
|
|
40
|
+
// here must write into the same root the channels worker watches, or signals
|
|
41
|
+
// are silently dropped. Default stays tmpdir()/mixdog so non-override installs
|
|
42
|
+
// are unchanged.
|
|
43
|
+
const RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT
|
|
44
|
+
? pathResolve(process.env.MIXDOG_RUNTIME_ROOT)
|
|
45
|
+
: join(tmpdir(), 'mixdog')
|
|
39
46
|
const SIGNAL_CONSUMER_MARKER = join(RUNTIME_ROOT, '.tool-exec-consumer')
|
|
40
47
|
const SUBAGENT_SIGNAL_CONSUMER_MARKER = join(RUNTIME_ROOT, '.tool-exec-subagent-consumer')
|
|
41
48
|
const SIGNAL_RE_GENERIC = /^tool-exec-\d+-[0-9a-f]+\.signal$/
|
|
@@ -341,13 +341,29 @@ export async function ensureRuntime(dataDir) {
|
|
|
341
341
|
const pkey = platformKey()
|
|
342
342
|
const asset = manifest.assets?.[pkey]
|
|
343
343
|
if (!asset) {
|
|
344
|
+
// Platform/arch absent from the manifest entirely (e.g. win32-arm64).
|
|
345
|
+
// The memory PG runtime cannot start here; fail with a single clear,
|
|
346
|
+
// actionable message. The memory worker's init().catch reports this as
|
|
347
|
+
// degraded and the rest of mixdog (agent, tools) keeps working without
|
|
348
|
+
// memory.
|
|
349
|
+
const supported = Object.keys(manifest.assets || {})
|
|
350
|
+
.filter((k) => isUsableAsset(manifest.assets[k]))
|
|
351
|
+
.join(', ') || '(none)'
|
|
344
352
|
throw new Error(
|
|
345
|
-
`[runtime-fetcher]
|
|
346
|
-
`
|
|
353
|
+
`[runtime-fetcher] memory runtime not available on ${pkey}: ` +
|
|
354
|
+
`no runtime asset for this platform/arch in the manifest. ` +
|
|
355
|
+
`Supported: ${supported}. ` +
|
|
356
|
+
`Memory is disabled on this platform; the rest of mixdog continues to work.`
|
|
347
357
|
)
|
|
348
358
|
}
|
|
349
359
|
if (!isUsableAsset(asset)) {
|
|
350
|
-
|
|
360
|
+
// Platform/arch present but explicitly marked unsupported or carrying a
|
|
361
|
+
// placeholder/TBD payload (e.g. linux-arm64). Same graceful-degrade path.
|
|
362
|
+
throw new Error(
|
|
363
|
+
`[runtime-fetcher] memory runtime not available on ${pkey}: ` +
|
|
364
|
+
`this platform/arch is marked unsupported (no validated runtime asset). ` +
|
|
365
|
+
`Memory is disabled on this platform; the rest of mixdog continues to work.`
|
|
366
|
+
)
|
|
351
367
|
}
|
|
352
368
|
|
|
353
369
|
const { url, sha256, size } = asset
|
|
@@ -6,6 +6,7 @@ import { Agent, fetch as undiciFetch } from 'undici'
|
|
|
6
6
|
import { JSDOM } from 'jsdom'
|
|
7
7
|
import puppeteer from 'puppeteer-core'
|
|
8
8
|
import { Readability } from '@mozilla/readability'
|
|
9
|
+
import { isWSL } from '../../shared/wsl.mjs'
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
const PKG_VERSION = (() => { try { return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version } catch { return '0.0.1' } })()
|
|
@@ -38,13 +39,24 @@ const COMMON_BROWSER_PATHS = (() => {
|
|
|
38
39
|
].filter(Boolean)
|
|
39
40
|
}
|
|
40
41
|
if (platform === 'linux') {
|
|
41
|
-
|
|
42
|
+
// Native-Linux Chromium/Chrome binaries first. The /mnt/c Windows .exe
|
|
43
|
+
// entries are reachable from WSL's filesystem but puppeteer-core CANNOT
|
|
44
|
+
// drive a Windows GUI browser as a Linux child process (CDP over a pipe to
|
|
45
|
+
// a Win32 binary launched from the Linux ABI does not work), so advertising
|
|
46
|
+
// puppeteer-available off a Windows .exe yields launch failures at runtime.
|
|
47
|
+
// Only offer the Windows .exe fallbacks on plain Linux (Wine/dual-mount
|
|
48
|
+
// edge cases), never under WSL.
|
|
49
|
+
const linuxNative = [
|
|
42
50
|
'/usr/bin/google-chrome',
|
|
43
51
|
'/usr/bin/google-chrome-stable',
|
|
44
52
|
'/usr/bin/chromium',
|
|
45
53
|
'/usr/bin/chromium-browser',
|
|
46
54
|
'/snap/bin/chromium',
|
|
47
55
|
'/usr/bin/microsoft-edge',
|
|
56
|
+
]
|
|
57
|
+
if (isWSL()) return linuxNative
|
|
58
|
+
return [
|
|
59
|
+
...linuxNative,
|
|
48
60
|
'/mnt/c/Program Files/Google/Chrome/Application/chrome.exe',
|
|
49
61
|
'/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe',
|
|
50
62
|
'/mnt/c/Program Files/Microsoft/Edge/Application/msedge.exe',
|
package/src/shared/config.mjs
CHANGED
|
@@ -334,7 +334,26 @@ export function getAgentApiKey(provider) {
|
|
|
334
334
|
* Never writes to mixdog-config.json.
|
|
335
335
|
*/
|
|
336
336
|
export function saveSecret(account, value) {
|
|
337
|
-
|
|
337
|
+
try {
|
|
338
|
+
_setSecret(account, value)
|
|
339
|
+
} catch (err) {
|
|
340
|
+
// On WSL/headless Linux (and any host missing a usable keychain backend,
|
|
341
|
+
// e.g. keytar/libsecret not installed or no running Secret Service) the OS
|
|
342
|
+
// write throws a cryptic backend error. Surface an actionable message that
|
|
343
|
+
// points at the env-var read path the getters already honor, instead of
|
|
344
|
+
// silently writing the plaintext secret to mixdog-config.json.
|
|
345
|
+
const envKey = _envKey(account)
|
|
346
|
+
const agentMatch = String(account || '').match(/^agent\.([^.]+)\.apiKey$/)
|
|
347
|
+
const stdEnv = agentMatch ? AGENT_PROVIDER_ENV[agentMatch[1]] : null
|
|
348
|
+
const envHint = stdEnv ? `${stdEnv} (or ${envKey})` : envKey
|
|
349
|
+
const e = new Error(
|
|
350
|
+
`[config] could not save secret to the OS keychain: ${err && err.message ? err.message : err}\n` +
|
|
351
|
+
` No usable keychain backend on this host (common on WSL / headless Linux without libsecret).\n` +
|
|
352
|
+
` Set the ${envHint} environment variable instead — the runtime reads it directly.`
|
|
353
|
+
)
|
|
354
|
+
e.cause = err
|
|
355
|
+
throw e
|
|
356
|
+
}
|
|
338
357
|
}
|
|
339
358
|
|
|
340
359
|
export function deleteSecret(account) {
|
package/src/shared/open-url.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
+
import { isWSL } from './wsl.mjs';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Open a URL in the user's default browser. Best-effort and non-blocking —
|
|
@@ -11,27 +12,51 @@ import { spawn } from 'child_process';
|
|
|
11
12
|
* treats the first quoted string as the WINDOW TITLE, so the URL is dropped
|
|
12
13
|
* and nothing opens.
|
|
13
14
|
* - macOS: `open <url>`.
|
|
15
|
+
* - WSL: xdg-open usually fails (no Linux GUI), so reach the Windows-HOST
|
|
16
|
+
* browser — prefer `wslview` (wslu), else `powershell.exe Start-Process`.
|
|
17
|
+
* cmd.exe `start` is deliberately NOT used: cmd re-parses URL metacharacters
|
|
18
|
+
* so a query-string `&` (e.g. `?a=1&b=2`) is treated as a command separator
|
|
19
|
+
* and the URL truncates. PowerShell's Start-Process takes the URL as one
|
|
20
|
+
* argument and does not reparse `&`; the URL is passed inside a single-quoted
|
|
21
|
+
* PowerShell literal (single quotes doubled) so `&`, spaces and quotes
|
|
22
|
+
* survive and no injection is possible. Falls back through the chain when an
|
|
23
|
+
* opener is missing.
|
|
14
24
|
* - Linux/other: `xdg-open <url>`.
|
|
15
25
|
*/
|
|
16
26
|
export function openInBrowser(url) {
|
|
17
27
|
const u = String(url);
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
// Ordered [cmd, args] candidates. Every platform but WSL has exactly one;
|
|
29
|
+
// WSL has a fallback chain since the first opener may be absent.
|
|
30
|
+
let candidates;
|
|
20
31
|
if (process.platform === 'win32') {
|
|
21
|
-
|
|
22
|
-
args = ['url.dll,FileProtocolHandler', u];
|
|
32
|
+
candidates = [['rundll32', ['url.dll,FileProtocolHandler', u]]];
|
|
23
33
|
} else if (process.platform === 'darwin') {
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
candidates = [['open', [u]]];
|
|
35
|
+
} else if (isWSL()) {
|
|
36
|
+
// Single-quoted PowerShell literal: double any embedded single quote.
|
|
37
|
+
// Everything else (incl. `&`) is literal inside a single-quoted string.
|
|
38
|
+
const psUrl = `'${u.replace(/'/g, "''")}'`;
|
|
39
|
+
candidates = [
|
|
40
|
+
['wslview', [u]],
|
|
41
|
+
['powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `Start-Process -FilePath ${psUrl}`]],
|
|
42
|
+
];
|
|
26
43
|
} else {
|
|
27
|
-
|
|
28
|
-
args = [u];
|
|
44
|
+
candidates = [['xdg-open', [u]]];
|
|
29
45
|
}
|
|
46
|
+
tryOpenCandidates(candidates, 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Spawn candidates[index]; on spawn error (opener missing) advance to the next.
|
|
50
|
+
// Always non-blocking — the caller prints the URL, so exhausting the chain is
|
|
51
|
+
// never fatal.
|
|
52
|
+
function tryOpenCandidates(candidates, index) {
|
|
53
|
+
if (index >= candidates.length) return;
|
|
54
|
+
const [cmd, args] = candidates[index];
|
|
30
55
|
try {
|
|
31
56
|
const child = spawn(cmd, args, { stdio: 'ignore', detached: true, windowsHide: true });
|
|
32
|
-
child.on('error', () => {
|
|
57
|
+
child.on('error', () => { tryOpenCandidates(candidates, index + 1); });
|
|
33
58
|
child.unref();
|
|
34
59
|
} catch {
|
|
35
|
-
|
|
60
|
+
tryOpenCandidates(candidates, index + 1);
|
|
36
61
|
}
|
|
37
62
|
}
|
package/src/shared/user-cwd.mjs
CHANGED
|
@@ -54,8 +54,20 @@ export function _safeProcessCwd() {
|
|
|
54
54
|
function _normalizePlatformCwd(p) {
|
|
55
55
|
if (!p || typeof p !== 'string') return p
|
|
56
56
|
if (process.platform !== 'win32') return resolve(p)
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
// Map all three POSIX drive-prefix shapes to native Windows paths before
|
|
58
|
+
// resolution, aligned with path-utils.mjs posixPathToWindowsPath:
|
|
59
|
+
// /cygdrive/c/... (Cygwin), /mnt/c/... (WSL), /c/... (MSYS/Git-Bash) → C:\...
|
|
60
|
+
// Cygwin/WSL prefixes must be tested before the bare single-letter form
|
|
61
|
+
// because their slice offsets differ.
|
|
62
|
+
const cyg = p.match(/^\/cygdrive\/([a-zA-Z])\//)
|
|
63
|
+
const wsl = p.match(/^\/mnt\/([a-zA-Z])\//)
|
|
64
|
+
let native = p
|
|
65
|
+
if (cyg) native = `${cyg[1].toUpperCase()}:\\${p.slice(11).replace(/\//g, '\\')}`
|
|
66
|
+
else if (wsl) native = `${wsl[1].toUpperCase()}:\\${p.slice(7).replace(/\//g, '\\')}`
|
|
67
|
+
else {
|
|
68
|
+
const m = p.match(/^[\/\\]([a-zA-Z])[\/\\](.*)$/)
|
|
69
|
+
if (m) native = `${m[1].toUpperCase()}:\\${m[2].replace(/\//g, '\\')}`
|
|
70
|
+
}
|
|
59
71
|
return resolve(native)
|
|
60
72
|
}
|
|
61
73
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// WSL (Windows Subsystem for Linux) detection.
|
|
2
|
+
//
|
|
3
|
+
// process.platform reports 'linux' inside WSL, so any caller that needs
|
|
4
|
+
// Windows-HOST behavior — opening the Windows default browser, reaching the
|
|
5
|
+
// Windows credential store, translating Windows drive paths — must branch on
|
|
6
|
+
// this explicitly rather than on process.platform alone. Plain-Linux behavior
|
|
7
|
+
// stays the default; this only flags the WSL special case.
|
|
8
|
+
//
|
|
9
|
+
// Detection order (cheapest / most reliable first):
|
|
10
|
+
// 1. WSL_DISTRO_NAME / WSL_INTEROP env vars (set by the WSL init for every
|
|
11
|
+
// interactive and most non-interactive sessions).
|
|
12
|
+
// 2. /proc/version containing "microsoft"/"wsl" (the kernel is a Microsoft
|
|
13
|
+
// build under WSL1/WSL2). Read once and memoized.
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
|
|
16
|
+
let _isWSL;
|
|
17
|
+
|
|
18
|
+
export function isWSL() {
|
|
19
|
+
if (_isWSL !== undefined) return _isWSL;
|
|
20
|
+
if (process.platform !== 'linux') {
|
|
21
|
+
_isWSL = false;
|
|
22
|
+
return _isWSL;
|
|
23
|
+
}
|
|
24
|
+
if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) {
|
|
25
|
+
_isWSL = true;
|
|
26
|
+
return _isWSL;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
const v = readFileSync('/proc/version', 'utf8').toLowerCase();
|
|
30
|
+
_isWSL = v.includes('microsoft') || v.includes('wsl');
|
|
31
|
+
} catch {
|
|
32
|
+
_isWSL = false;
|
|
33
|
+
}
|
|
34
|
+
return _isWSL;
|
|
35
|
+
}
|