maqcli 0.6.1 → 0.6.2
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/launcher.js +1 -1
- package/dist/core/update.d.ts +81 -2
- package/dist/core/update.js +115 -5
- package/dist/index.js +44 -10
- package/dist/server/daemon.js +1 -1
- package/package.json +1 -1
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.6.
|
|
356
|
+
const daemon = createDaemon({ token: authKey, version: "0.6.2" });
|
|
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
|
@@ -7,7 +7,25 @@
|
|
|
7
7
|
* Network access here is a deliberate, user-initiated exception to the
|
|
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
|
+
*
|
|
11
|
+
* THE WINDOWS FILE-LOCK PROBLEM AND ITS FIX
|
|
12
|
+
* ------------------------------------------
|
|
13
|
+
* `npm install -g` has to overwrite the `maq`/`maqcli` shim and the running
|
|
14
|
+
* package's files. If that `npm install` runs as a CHILD of the currently
|
|
15
|
+
* executing `maq` process (i.e. `maq` spawns it and waits), Windows can hold
|
|
16
|
+
* file/handle locks on the very binary being replaced for as long as the
|
|
17
|
+
* parent process tree is alive — npm's overwrite can fail or silently leave a
|
|
18
|
+
* half-updated install. Waiting-for-the-child is the actual cause, not npm
|
|
19
|
+
* itself.
|
|
20
|
+
*
|
|
21
|
+
* The fix: spawn the npm install fully DETACHED (`detached: true`,
|
|
22
|
+
* `stdio: "ignore"`, `.unref()`) so it is not a child of `maq` in any way
|
|
23
|
+
* meaningful to the OS's lock semantics, then have `maq` EXIT IMMEDIATELY
|
|
24
|
+
* instead of waiting on it. By the time npm actually touches the shim files,
|
|
25
|
+
* the process that used to hold them has already terminated. This is the same
|
|
26
|
+
* pattern self-updating CLIs (rustup, deno, bun) use on Windows.
|
|
10
27
|
*/
|
|
28
|
+
import { spawn } from "node:child_process";
|
|
11
29
|
export interface VersionInfo {
|
|
12
30
|
current: string;
|
|
13
31
|
latest: string | null;
|
|
@@ -24,17 +42,78 @@ export interface UpdateOutcome {
|
|
|
24
42
|
ok: boolean;
|
|
25
43
|
message: string;
|
|
26
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
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the OS-appropriate command to run `npm install -g maqcli@<version>`
|
|
52
|
+
* 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`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function detachedInstallCommand(version: string, logPath: string, platform?: NodeJS.Platform): {
|
|
58
|
+
cmd: string;
|
|
59
|
+
args: string[];
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Launch the update fully detached and return immediately — the caller
|
|
63
|
+
* (cmdUpdate) should print guidance and exit right away rather than await
|
|
64
|
+
* anything else, which is what avoids the Windows file-lock failure mode.
|
|
65
|
+
* `spawner` is injectable for tests so nothing is actually spawned there.
|
|
66
|
+
*/
|
|
67
|
+
export declare function launchDetachedUpdate(version: string, logPath: string, spawner?: typeof spawn): number | undefined;
|
|
68
|
+
export interface UpdateOutcome {
|
|
69
|
+
ok: boolean;
|
|
70
|
+
message: string;
|
|
71
|
+
version?: VersionInfo;
|
|
72
|
+
/** Set when the update was launched detached; the caller should exit(0) promptly instead of doing more work. */
|
|
73
|
+
detachedPid?: number;
|
|
74
|
+
/** Path a caller can inspect afterward to see the detached install's own npm output. */
|
|
75
|
+
logPath?: string;
|
|
27
76
|
}
|
|
28
77
|
/**
|
|
29
78
|
* Perform the update: check the registry, and if a newer version exists,
|
|
30
|
-
*
|
|
31
|
-
*
|
|
79
|
+
* launch `npm install -g maqcli@<latest>` DETACHED from this process (see the
|
|
80
|
+
* module doc — this is what avoids the Windows file-lock failure mode where
|
|
81
|
+
* npm can't overwrite files the running `maq` process still holds open).
|
|
82
|
+
*
|
|
83
|
+
* The detached path is the default and recommended flow: this function
|
|
84
|
+
* returns as soon as the install is launched; it does not wait for npm to
|
|
85
|
+
* finish, because waiting would defeat the point (the parent would still be
|
|
86
|
+
* alive, still holding the lock, for the whole duration).
|
|
87
|
+
*
|
|
88
|
+
* `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.
|
|
32
91
|
*/
|
|
33
92
|
export declare function performUpdate(currentVersion: string, opts?: {
|
|
34
93
|
checkOnly?: boolean;
|
|
94
|
+
/** Run synchronously in-process instead of detaching (tests; --wait). */
|
|
95
|
+
wait?: boolean;
|
|
35
96
|
runner?: (cmd: string, args: string[]) => Promise<{
|
|
36
97
|
code: number | null;
|
|
37
98
|
stdout: string;
|
|
38
99
|
stderr: string;
|
|
39
100
|
}>;
|
|
101
|
+
spawner?: typeof spawn;
|
|
102
|
+
logPath?: string;
|
|
40
103
|
}): Promise<UpdateOutcome>;
|
|
104
|
+
/**
|
|
105
|
+
* Read a detached update's log file (best-effort). Used by `maq update
|
|
106
|
+
* --status` to show what the background install actually did, since the
|
|
107
|
+
* `maq update` invocation that launched it already exited.
|
|
108
|
+
*/
|
|
109
|
+
export declare function readUpdateLog(logPath: string): Promise<string | null>;
|
|
110
|
+
/**
|
|
111
|
+
* Ask npm what is ACTUALLY installed globally right now (ground truth, not
|
|
112
|
+
* "what did we ask for") — this is what confirms a detached update landed,
|
|
113
|
+
* independent of which process/terminal you're asking from.
|
|
114
|
+
*/
|
|
115
|
+
export declare function installedGlobalVersion(runner?: (cmd: string, args: string[]) => Promise<{
|
|
116
|
+
code: number | null;
|
|
117
|
+
stdout: string;
|
|
118
|
+
stderr: string;
|
|
119
|
+
}>): Promise<string | null>;
|
package/dist/core/update.js
CHANGED
|
@@ -7,7 +7,27 @@
|
|
|
7
7
|
* Network access here is a deliberate, user-initiated exception to the
|
|
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
|
+
*
|
|
11
|
+
* THE WINDOWS FILE-LOCK PROBLEM AND ITS FIX
|
|
12
|
+
* ------------------------------------------
|
|
13
|
+
* `npm install -g` has to overwrite the `maq`/`maqcli` shim and the running
|
|
14
|
+
* package's files. If that `npm install` runs as a CHILD of the currently
|
|
15
|
+
* executing `maq` process (i.e. `maq` spawns it and waits), Windows can hold
|
|
16
|
+
* file/handle locks on the very binary being replaced for as long as the
|
|
17
|
+
* parent process tree is alive — npm's overwrite can fail or silently leave a
|
|
18
|
+
* half-updated install. Waiting-for-the-child is the actual cause, not npm
|
|
19
|
+
* itself.
|
|
20
|
+
*
|
|
21
|
+
* The fix: spawn the npm install fully DETACHED (`detached: true`,
|
|
22
|
+
* `stdio: "ignore"`, `.unref()`) so it is not a child of `maq` in any way
|
|
23
|
+
* meaningful to the OS's lock semantics, then have `maq` EXIT IMMEDIATELY
|
|
24
|
+
* instead of waiting on it. By the time npm actually touches the shim files,
|
|
25
|
+
* the process that used to hold them has already terminated. This is the same
|
|
26
|
+
* pattern self-updating CLIs (rustup, deno, bun) use on Windows.
|
|
10
27
|
*/
|
|
28
|
+
import { spawn } from "node:child_process";
|
|
29
|
+
import { tmpdir } from "node:os";
|
|
30
|
+
import { join } from "node:path";
|
|
11
31
|
import { execSafe } from "./exec.js";
|
|
12
32
|
/** Parse "x.y.z" into a comparable tuple; non-numeric parts sort as 0. */
|
|
13
33
|
function parseSemver(v) {
|
|
@@ -46,10 +66,50 @@ export async function checkForUpdate(currentVersion) {
|
|
|
46
66
|
return { current: currentVersion, latest: null, upToDate: true, error: e.message };
|
|
47
67
|
}
|
|
48
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Build the OS-appropriate command to run `npm install -g maqcli@<version>`
|
|
71
|
+
* 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`.
|
|
75
|
+
*/
|
|
76
|
+
export function detachedInstallCommand(version, logPath, platform = process.platform) {
|
|
77
|
+
const pkg = `maqcli@${version}`;
|
|
78
|
+
if (platform === "win32") {
|
|
79
|
+
// /c runs and closes; redirection captures npm's own output for later inspection.
|
|
80
|
+
return { cmd: "cmd.exe", args: ["/c", `npm install -g ${pkg} > "${logPath}" 2>&1`] };
|
|
81
|
+
}
|
|
82
|
+
return { cmd: "sh", args: ["-c", `npm install -g ${pkg} > '${logPath}' 2>&1`] };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Launch the update fully detached and return immediately — the caller
|
|
86
|
+
* (cmdUpdate) should print guidance and exit right away rather than await
|
|
87
|
+
* anything else, which is what avoids the Windows file-lock failure mode.
|
|
88
|
+
* `spawner` is injectable for tests so nothing is actually spawned there.
|
|
89
|
+
*/
|
|
90
|
+
export function launchDetachedUpdate(version, logPath, spawner = spawn) {
|
|
91
|
+
const { cmd, args } = detachedInstallCommand(version, logPath);
|
|
92
|
+
const child = spawner(cmd, args, { detached: true, stdio: "ignore", windowsHide: true });
|
|
93
|
+
child.on("error", () => {
|
|
94
|
+
/* best-effort; the log file (or its absence) is the source of truth */
|
|
95
|
+
});
|
|
96
|
+
child.unref();
|
|
97
|
+
return child.pid;
|
|
98
|
+
}
|
|
49
99
|
/**
|
|
50
100
|
* Perform the update: check the registry, and if a newer version exists,
|
|
51
|
-
*
|
|
52
|
-
*
|
|
101
|
+
* launch `npm install -g maqcli@<latest>` DETACHED from this process (see the
|
|
102
|
+
* module doc — this is what avoids the Windows file-lock failure mode where
|
|
103
|
+
* npm can't overwrite files the running `maq` process still holds open).
|
|
104
|
+
*
|
|
105
|
+
* The detached path is the default and recommended flow: this function
|
|
106
|
+
* returns as soon as the install is launched; it does not wait for npm to
|
|
107
|
+
* finish, because waiting would defeat the point (the parent would still be
|
|
108
|
+
* alive, still holding the lock, for the whole duration).
|
|
109
|
+
*
|
|
110
|
+
* `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.
|
|
53
113
|
*/
|
|
54
114
|
export async function performUpdate(currentVersion, opts = {}) {
|
|
55
115
|
const version = await checkForUpdate(currentVersion);
|
|
@@ -62,14 +122,64 @@ export async function performUpdate(currentVersion, opts = {}) {
|
|
|
62
122
|
if (opts.checkOnly) {
|
|
63
123
|
return { ok: true, message: `update available: v${version.current} -> v${version.latest}`, version };
|
|
64
124
|
}
|
|
125
|
+
const latest = version.latest;
|
|
126
|
+
if (!opts.wait) {
|
|
127
|
+
const logPath = opts.logPath ?? defaultLogPath();
|
|
128
|
+
const pid = launchDetachedUpdate(latest, logPath, opts.spawner);
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
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.`,
|
|
132
|
+
version,
|
|
133
|
+
detachedPid: pid,
|
|
134
|
+
logPath,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
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.
|
|
65
141
|
const runner = opts.runner ?? (async (cmd, args) => execSafe(cmd, args, { timeoutMs: 120000 }));
|
|
66
|
-
const outcome = await runner("npm", ["install", "-g", `maqcli@${
|
|
142
|
+
const outcome = await runner("npm", ["install", "-g", `maqcli@${latest}`]);
|
|
67
143
|
if (outcome.code !== 0) {
|
|
68
144
|
return {
|
|
69
145
|
ok: false,
|
|
70
|
-
message: `update to v${
|
|
146
|
+
message: `update to v${latest} failed (exit ${outcome.code}): ${outcome.stderr.slice(0, 400) || outcome.stdout.slice(0, 400)}`,
|
|
71
147
|
version,
|
|
72
148
|
};
|
|
73
149
|
}
|
|
74
|
-
return { ok: true, message: `updated v${version.current} -> v${
|
|
150
|
+
return { ok: true, message: `updated v${version.current} -> v${latest}`, version };
|
|
151
|
+
}
|
|
152
|
+
function defaultLogPath() {
|
|
153
|
+
return join(tmpdir(), `maq-update-${Date.now()}.log`);
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Read a detached update's log file (best-effort). Used by `maq update
|
|
157
|
+
* --status` to show what the background install actually did, since the
|
|
158
|
+
* `maq update` invocation that launched it already exited.
|
|
159
|
+
*/
|
|
160
|
+
export async function readUpdateLog(logPath) {
|
|
161
|
+
try {
|
|
162
|
+
const { readFile } = await import("node:fs/promises");
|
|
163
|
+
return await readFile(logPath, "utf8");
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Ask npm what is ACTUALLY installed globally right now (ground truth, not
|
|
171
|
+
* "what did we ask for") — this is what confirms a detached update landed,
|
|
172
|
+
* independent of which process/terminal you're asking from.
|
|
173
|
+
*/
|
|
174
|
+
export async function installedGlobalVersion(runner = (cmd, args) => execSafe(cmd, args, { timeoutMs: 15000 })) {
|
|
175
|
+
const outcome = await runner("npm", ["list", "-g", "maqcli", "--depth=0", "--json"]);
|
|
176
|
+
if (outcome.code !== 0)
|
|
177
|
+
return null;
|
|
178
|
+
try {
|
|
179
|
+
const parsed = JSON.parse(outcome.stdout);
|
|
180
|
+
return parsed.dependencies?.maqcli?.version ?? null;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
75
185
|
}
|
package/dist/index.js
CHANGED
|
@@ -43,10 +43,10 @@ import { runInit } from "./core/init-wizard.js";
|
|
|
43
43
|
import { CostTracker } from "./core/cost-tracker.js";
|
|
44
44
|
import { runLauncher } from "./core/launcher.js";
|
|
45
45
|
import { runOrchestration } from "./core/orchestrator.js";
|
|
46
|
-
import { performUpdate } from "./core/update.js";
|
|
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.
|
|
49
|
+
const VERSION = "0.6.2";
|
|
50
50
|
async function main(argv) {
|
|
51
51
|
const [command, ...rest] = argv;
|
|
52
52
|
switch (command) {
|
|
@@ -432,22 +432,56 @@ function cmdSecurity(args) {
|
|
|
432
432
|
async function cmdUpdate(args) {
|
|
433
433
|
const { values } = parseArgs({
|
|
434
434
|
args,
|
|
435
|
-
options: {
|
|
435
|
+
options: {
|
|
436
|
+
...commonFlags(),
|
|
437
|
+
check: { type: "boolean", default: false },
|
|
438
|
+
wait: { type: "boolean", default: false },
|
|
439
|
+
status: { type: "boolean", default: false },
|
|
440
|
+
},
|
|
436
441
|
allowPositionals: true,
|
|
437
442
|
});
|
|
443
|
+
if (values.status) {
|
|
444
|
+
const installed = await installedGlobalVersion();
|
|
445
|
+
if (values.json) {
|
|
446
|
+
logger.out(JSON.stringify({ runningVersion: VERSION, installedGlobalVersion: installed }, null, 0));
|
|
447
|
+
return 0;
|
|
448
|
+
}
|
|
449
|
+
logger.out(`this terminal's maq: v${VERSION}`);
|
|
450
|
+
logger.out(`globally installed maq: ${installed ? "v" + installed : "unknown (npm list failed)"}`);
|
|
451
|
+
if (installed && installed !== VERSION) {
|
|
452
|
+
logger.out("\nThese differ — a background update likely finished. Open a NEW terminal window");
|
|
453
|
+
logger.out("(this one is still running the old code in memory) to pick up the new version.");
|
|
454
|
+
}
|
|
455
|
+
else if (installed) {
|
|
456
|
+
logger.out("\nUp to date and consistent.");
|
|
457
|
+
}
|
|
458
|
+
return 0;
|
|
459
|
+
}
|
|
438
460
|
logger.out(`current version: ${VERSION}`);
|
|
439
461
|
logger.out(values.check ? "checking npm for a newer version…" : "checking npm and updating if a newer version is available…");
|
|
440
|
-
const outcome = await performUpdate(VERSION, { checkOnly: values.check });
|
|
462
|
+
const outcome = await performUpdate(VERSION, { checkOnly: values.check, wait: values.wait });
|
|
441
463
|
if (values.json) {
|
|
442
464
|
logger.out(JSON.stringify(outcome, null, 0));
|
|
443
465
|
return outcome.ok ? 0 : 1;
|
|
444
466
|
}
|
|
445
467
|
logger.out(outcome.message);
|
|
446
|
-
if (outcome.ok && outcome.version && !outcome.version.upToDate
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
468
|
+
if (outcome.ok && outcome.version && !outcome.version.upToDate) {
|
|
469
|
+
if (values.check) {
|
|
470
|
+
logger.out("run 'maq update' (without --check) to install it.");
|
|
471
|
+
}
|
|
472
|
+
else if (outcome.detachedPid) {
|
|
473
|
+
// Detached path: THIS is the fix for the Windows file-lock problem —
|
|
474
|
+
// maq is exiting now, on purpose, so nothing holds a lock on the files
|
|
475
|
+
// npm is about to overwrite. Give the user a concrete way to confirm it
|
|
476
|
+
// landed instead of leaving them guessing.
|
|
477
|
+
logger.out("");
|
|
478
|
+
logger.out(` install log: ${outcome.logPath}`);
|
|
479
|
+
logger.out(" check progress any time with: maq update --status");
|
|
480
|
+
logger.out(" (open a NEW terminal window before running that — this one will keep showing the old version)");
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
logger.out("restart your terminal (or open a new one) to pick up the updated binary.");
|
|
484
|
+
}
|
|
451
485
|
}
|
|
452
486
|
return outcome.ok ? 0 : 1;
|
|
453
487
|
}
|
|
@@ -1202,7 +1236,7 @@ function printHelp() {
|
|
|
1202
1236
|
' completion <shell> Shell completions (bash/zsh/fish/powershell)',
|
|
1203
1237
|
" audit verify <run-dir> Verify a run's hash-chained audit log",
|
|
1204
1238
|
" security [report|rules|scan <path>|add|remove] Enforced security rules + recent findings",
|
|
1205
|
-
" update [--check]
|
|
1239
|
+
" update [--check|--wait|--status] Check npm for a newer maqcli and self-update (detached; avoids file locks)",
|
|
1206
1240
|
" config [get|set|path|reset] Read or update ~/.maqcli/config.json",
|
|
1207
1241
|
" version | help [<topic>]",
|
|
1208
1242
|
"",
|
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.6.
|
|
55
|
+
const version = opts.version ?? "0.6.2";
|
|
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
|
+
"version": "0.6.2",
|
|
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": {
|