maqcli 0.6.5 → 0.7.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/dist/core/exec.d.ts +40 -0
- package/dist/core/exec.js +47 -2
- package/dist/core/launcher.js +1 -1
- package/dist/core/update.d.ts +27 -74
- package/dist/core/update.js +45 -98
- package/dist/index.js +1 -1
- package/dist/server/daemon.js +1 -1
- package/package.json +1 -1
package/dist/core/exec.d.ts
CHANGED
|
@@ -4,7 +4,47 @@
|
|
|
4
4
|
* Uses spawn with an explicit argument array and shell:false so user/model
|
|
5
5
|
* provided values can never be interpreted by a shell. This is the single
|
|
6
6
|
* chokepoint for running worker CLIs and raw commands.
|
|
7
|
+
*
|
|
8
|
+
* WINDOWS BATCH-SHIM HANDLING (the general fix, applied once here)
|
|
9
|
+
* ------------------------------------------------------------------
|
|
10
|
+
* Node's `spawn(cmd, args, {shell:false})` can only directly execute native
|
|
11
|
+
* executables. On Windows, a great many "binaries" resolve to `.cmd`/`.bat`
|
|
12
|
+
* (npm's own global-install shims — `codex.cmd`, `gemini.cmd`, `npm.cmd`, …)
|
|
13
|
+
* or `.ps1` files, which are NOT directly executable — spawning them without
|
|
14
|
+
* a command interpreter throws EINVAL/ENOENT/UNKNOWN regardless of the target
|
|
15
|
+
* name. This bit both the worker-CLI dispatch path (execute.ts → CliProvider)
|
|
16
|
+
* and the self-updater before this fix landed.
|
|
17
|
+
*
|
|
18
|
+
* The fix (the same approach the `cross-spawn` ecosystem package uses):
|
|
19
|
+
* detect a `.cmd`/`.bat`/`.ps1` target and transparently re-target the spawn
|
|
20
|
+
* to `cmd.exe /c <target> <args...>`, passing the target and each argument as
|
|
21
|
+
* SEPARATE array elements — never a hand-assembled command-line string. This
|
|
22
|
+
* still goes through `child_process.spawn` with `shell:false` at the Node
|
|
23
|
+
* API level; cmd.exe is the one fixed, trusted executable being spawned, and
|
|
24
|
+
* Node's own per-argument Windows escaping (which correctly handles spaces,
|
|
25
|
+
* quotes, and metacharacters within a single argument) does the rest. Every
|
|
26
|
+
* caller of execSafe/execStream gets this for free; nothing above this layer
|
|
27
|
+
* needs to know.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the effective spawn target for Windows batch/PowerShell shims.
|
|
31
|
+
* Returns the original {cmd, args} unchanged on POSIX or for native
|
|
32
|
+
* executables; re-targets through cmd.exe otherwise.
|
|
33
|
+
*
|
|
34
|
+
* Critically, the shim's arguments are passed as SEPARATE array elements to
|
|
35
|
+
* cmd.exe (`/c`, "<shim>", ...args), not hand-assembled into one big quoted
|
|
36
|
+
* string. Node's own spawn implementation already knows how to correctly
|
|
37
|
+
* quote each array element for a Windows command line (including embedded
|
|
38
|
+
* spaces/quotes); building our own single-string command line and adding
|
|
39
|
+
* `/d /s` on top double-quotes it and corrupts paths with spaces (verified:
|
|
40
|
+
* it mangled "C:\Program Files\nodejs\npm.CMD" into an unrecognized string).
|
|
41
|
+
* Letting spawn's own per-argument escaping do the work, one level of
|
|
42
|
+
* cmd.exe indirection, is both simpler and correct.
|
|
7
43
|
*/
|
|
44
|
+
export declare function resolveSpawnTarget(cmd: string, args: string[]): {
|
|
45
|
+
cmd: string;
|
|
46
|
+
args: string[];
|
|
47
|
+
};
|
|
8
48
|
export interface ExecOptions {
|
|
9
49
|
cwd?: string;
|
|
10
50
|
/** Milliseconds before the child is killed. */
|
package/dist/core/exec.js
CHANGED
|
@@ -4,9 +4,52 @@
|
|
|
4
4
|
* Uses spawn with an explicit argument array and shell:false so user/model
|
|
5
5
|
* provided values can never be interpreted by a shell. This is the single
|
|
6
6
|
* chokepoint for running worker CLIs and raw commands.
|
|
7
|
+
*
|
|
8
|
+
* WINDOWS BATCH-SHIM HANDLING (the general fix, applied once here)
|
|
9
|
+
* ------------------------------------------------------------------
|
|
10
|
+
* Node's `spawn(cmd, args, {shell:false})` can only directly execute native
|
|
11
|
+
* executables. On Windows, a great many "binaries" resolve to `.cmd`/`.bat`
|
|
12
|
+
* (npm's own global-install shims — `codex.cmd`, `gemini.cmd`, `npm.cmd`, …)
|
|
13
|
+
* or `.ps1` files, which are NOT directly executable — spawning them without
|
|
14
|
+
* a command interpreter throws EINVAL/ENOENT/UNKNOWN regardless of the target
|
|
15
|
+
* name. This bit both the worker-CLI dispatch path (execute.ts → CliProvider)
|
|
16
|
+
* and the self-updater before this fix landed.
|
|
17
|
+
*
|
|
18
|
+
* The fix (the same approach the `cross-spawn` ecosystem package uses):
|
|
19
|
+
* detect a `.cmd`/`.bat`/`.ps1` target and transparently re-target the spawn
|
|
20
|
+
* to `cmd.exe /c <target> <args...>`, passing the target and each argument as
|
|
21
|
+
* SEPARATE array elements — never a hand-assembled command-line string. This
|
|
22
|
+
* still goes through `child_process.spawn` with `shell:false` at the Node
|
|
23
|
+
* API level; cmd.exe is the one fixed, trusted executable being spawned, and
|
|
24
|
+
* Node's own per-argument Windows escaping (which correctly handles spaces,
|
|
25
|
+
* quotes, and metacharacters within a single argument) does the rest. Every
|
|
26
|
+
* caller of execSafe/execStream gets this for free; nothing above this layer
|
|
27
|
+
* needs to know.
|
|
7
28
|
*/
|
|
8
29
|
import { spawn } from "node:child_process";
|
|
9
30
|
import { scrubEnv } from "./security.js";
|
|
31
|
+
const WINDOWS_SHIM_EXT = /\.(cmd|bat|ps1)$/i;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the effective spawn target for Windows batch/PowerShell shims.
|
|
34
|
+
* Returns the original {cmd, args} unchanged on POSIX or for native
|
|
35
|
+
* executables; re-targets through cmd.exe otherwise.
|
|
36
|
+
*
|
|
37
|
+
* Critically, the shim's arguments are passed as SEPARATE array elements to
|
|
38
|
+
* cmd.exe (`/c`, "<shim>", ...args), not hand-assembled into one big quoted
|
|
39
|
+
* string. Node's own spawn implementation already knows how to correctly
|
|
40
|
+
* quote each array element for a Windows command line (including embedded
|
|
41
|
+
* spaces/quotes); building our own single-string command line and adding
|
|
42
|
+
* `/d /s` on top double-quotes it and corrupts paths with spaces (verified:
|
|
43
|
+
* it mangled "C:\Program Files\nodejs\npm.CMD" into an unrecognized string).
|
|
44
|
+
* Letting spawn's own per-argument escaping do the work, one level of
|
|
45
|
+
* cmd.exe indirection, is both simpler and correct.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveSpawnTarget(cmd, args) {
|
|
48
|
+
if (process.platform !== "win32" || !WINDOWS_SHIM_EXT.test(cmd)) {
|
|
49
|
+
return { cmd, args };
|
|
50
|
+
}
|
|
51
|
+
return { cmd: "cmd.exe", args: ["/c", cmd, ...args] };
|
|
52
|
+
}
|
|
10
53
|
/**
|
|
11
54
|
* Run a command safely. `cmd` is the binary, `args` are passed verbatim.
|
|
12
55
|
* Never pass a full command string here — that is what enables injection.
|
|
@@ -16,7 +59,8 @@ export function execSafe(cmd, args = [], opts = {}) {
|
|
|
16
59
|
return new Promise((resolve) => {
|
|
17
60
|
const baseEnv = opts.env ?? process.env;
|
|
18
61
|
const env = opts.scrubSecrets === false ? baseEnv : scrubEnv(baseEnv, opts.secretAllowlist);
|
|
19
|
-
const
|
|
62
|
+
const target = resolveSpawnTarget(cmd, args);
|
|
63
|
+
const child = spawn(target.cmd, target.args, {
|
|
20
64
|
cwd: opts.cwd ?? process.cwd(),
|
|
21
65
|
env,
|
|
22
66
|
shell: false,
|
|
@@ -83,7 +127,8 @@ export function execStream(cmd, args = [], opts = {}) {
|
|
|
83
127
|
return new Promise((resolve) => {
|
|
84
128
|
const baseEnv = opts.env ?? process.env;
|
|
85
129
|
const env = opts.scrubSecrets === false ? baseEnv : scrubEnv(baseEnv, opts.secretAllowlist);
|
|
86
|
-
const
|
|
130
|
+
const target = resolveSpawnTarget(cmd, args);
|
|
131
|
+
const child = spawn(target.cmd, target.args, {
|
|
87
132
|
cwd: opts.cwd ?? process.cwd(),
|
|
88
133
|
env,
|
|
89
134
|
shell: false,
|
package/dist/core/launcher.js
CHANGED
|
@@ -353,7 +353,7 @@ async function connectMobile(rl) {
|
|
|
353
353
|
async function launchUi(authKey) {
|
|
354
354
|
// Reuse the daemon; open its landing page. Import lazily to avoid a cycle.
|
|
355
355
|
const { createDaemon } = await import("../server/daemon.js");
|
|
356
|
-
const daemon = createDaemon({ token: authKey, version: "0.
|
|
356
|
+
const daemon = createDaemon({ token: authKey, version: "0.7.0" });
|
|
357
357
|
try {
|
|
358
358
|
const { host, port } = await daemon.listen();
|
|
359
359
|
const url = `http://${host}:${port}/`;
|
package/dist/core/update.d.ts
CHANGED
|
@@ -8,37 +8,21 @@
|
|
|
8
8
|
* default-deny egress posture (core/security.ts) — it only ever talks to the
|
|
9
9
|
* npm registry, and only when the user explicitly runs this command.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* instead of
|
|
25
|
-
*
|
|
26
|
-
* pattern self-updating CLIs (rustup, deno, bun) use on Windows.
|
|
27
|
-
*
|
|
28
|
-
* THE SEPARATE "CAN'T EVEN LAUNCH NPM" PROBLEM AND ITS FIX
|
|
29
|
-
* ------------------------------------------------------------
|
|
30
|
-
* `child_process.spawn(cmd, args, {shell:false})` — which execSafe always
|
|
31
|
-
* uses, deliberately, to avoid shell-injection — cannot execute `npm`
|
|
32
|
-
* directly on Windows AT ALL, regardless of which name you pass. The `npm` on
|
|
33
|
-
* PATH there is `npm.cmd`, a batch-file shim, and Windows can only run batch
|
|
34
|
-
* files through a command interpreter; spawning it as a bare child process
|
|
35
|
-
* throws ENOENT (name not found as-is) or EINVAL (found but not directly
|
|
36
|
-
* executable), never succeeds. The fix is `runNpm()` below: on Windows it
|
|
37
|
-
* routes through `cmd.exe /c "npm ..."` (still shell:false at the
|
|
38
|
-
* child_process level — cmd.exe is the fixed, trusted executable being
|
|
39
|
-
* spawned; the args passed to IT are our own literal strings, never
|
|
40
|
-
* unsanitized input, so this does not reopen the shell-injection risk
|
|
41
|
-
* execSafe exists to close).
|
|
11
|
+
* TWO WINDOWS PROBLEMS, BOTH FIXED AT THE ROOT NOW
|
|
12
|
+
* ---------------------------------------------------
|
|
13
|
+
* 1. "spawn npm ENOENT/EINVAL" — `npm` on PATH is `npm.cmd`, a batch shim;
|
|
14
|
+
* `child_process.spawn(shell:false)` cannot execute batch files directly.
|
|
15
|
+
* Fixed generically in exec.ts's `resolveSpawnTarget()` — it detects
|
|
16
|
+
* .cmd/.bat/.ps1 targets and transparently re-targets through
|
|
17
|
+
* `cmd.exe /d /s /c`, as an argv array (never a hand-built shell string).
|
|
18
|
+
* Every execSafe/execStream caller gets this for free; this module no
|
|
19
|
+
* longer needs its own cmd.exe-wrapping or quoting logic at all.
|
|
20
|
+
* 2. The file-lock problem — `npm install -g` has to overwrite the running
|
|
21
|
+
* `maq` binary's own files. If that install runs as a CHILD that `maq`
|
|
22
|
+
* waits on, Windows can hold locks on those files for the wait's entire
|
|
23
|
+
* duration. Fixed by spawning npm with `detached: true` + `.unref()` and
|
|
24
|
+
* returning immediately instead of awaiting it — nothing above this layer
|
|
25
|
+
* holds a lock on anything by the time npm actually touches the files.
|
|
42
26
|
*/
|
|
43
27
|
import { spawn } from "node:child_process";
|
|
44
28
|
export interface VersionInfo {
|
|
@@ -54,8 +38,9 @@ export declare function fetchLatestVersion(registryUrl?: string, timeoutMs?: num
|
|
|
54
38
|
/** Compare the running version against the registry; never throws. */
|
|
55
39
|
export declare function checkForUpdate(currentVersion: string): Promise<VersionInfo>;
|
|
56
40
|
/**
|
|
57
|
-
* Run an npm subcommand
|
|
58
|
-
*
|
|
41
|
+
* Run an npm subcommand synchronously and capture its output. Safe on
|
|
42
|
+
* Windows because execSafe's resolveSpawnTarget transparently handles the
|
|
43
|
+
* `.cmd` shim; no manual cmd.exe wrapping needed here anymore.
|
|
59
44
|
*/
|
|
60
45
|
export declare function runNpm(args: string[], opts?: {
|
|
61
46
|
timeoutMs?: number;
|
|
@@ -64,37 +49,6 @@ export declare function runNpm(args: string[], opts?: {
|
|
|
64
49
|
stdout: string;
|
|
65
50
|
stderr: string;
|
|
66
51
|
}>;
|
|
67
|
-
/**
|
|
68
|
-
* Build the OS-appropriate command to run `npm install -g maqcli@<version>`
|
|
69
|
-
* fully detached from the current process (see module doc for why).
|
|
70
|
-
*
|
|
71
|
-
* On Windows, `cmd.exe /c "npm install..."` alone is NOT enough: even with
|
|
72
|
-
* `detached: true` + `.unref()` on the Node side, the child stays tied to the
|
|
73
|
-
* parent's console/job object, and it dies the moment the parent's console
|
|
74
|
-
* closes (a long-standing Node behavior on Windows — nodejs/node#5614). The
|
|
75
|
-
* fix is to have cmd.exe launch the real work via the `start` builtin, which
|
|
76
|
-
* creates a genuinely independent process outside the parent's job object.
|
|
77
|
-
*
|
|
78
|
-
* `start`'s own command-line parsing is notoriously fragile with nested
|
|
79
|
-
* quotes (redirection operators and quoted paths inside a quoted inner
|
|
80
|
-
* command reliably break it). Rather than fight cmd.exe's quoting rules, we
|
|
81
|
-
* write the actual work to a small, disposable .bat/.sh script on disk and
|
|
82
|
-
* have `start`/the shell launch THAT — no nested quoting at all.
|
|
83
|
-
*/
|
|
84
|
-
export declare function detachedInstallCommand(version: string, logPath: string, platform?: NodeJS.Platform): {
|
|
85
|
-
cmd: string;
|
|
86
|
-
args: string[];
|
|
87
|
-
scriptPath: string;
|
|
88
|
-
scriptContent: string;
|
|
89
|
-
};
|
|
90
|
-
/**
|
|
91
|
-
* Launch the update fully detached and return immediately — the caller
|
|
92
|
-
* (cmdUpdate) should print guidance and exit right away rather than await
|
|
93
|
-
* anything else, which is what avoids the Windows file-lock failure mode.
|
|
94
|
-
* `spawner` is injectable for tests so nothing is actually spawned there.
|
|
95
|
-
* `writer` is injectable so tests don't touch the real filesystem either.
|
|
96
|
-
*/
|
|
97
|
-
export declare function launchDetachedUpdate(version: string, logPath: string, spawner?: typeof spawn, writer?: (path: string, content: string) => void): number | undefined;
|
|
98
52
|
export interface UpdateOutcome {
|
|
99
53
|
ok: boolean;
|
|
100
54
|
message: string;
|
|
@@ -104,16 +58,20 @@ export interface UpdateOutcome {
|
|
|
104
58
|
/** Path a caller can inspect afterward to see the detached install's own npm output. */
|
|
105
59
|
logPath?: string;
|
|
106
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Launch `npm install -g maqcli@<version>` fully detached (survives this
|
|
63
|
+
* process exiting) and pipe its output to a log file for later inspection.
|
|
64
|
+
* `spawner` is injectable for tests so nothing is actually spawned there.
|
|
65
|
+
*/
|
|
66
|
+
export declare function launchDetachedUpdate(version: string, logPath: string, spawner?: typeof spawn): number | undefined;
|
|
107
67
|
/**
|
|
108
68
|
* Perform the update: check the registry, and if a newer version exists,
|
|
109
69
|
* launch `npm install -g maqcli@<latest>` DETACHED from this process (see the
|
|
110
|
-
* module doc — this is what avoids the Windows file-lock failure mode
|
|
111
|
-
* npm can't overwrite files the running `maq` process still holds open).
|
|
70
|
+
* module doc — this is what avoids the Windows file-lock failure mode).
|
|
112
71
|
*
|
|
113
72
|
* The detached path is the default and recommended flow: this function
|
|
114
73
|
* returns as soon as the install is launched; it does not wait for npm to
|
|
115
|
-
* finish, because waiting would defeat the point
|
|
116
|
-
* alive, still holding the lock, for the whole duration).
|
|
74
|
+
* finish, because waiting would defeat the point.
|
|
117
75
|
*
|
|
118
76
|
* `opts.wait: true` (used by tests, and by --wait for a CI/non-interactive
|
|
119
77
|
* caller that can tolerate the lock risk) runs synchronously instead, so the
|
|
@@ -129,14 +87,9 @@ export declare function performUpdate(currentVersion: string, opts?: {
|
|
|
129
87
|
stderr: string;
|
|
130
88
|
}>;
|
|
131
89
|
spawner?: typeof spawn;
|
|
132
|
-
writer?: (path: string, content: string) => void;
|
|
133
90
|
logPath?: string;
|
|
134
91
|
}): Promise<UpdateOutcome>;
|
|
135
|
-
/**
|
|
136
|
-
* Read a detached update's log file (best-effort). Used by `maq update
|
|
137
|
-
* --status` to show what the background install actually did, since the
|
|
138
|
-
* `maq update` invocation that launched it already exited.
|
|
139
|
-
*/
|
|
92
|
+
/** Read a detached update's log file (best-effort). Used by `maq update --status`. */
|
|
140
93
|
export declare function readUpdateLog(logPath: string): Promise<string | null>;
|
|
141
94
|
/**
|
|
142
95
|
* Ask npm what is ACTUALLY installed globally right now (ground truth, not
|
package/dist/core/update.js
CHANGED
|
@@ -8,43 +8,28 @@
|
|
|
8
8
|
* default-deny egress posture (core/security.ts) — it only ever talks to the
|
|
9
9
|
* npm registry, and only when the user explicitly runs this command.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* instead of
|
|
25
|
-
*
|
|
26
|
-
* pattern self-updating CLIs (rustup, deno, bun) use on Windows.
|
|
27
|
-
*
|
|
28
|
-
* THE SEPARATE "CAN'T EVEN LAUNCH NPM" PROBLEM AND ITS FIX
|
|
29
|
-
* ------------------------------------------------------------
|
|
30
|
-
* `child_process.spawn(cmd, args, {shell:false})` — which execSafe always
|
|
31
|
-
* uses, deliberately, to avoid shell-injection — cannot execute `npm`
|
|
32
|
-
* directly on Windows AT ALL, regardless of which name you pass. The `npm` on
|
|
33
|
-
* PATH there is `npm.cmd`, a batch-file shim, and Windows can only run batch
|
|
34
|
-
* files through a command interpreter; spawning it as a bare child process
|
|
35
|
-
* throws ENOENT (name not found as-is) or EINVAL (found but not directly
|
|
36
|
-
* executable), never succeeds. The fix is `runNpm()` below: on Windows it
|
|
37
|
-
* routes through `cmd.exe /c "npm ..."` (still shell:false at the
|
|
38
|
-
* child_process level — cmd.exe is the fixed, trusted executable being
|
|
39
|
-
* spawned; the args passed to IT are our own literal strings, never
|
|
40
|
-
* unsanitized input, so this does not reopen the shell-injection risk
|
|
41
|
-
* execSafe exists to close).
|
|
11
|
+
* TWO WINDOWS PROBLEMS, BOTH FIXED AT THE ROOT NOW
|
|
12
|
+
* ---------------------------------------------------
|
|
13
|
+
* 1. "spawn npm ENOENT/EINVAL" — `npm` on PATH is `npm.cmd`, a batch shim;
|
|
14
|
+
* `child_process.spawn(shell:false)` cannot execute batch files directly.
|
|
15
|
+
* Fixed generically in exec.ts's `resolveSpawnTarget()` — it detects
|
|
16
|
+
* .cmd/.bat/.ps1 targets and transparently re-targets through
|
|
17
|
+
* `cmd.exe /d /s /c`, as an argv array (never a hand-built shell string).
|
|
18
|
+
* Every execSafe/execStream caller gets this for free; this module no
|
|
19
|
+
* longer needs its own cmd.exe-wrapping or quoting logic at all.
|
|
20
|
+
* 2. The file-lock problem — `npm install -g` has to overwrite the running
|
|
21
|
+
* `maq` binary's own files. If that install runs as a CHILD that `maq`
|
|
22
|
+
* waits on, Windows can hold locks on those files for the wait's entire
|
|
23
|
+
* duration. Fixed by spawning npm with `detached: true` + `.unref()` and
|
|
24
|
+
* returning immediately instead of awaiting it — nothing above this layer
|
|
25
|
+
* holds a lock on anything by the time npm actually touches the files.
|
|
42
26
|
*/
|
|
43
27
|
import { spawn } from "node:child_process";
|
|
44
28
|
import { tmpdir } from "node:os";
|
|
45
29
|
import { join } from "node:path";
|
|
46
|
-
import {
|
|
30
|
+
import { openSync } from "node:fs";
|
|
47
31
|
import { execSafe } from "./exec.js";
|
|
32
|
+
import { whichBin } from "./registry.js";
|
|
48
33
|
/** Parse "x.y.z" into a comparable tuple; non-numeric parts sort as 0. */
|
|
49
34
|
function parseSemver(v) {
|
|
50
35
|
const parts = v.replace(/^v/, "").split(".").map((p) => parseInt(p, 10));
|
|
@@ -82,83 +67,51 @@ export async function checkForUpdate(currentVersion) {
|
|
|
82
67
|
return { current: currentVersion, latest: null, upToDate: true, error: e.message };
|
|
83
68
|
}
|
|
84
69
|
}
|
|
85
|
-
/**
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
*/
|
|
89
|
-
export async function runNpm(args, opts = {}) {
|
|
90
|
-
if (process.platform === "win32") {
|
|
91
|
-
// Every element here is our own literal string (package name, flags) —
|
|
92
|
-
// never unsanitized user input — so building one cmd.exe command line is
|
|
93
|
-
// safe. Defensively quote anything with whitespace anyway.
|
|
94
|
-
const cmdLine = ["npm", ...args].map((a) => (/\s/.test(a) ? `"${a}"` : a)).join(" ");
|
|
95
|
-
return execSafe("cmd.exe", ["/c", cmdLine], opts);
|
|
96
|
-
}
|
|
97
|
-
return execSafe("npm", args, opts);
|
|
70
|
+
/** Resolve the real npm binary path once; falls back to the bare name (POSIX is fine with that). */
|
|
71
|
+
function resolveNpmPath() {
|
|
72
|
+
return whichBin("npm") ?? "npm";
|
|
98
73
|
}
|
|
99
74
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* On Windows, `cmd.exe /c "npm install..."` alone is NOT enough: even with
|
|
104
|
-
* `detached: true` + `.unref()` on the Node side, the child stays tied to the
|
|
105
|
-
* parent's console/job object, and it dies the moment the parent's console
|
|
106
|
-
* closes (a long-standing Node behavior on Windows — nodejs/node#5614). The
|
|
107
|
-
* fix is to have cmd.exe launch the real work via the `start` builtin, which
|
|
108
|
-
* creates a genuinely independent process outside the parent's job object.
|
|
109
|
-
*
|
|
110
|
-
* `start`'s own command-line parsing is notoriously fragile with nested
|
|
111
|
-
* quotes (redirection operators and quoted paths inside a quoted inner
|
|
112
|
-
* command reliably break it). Rather than fight cmd.exe's quoting rules, we
|
|
113
|
-
* write the actual work to a small, disposable .bat/.sh script on disk and
|
|
114
|
-
* have `start`/the shell launch THAT — no nested quoting at all.
|
|
75
|
+
* Run an npm subcommand synchronously and capture its output. Safe on
|
|
76
|
+
* Windows because execSafe's resolveSpawnTarget transparently handles the
|
|
77
|
+
* `.cmd` shim; no manual cmd.exe wrapping needed here anymore.
|
|
115
78
|
*/
|
|
116
|
-
export function
|
|
117
|
-
|
|
118
|
-
if (platform === "win32") {
|
|
119
|
-
const scriptPath = join(tmpdir(), `maq-update-${Date.now()}.bat`);
|
|
120
|
-
const scriptContent = ["@echo off", `npm install -g ${pkg} > "${logPath}" 2>&1`, `start "" /min cmd.exe /c del "%~f0"`].join("\r\n");
|
|
121
|
-
return { cmd: "cmd.exe", args: ["/c", "start", "", "/min", scriptPath], scriptPath, scriptContent };
|
|
122
|
-
}
|
|
123
|
-
const scriptPath = join(tmpdir(), `maq-update-${Date.now()}.sh`);
|
|
124
|
-
const scriptContent = ["#!/bin/sh", `npm install -g ${pkg} > '${logPath}' 2>&1`, `rm -f "$0"`].join("\n");
|
|
125
|
-
return { cmd: "sh", args: [scriptPath], scriptPath, scriptContent };
|
|
79
|
+
export async function runNpm(args, opts = {}) {
|
|
80
|
+
return execSafe(resolveNpmPath(), args, opts);
|
|
126
81
|
}
|
|
127
82
|
/**
|
|
128
|
-
* Launch
|
|
129
|
-
*
|
|
130
|
-
* anything else, which is what avoids the Windows file-lock failure mode.
|
|
83
|
+
* Launch `npm install -g maqcli@<version>` fully detached (survives this
|
|
84
|
+
* process exiting) and pipe its output to a log file for later inspection.
|
|
131
85
|
* `spawner` is injectable for tests so nothing is actually spawned there.
|
|
132
|
-
* `writer` is injectable so tests don't touch the real filesystem either.
|
|
133
86
|
*/
|
|
134
|
-
export function launchDetachedUpdate(version, logPath, spawner = spawn
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
const child = spawner(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
87
|
+
export function launchDetachedUpdate(version, logPath, spawner = spawn) {
|
|
88
|
+
const npmPath = resolveNpmPath();
|
|
89
|
+
const args = ["install", "-g", `maqcli@${version}`];
|
|
90
|
+
const child = spawner(npmPath, args, {
|
|
91
|
+
detached: true,
|
|
92
|
+
// Redirect the detached child's own stdio straight to a log file instead
|
|
93
|
+
// of inheriting this process's streams — this is what lets it keep
|
|
94
|
+
// running (and lets THIS process exit) independently of the parent.
|
|
95
|
+
stdio: ["ignore", openLogFd(logPath), openLogFd(logPath)],
|
|
96
|
+
windowsHide: true,
|
|
97
|
+
});
|
|
146
98
|
child.on("error", () => {
|
|
147
99
|
/* best-effort; the log file (or its absence) is the source of truth */
|
|
148
100
|
});
|
|
149
101
|
child.unref();
|
|
150
102
|
return child.pid;
|
|
151
103
|
}
|
|
104
|
+
function openLogFd(logPath) {
|
|
105
|
+
return openSync(logPath, "a");
|
|
106
|
+
}
|
|
152
107
|
/**
|
|
153
108
|
* Perform the update: check the registry, and if a newer version exists,
|
|
154
109
|
* launch `npm install -g maqcli@<latest>` DETACHED from this process (see the
|
|
155
|
-
* module doc — this is what avoids the Windows file-lock failure mode
|
|
156
|
-
* npm can't overwrite files the running `maq` process still holds open).
|
|
110
|
+
* module doc — this is what avoids the Windows file-lock failure mode).
|
|
157
111
|
*
|
|
158
112
|
* The detached path is the default and recommended flow: this function
|
|
159
113
|
* returns as soon as the install is launched; it does not wait for npm to
|
|
160
|
-
* finish, because waiting would defeat the point
|
|
161
|
-
* alive, still holding the lock, for the whole duration).
|
|
114
|
+
* finish, because waiting would defeat the point.
|
|
162
115
|
*
|
|
163
116
|
* `opts.wait: true` (used by tests, and by --wait for a CI/non-interactive
|
|
164
117
|
* caller that can tolerate the lock risk) runs synchronously instead, so the
|
|
@@ -178,9 +131,7 @@ export async function performUpdate(currentVersion, opts = {}) {
|
|
|
178
131
|
const latest = version.latest;
|
|
179
132
|
if (!opts.wait) {
|
|
180
133
|
const logPath = opts.logPath ?? defaultLogPath();
|
|
181
|
-
const pid = opts.
|
|
182
|
-
? launchDetachedUpdate(latest, logPath, opts.spawner, opts.writer)
|
|
183
|
-
: launchDetachedUpdate(latest, logPath, opts.spawner);
|
|
134
|
+
const pid = launchDetachedUpdate(latest, logPath, opts.spawner);
|
|
184
135
|
return {
|
|
185
136
|
ok: true,
|
|
186
137
|
message: `update to v${latest} launched in the background (detached, avoids file locks). Open a new terminal in a few seconds to pick it up.`,
|
|
@@ -208,11 +159,7 @@ export async function performUpdate(currentVersion, opts = {}) {
|
|
|
208
159
|
function defaultLogPath() {
|
|
209
160
|
return join(tmpdir(), `maq-update-${Date.now()}.log`);
|
|
210
161
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Read a detached update's log file (best-effort). Used by `maq update
|
|
213
|
-
* --status` to show what the background install actually did, since the
|
|
214
|
-
* `maq update` invocation that launched it already exited.
|
|
215
|
-
*/
|
|
162
|
+
/** Read a detached update's log file (best-effort). Used by `maq update --status`. */
|
|
216
163
|
export async function readUpdateLog(logPath) {
|
|
217
164
|
try {
|
|
218
165
|
const { readFile } = await import("node:fs/promises");
|
package/dist/index.js
CHANGED
|
@@ -46,7 +46,7 @@ import { runOrchestration } from "./core/orchestrator.js";
|
|
|
46
46
|
import { performUpdate, installedGlobalVersion } from "./core/update.js";
|
|
47
47
|
import { checkProtectedPath, scanForInjection, securityLog, securityRules, } from "./core/security.js";
|
|
48
48
|
import { readFileSync, statSync } from "node:fs";
|
|
49
|
-
const VERSION = "0.
|
|
49
|
+
const VERSION = "0.7.0";
|
|
50
50
|
async function main(argv) {
|
|
51
51
|
const [command, ...rest] = argv;
|
|
52
52
|
switch (command) {
|
package/dist/server/daemon.js
CHANGED
|
@@ -52,7 +52,7 @@ export function createDaemon(opts = {}) {
|
|
|
52
52
|
const host = opts.host ?? process.env.MAQ_HOST ?? "127.0.0.1";
|
|
53
53
|
const port = opts.port ?? Number(process.env.MAQ_PORT ?? 7717);
|
|
54
54
|
const token = opts.token ?? process.env.MAQ_TOKEN ?? generateToken();
|
|
55
|
-
const version = opts.version ?? "0.
|
|
55
|
+
const version = opts.version ?? "0.7.0";
|
|
56
56
|
const corsOrigin = opts.corsOrigin ?? process.env.MAQ_CORS_ORIGIN;
|
|
57
57
|
const registry = opts.registry ?? new SessionRegistry();
|
|
58
58
|
const interactive = new InteractiveRegistry();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "maqcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "MAQ master orchestrator — a token-efficient, agent-agnostic supervisor CLI that sits on top of any worker CLI (AI or not) via a Scout -> Plan -> Execute -> Verify pipeline.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|