maqcli 0.6.6 → 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 -76
- package/dist/core/update.js +45 -116
- 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,39 +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
|
-
cleanupPath?: string;
|
|
90
|
-
cleanupContent?: string;
|
|
91
|
-
};
|
|
92
|
-
/**
|
|
93
|
-
* Launch the update fully detached and return immediately — the caller
|
|
94
|
-
* (cmdUpdate) should print guidance and exit right away rather than await
|
|
95
|
-
* anything else, which is what avoids the Windows file-lock failure mode.
|
|
96
|
-
* `spawner` is injectable for tests so nothing is actually spawned there.
|
|
97
|
-
* `writer` is injectable so tests don't touch the real filesystem either.
|
|
98
|
-
*/
|
|
99
|
-
export declare function launchDetachedUpdate(version: string, logPath: string, spawner?: typeof spawn, writer?: (path: string, content: string) => void): number | undefined;
|
|
100
52
|
export interface UpdateOutcome {
|
|
101
53
|
ok: boolean;
|
|
102
54
|
message: string;
|
|
@@ -106,16 +58,20 @@ export interface UpdateOutcome {
|
|
|
106
58
|
/** Path a caller can inspect afterward to see the detached install's own npm output. */
|
|
107
59
|
logPath?: string;
|
|
108
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;
|
|
109
67
|
/**
|
|
110
68
|
* Perform the update: check the registry, and if a newer version exists,
|
|
111
69
|
* launch `npm install -g maqcli@<latest>` DETACHED from this process (see the
|
|
112
|
-
* module doc — this is what avoids the Windows file-lock failure mode
|
|
113
|
-
* 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).
|
|
114
71
|
*
|
|
115
72
|
* The detached path is the default and recommended flow: this function
|
|
116
73
|
* returns as soon as the install is launched; it does not wait for npm to
|
|
117
|
-
* finish, because waiting would defeat the point
|
|
118
|
-
* alive, still holding the lock, for the whole duration).
|
|
74
|
+
* finish, because waiting would defeat the point.
|
|
119
75
|
*
|
|
120
76
|
* `opts.wait: true` (used by tests, and by --wait for a CI/non-interactive
|
|
121
77
|
* caller that can tolerate the lock risk) runs synchronously instead, so the
|
|
@@ -131,14 +87,9 @@ export declare function performUpdate(currentVersion: string, opts?: {
|
|
|
131
87
|
stderr: string;
|
|
132
88
|
}>;
|
|
133
89
|
spawner?: typeof spawn;
|
|
134
|
-
writer?: (path: string, content: string) => void;
|
|
135
90
|
logPath?: string;
|
|
136
91
|
}): Promise<UpdateOutcome>;
|
|
137
|
-
/**
|
|
138
|
-
* Read a detached update's log file (best-effort). Used by `maq update
|
|
139
|
-
* --status` to show what the background install actually did, since the
|
|
140
|
-
* `maq update` invocation that launched it already exited.
|
|
141
|
-
*/
|
|
92
|
+
/** Read a detached update's log file (best-effort). Used by `maq update --status`. */
|
|
142
93
|
export declare function readUpdateLog(logPath: string): Promise<string | null>;
|
|
143
94
|
/**
|
|
144
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,101 +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 cleanupPath = join(tmpdir(), `maq-update-cleanup-${Date.now()}.bat`);
|
|
121
|
-
// The install script does NOT try to delete itself — a .bat can't
|
|
122
|
-
// reliably remove the file cmd.exe is actively interpreting. Instead it
|
|
123
|
-
// launches this second, separate cleanup script (detached, its own
|
|
124
|
-
// process) once npm finishes; THAT script deletes both files after a
|
|
125
|
-
// short delay, by which point neither is open by anything.
|
|
126
|
-
const scriptContent = [
|
|
127
|
-
"@echo off",
|
|
128
|
-
`npm install -g ${pkg} > "${logPath}" 2>&1`,
|
|
129
|
-
`start "" /min "${cleanupPath}"`,
|
|
130
|
-
].join("\r\n");
|
|
131
|
-
const cleanupContent = [
|
|
132
|
-
"@echo off",
|
|
133
|
-
"ping -n 2 127.0.0.1 >nul",
|
|
134
|
-
`del "${scriptPath}"`,
|
|
135
|
-
`del "%~f0"`,
|
|
136
|
-
].join("\r\n");
|
|
137
|
-
return { cmd: "cmd.exe", args: ["/c", "start", "", "/min", scriptPath], scriptPath, scriptContent, cleanupPath, cleanupContent };
|
|
138
|
-
}
|
|
139
|
-
const scriptPath = join(tmpdir(), `maq-update-${Date.now()}.sh`);
|
|
140
|
-
const scriptContent = ["#!/bin/sh", `npm install -g ${pkg} > '${logPath}' 2>&1`, `rm -f "$0"`].join("\n");
|
|
141
|
-
return { cmd: "sh", args: [scriptPath], scriptPath, scriptContent };
|
|
79
|
+
export async function runNpm(args, opts = {}) {
|
|
80
|
+
return execSafe(resolveNpmPath(), args, opts);
|
|
142
81
|
}
|
|
143
82
|
/**
|
|
144
|
-
* Launch
|
|
145
|
-
*
|
|
146
|
-
* 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.
|
|
147
85
|
* `spawner` is injectable for tests so nothing is actually spawned there.
|
|
148
|
-
* `writer` is injectable so tests don't touch the real filesystem either.
|
|
149
86
|
*/
|
|
150
|
-
export function launchDetachedUpdate(version, logPath, spawner = spawn
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
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
|
+
});
|
|
164
98
|
child.on("error", () => {
|
|
165
99
|
/* best-effort; the log file (or its absence) is the source of truth */
|
|
166
100
|
});
|
|
167
101
|
child.unref();
|
|
168
102
|
return child.pid;
|
|
169
103
|
}
|
|
104
|
+
function openLogFd(logPath) {
|
|
105
|
+
return openSync(logPath, "a");
|
|
106
|
+
}
|
|
170
107
|
/**
|
|
171
108
|
* Perform the update: check the registry, and if a newer version exists,
|
|
172
109
|
* launch `npm install -g maqcli@<latest>` DETACHED from this process (see the
|
|
173
|
-
* module doc — this is what avoids the Windows file-lock failure mode
|
|
174
|
-
* 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).
|
|
175
111
|
*
|
|
176
112
|
* The detached path is the default and recommended flow: this function
|
|
177
113
|
* returns as soon as the install is launched; it does not wait for npm to
|
|
178
|
-
* finish, because waiting would defeat the point
|
|
179
|
-
* alive, still holding the lock, for the whole duration).
|
|
114
|
+
* finish, because waiting would defeat the point.
|
|
180
115
|
*
|
|
181
116
|
* `opts.wait: true` (used by tests, and by --wait for a CI/non-interactive
|
|
182
117
|
* caller that can tolerate the lock risk) runs synchronously instead, so the
|
|
@@ -196,9 +131,7 @@ export async function performUpdate(currentVersion, opts = {}) {
|
|
|
196
131
|
const latest = version.latest;
|
|
197
132
|
if (!opts.wait) {
|
|
198
133
|
const logPath = opts.logPath ?? defaultLogPath();
|
|
199
|
-
const pid = opts.
|
|
200
|
-
? launchDetachedUpdate(latest, logPath, opts.spawner, opts.writer)
|
|
201
|
-
: launchDetachedUpdate(latest, logPath, opts.spawner);
|
|
134
|
+
const pid = launchDetachedUpdate(latest, logPath, opts.spawner);
|
|
202
135
|
return {
|
|
203
136
|
ok: true,
|
|
204
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.`,
|
|
@@ -226,11 +159,7 @@ export async function performUpdate(currentVersion, opts = {}) {
|
|
|
226
159
|
function defaultLogPath() {
|
|
227
160
|
return join(tmpdir(), `maq-update-${Date.now()}.log`);
|
|
228
161
|
}
|
|
229
|
-
/**
|
|
230
|
-
* Read a detached update's log file (best-effort). Used by `maq update
|
|
231
|
-
* --status` to show what the background install actually did, since the
|
|
232
|
-
* `maq update` invocation that launched it already exited.
|
|
233
|
-
*/
|
|
162
|
+
/** Read a detached update's log file (best-effort). Used by `maq update --status`. */
|
|
234
163
|
export async function readUpdateLog(logPath) {
|
|
235
164
|
try {
|
|
236
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": {
|