maqcli 0.6.2 → 0.6.4

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.
@@ -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.6.2" });
356
+ const daemon = createDaemon({ token: authKey, version: "0.6.4" });
357
357
  try {
358
358
  const { host, port } = await daemon.listen();
359
359
  const url = `http://${host}:${port}/`;
@@ -24,6 +24,21 @@
24
24
  * instead of waiting on it. By the time npm actually touches the shim files,
25
25
  * the process that used to hold them has already terminated. This is the same
26
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).
27
42
  */
28
43
  import { spawn } from "node:child_process";
29
44
  export interface VersionInfo {
@@ -38,21 +53,23 @@ export declare function isNewer(current: string, latest: string): boolean;
38
53
  export declare function fetchLatestVersion(registryUrl?: string, timeoutMs?: number): Promise<string>;
39
54
  /** Compare the running version against the registry; never throws. */
40
55
  export declare function checkForUpdate(currentVersion: string): Promise<VersionInfo>;
41
- export interface UpdateOutcome {
42
- ok: boolean;
43
- message: string;
44
- version?: VersionInfo;
45
- /** Set when the update was launched detached; the caller should exit(0) promptly instead of doing more work. */
46
- detachedPid?: number;
47
- /** Path a --wait caller can poll to see the detached install's result. */
48
- logPath?: string;
49
- }
56
+ /**
57
+ * Run an npm subcommand safely and portably. See the module doc's second
58
+ * section for why this can't just be `execSafe("npm", args)` on Windows.
59
+ */
60
+ export declare function runNpm(args: string[], opts?: {
61
+ timeoutMs?: number;
62
+ }): Promise<{
63
+ code: number | null;
64
+ stdout: string;
65
+ stderr: string;
66
+ }>;
50
67
  /**
51
68
  * Build the OS-appropriate command to run `npm install -g maqcli@<version>`
52
69
  * fully detached from the current process (see module doc for why). On
53
- * Windows this wraps npm in a new cmd.exe so the child truly detaches from
54
- * the parent's console/job object; on POSIX, npm itself is spawned directly
55
- * with `detached: true`.
70
+ * Windows this wraps npm in a new cmd.exe (which is also how the shim gets
71
+ * resolved at all — see runNpm above); on POSIX, a `sh -c` one-liner gives us
72
+ * the same redirection-to-a-log-file behavior.
56
73
  */
57
74
  export declare function detachedInstallCommand(version: string, logPath: string, platform?: NodeJS.Platform): {
58
75
  cmd: string;
@@ -69,7 +86,7 @@ export interface UpdateOutcome {
69
86
  ok: boolean;
70
87
  message: string;
71
88
  version?: VersionInfo;
72
- /** Set when the update was launched detached; the caller should exit(0) promptly instead of doing more work. */
89
+ /** Set when the update was launched detached (the default path). */
73
90
  detachedPid?: number;
74
91
  /** Path a caller can inspect afterward to see the detached install's own npm output. */
75
92
  logPath?: string;
@@ -86,8 +103,8 @@ export interface UpdateOutcome {
86
103
  * alive, still holding the lock, for the whole duration).
87
104
  *
88
105
  * `opts.wait: true` (used by tests, and by --wait for a CI/non-interactive
89
- * caller that can tolerate the lock risk) runs synchronously via `runner`
90
- * instead, so the outcome is available in the same process.
106
+ * caller that can tolerate the lock risk) runs synchronously instead, so the
107
+ * outcome is available in the same process.
91
108
  */
92
109
  export declare function performUpdate(currentVersion: string, opts?: {
93
110
  checkOnly?: boolean;
@@ -24,6 +24,21 @@
24
24
  * instead of waiting on it. By the time npm actually touches the shim files,
25
25
  * the process that used to hold them has already terminated. This is the same
26
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).
27
42
  */
28
43
  import { spawn } from "node:child_process";
29
44
  import { tmpdir } from "node:os";
@@ -66,17 +81,30 @@ export async function checkForUpdate(currentVersion) {
66
81
  return { current: currentVersion, latest: null, upToDate: true, error: e.message };
67
82
  }
68
83
  }
84
+ /**
85
+ * Run an npm subcommand safely and portably. See the module doc's second
86
+ * section for why this can't just be `execSafe("npm", args)` on Windows.
87
+ */
88
+ export async function runNpm(args, opts = {}) {
89
+ if (process.platform === "win32") {
90
+ // Every element here is our own literal string (package name, flags) —
91
+ // never unsanitized user input — so building one cmd.exe command line is
92
+ // safe. Defensively quote anything with whitespace anyway.
93
+ const cmdLine = ["npm", ...args].map((a) => (/\s/.test(a) ? `"${a}"` : a)).join(" ");
94
+ return execSafe("cmd.exe", ["/c", cmdLine], opts);
95
+ }
96
+ return execSafe("npm", args, opts);
97
+ }
69
98
  /**
70
99
  * Build the OS-appropriate command to run `npm install -g maqcli@<version>`
71
100
  * fully detached from the current process (see module doc for why). On
72
- * Windows this wraps npm in a new cmd.exe so the child truly detaches from
73
- * the parent's console/job object; on POSIX, npm itself is spawned directly
74
- * with `detached: true`.
101
+ * Windows this wraps npm in a new cmd.exe (which is also how the shim gets
102
+ * resolved at all — see runNpm above); on POSIX, a `sh -c` one-liner gives us
103
+ * the same redirection-to-a-log-file behavior.
75
104
  */
76
105
  export function detachedInstallCommand(version, logPath, platform = process.platform) {
77
106
  const pkg = `maqcli@${version}`;
78
107
  if (platform === "win32") {
79
- // /c runs and closes; redirection captures npm's own output for later inspection.
80
108
  return { cmd: "cmd.exe", args: ["/c", `npm install -g ${pkg} > "${logPath}" 2>&1`] };
81
109
  }
82
110
  return { cmd: "sh", args: ["-c", `npm install -g ${pkg} > '${logPath}' 2>&1`] };
@@ -108,8 +136,8 @@ export function launchDetachedUpdate(version, logPath, spawner = spawn) {
108
136
  * alive, still holding the lock, for the whole duration).
109
137
  *
110
138
  * `opts.wait: true` (used by tests, and by --wait for a CI/non-interactive
111
- * caller that can tolerate the lock risk) runs synchronously via `runner`
112
- * instead, so the outcome is available in the same process.
139
+ * caller that can tolerate the lock risk) runs synchronously instead, so the
140
+ * outcome is available in the same process.
113
141
  */
114
142
  export async function performUpdate(currentVersion, opts = {}) {
115
143
  const version = await checkForUpdate(currentVersion);
@@ -134,11 +162,12 @@ export async function performUpdate(currentVersion, opts = {}) {
134
162
  logPath,
135
163
  };
136
164
  }
137
- // --wait / test path: run synchronously in this process. On Windows this
138
- // can hit the file-lock failure mode described above if `maq` itself is
139
- // the thing being replaced; it's kept for CI and for tests with a stubbed
140
- // runner, not as the default interactive experience.
141
- const runner = opts.runner ?? (async (cmd, args) => execSafe(cmd, args, { timeoutMs: 120000 }));
165
+ // --wait / test path: run synchronously in this process via runNpm (or an
166
+ // injected runner). On Windows this can still hit the file-lock failure
167
+ // mode described above if `maq` itself is the thing being replaced; it's
168
+ // kept for CI and for tests with a stubbed runner, not as the default
169
+ // interactive experience.
170
+ const runner = opts.runner ?? ((_cmd, args) => runNpm(args, { timeoutMs: 120000 }));
142
171
  const outcome = await runner("npm", ["install", "-g", `maqcli@${latest}`]);
143
172
  if (outcome.code !== 0) {
144
173
  return {
@@ -171,7 +200,7 @@ export async function readUpdateLog(logPath) {
171
200
  * "what did we ask for") — this is what confirms a detached update landed,
172
201
  * independent of which process/terminal you're asking from.
173
202
  */
174
- export async function installedGlobalVersion(runner = (cmd, args) => execSafe(cmd, args, { timeoutMs: 15000 })) {
203
+ export async function installedGlobalVersion(runner = (_cmd, args) => runNpm(args, { timeoutMs: 15000 })) {
175
204
  const outcome = await runner("npm", ["list", "-g", "maqcli", "--depth=0", "--json"]);
176
205
  if (outcome.code !== 0)
177
206
  return null;
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.6.2";
49
+ const VERSION = "0.6.4";
50
50
  async function main(argv) {
51
51
  const [command, ...rest] = argv;
52
52
  switch (command) {
@@ -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.6.2";
55
+ const version = opts.version ?? "0.6.4";
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.6.2",
3
+ "version": "0.6.4",
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": {