maqcli 0.6.3 → 0.6.5

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.3" });
356
+ const daemon = createDaemon({ token: authKey, version: "0.6.5" });
357
357
  try {
358
358
  const { host, port } = await daemon.listen();
359
359
  const url = `http://${host}:${port}/`;
@@ -66,22 +66,35 @@ export declare function runNpm(args: string[], opts?: {
66
66
  }>;
67
67
  /**
68
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). On
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.
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.
73
83
  */
74
84
  export declare function detachedInstallCommand(version: string, logPath: string, platform?: NodeJS.Platform): {
75
85
  cmd: string;
76
86
  args: string[];
87
+ scriptPath: string;
88
+ scriptContent: string;
77
89
  };
78
90
  /**
79
91
  * Launch the update fully detached and return immediately — the caller
80
92
  * (cmdUpdate) should print guidance and exit right away rather than await
81
93
  * anything else, which is what avoids the Windows file-lock failure mode.
82
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.
83
96
  */
84
- export declare function launchDetachedUpdate(version: string, logPath: string, spawner?: typeof spawn): number | undefined;
97
+ export declare function launchDetachedUpdate(version: string, logPath: string, spawner?: typeof spawn, writer?: (path: string, content: string) => void): number | undefined;
85
98
  export interface UpdateOutcome {
86
99
  ok: boolean;
87
100
  message: string;
@@ -116,6 +129,7 @@ export declare function performUpdate(currentVersion: string, opts?: {
116
129
  stderr: string;
117
130
  }>;
118
131
  spawner?: typeof spawn;
132
+ writer?: (path: string, content: string) => void;
119
133
  logPath?: string;
120
134
  }): Promise<UpdateOutcome>;
121
135
  /**
@@ -43,6 +43,7 @@
43
43
  import { spawn } from "node:child_process";
44
44
  import { tmpdir } from "node:os";
45
45
  import { join } from "node:path";
46
+ import { writeFileSync, chmodSync } from "node:fs";
46
47
  import { execSafe } from "./exec.js";
47
48
  /** Parse "x.y.z" into a comparable tuple; non-numeric parts sort as 0. */
48
49
  function parseSemver(v) {
@@ -97,26 +98,50 @@ export async function runNpm(args, opts = {}) {
97
98
  }
98
99
  /**
99
100
  * Build the OS-appropriate command to run `npm install -g maqcli@<version>`
100
- * fully detached from the current process (see module doc for why). On
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.
101
+ * fully detached from the current process (see module doc for why).
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.
104
115
  */
105
116
  export function detachedInstallCommand(version, logPath, platform = process.platform) {
106
117
  const pkg = `maqcli@${version}`;
107
118
  if (platform === "win32") {
108
- return { cmd: "cmd.exe", args: ["/c", `npm install -g ${pkg} > "${logPath}" 2>&1`] };
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 };
109
122
  }
110
- return { cmd: "sh", args: ["-c", `npm install -g ${pkg} > '${logPath}' 2>&1`] };
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 };
111
126
  }
112
127
  /**
113
128
  * Launch the update fully detached and return immediately — the caller
114
129
  * (cmdUpdate) should print guidance and exit right away rather than await
115
130
  * anything else, which is what avoids the Windows file-lock failure mode.
116
131
  * `spawner` is injectable for tests so nothing is actually spawned there.
132
+ * `writer` is injectable so tests don't touch the real filesystem either.
117
133
  */
118
- export function launchDetachedUpdate(version, logPath, spawner = spawn) {
119
- const { cmd, args } = detachedInstallCommand(version, logPath);
134
+ export function launchDetachedUpdate(version, logPath, spawner = spawn, writer = (path, content) => writeFileSync(path, content, "utf8")) {
135
+ const { cmd, args, scriptPath, scriptContent } = detachedInstallCommand(version, logPath);
136
+ writer(scriptPath, scriptContent);
137
+ if (process.platform !== "win32") {
138
+ try {
139
+ chmodSync(scriptPath, 0o755);
140
+ }
141
+ catch {
142
+ /* best-effort */
143
+ }
144
+ }
120
145
  const child = spawner(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
121
146
  child.on("error", () => {
122
147
  /* best-effort; the log file (or its absence) is the source of truth */
@@ -153,7 +178,9 @@ export async function performUpdate(currentVersion, opts = {}) {
153
178
  const latest = version.latest;
154
179
  if (!opts.wait) {
155
180
  const logPath = opts.logPath ?? defaultLogPath();
156
- const pid = launchDetachedUpdate(latest, logPath, opts.spawner);
181
+ const pid = opts.writer
182
+ ? launchDetachedUpdate(latest, logPath, opts.spawner, opts.writer)
183
+ : launchDetachedUpdate(latest, logPath, opts.spawner);
157
184
  return {
158
185
  ok: true,
159
186
  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.`,
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.3";
49
+ const VERSION = "0.6.5";
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.3";
55
+ const version = opts.version ?? "0.6.5";
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.3",
3
+ "version": "0.6.5",
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": {