nolo-cli 0.178.0-alpha.2 → 0.179.0-alpha.1
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/{chunk-TWJYVVUB.js → chunk-6MXCQLLB.js} +74 -321
- package/{commandRegistry-YGDYRIAX.js → commandRegistry-OW2MIVIT.js} +5 -5
- package/index.js +45 -20
- package/package.json +1 -1
- package/{readlineWorkspace-C3FPP5KC.js → readlineWorkspace-PSJS3ZS2.js} +1 -1
- package/{updateCommands-C56QMWHF.js → updateCommands-BXGFH4VX.js} +1 -1
|
@@ -28,40 +28,24 @@ import {
|
|
|
28
28
|
mkdirSync,
|
|
29
29
|
openSync,
|
|
30
30
|
readFileSync,
|
|
31
|
-
|
|
31
|
+
readdirSync,
|
|
32
32
|
rmSync,
|
|
33
|
-
statSync,
|
|
34
33
|
writeSync,
|
|
35
34
|
writeFileSync
|
|
36
35
|
} from "node:fs";
|
|
37
36
|
import { homedir, tmpdir } from "node:os";
|
|
38
37
|
import { dirname, isAbsolute, join, normalize } from "node:path";
|
|
39
38
|
import { randomUUID } from "node:crypto";
|
|
40
|
-
var UPDATE_STATE_FILE = "update-result.json";
|
|
41
|
-
var UPDATE_STALE_MS = 15 * 60 * 1e3;
|
|
42
|
-
var UPDATE_CORRUPT_MAX_AGE_MS = 30 * 60 * 1e3;
|
|
43
39
|
var UPDATE_LOCK_FILE = "update.lock";
|
|
44
40
|
function resolveNoloHome(env, homeDir = homedir()) {
|
|
45
41
|
return env.NOLO_HOME?.trim() || join(homeDir, ".nolo");
|
|
46
42
|
}
|
|
47
|
-
function resolveWindowsUpdateStatePath(env = process.env, homeDir = homedir()) {
|
|
48
|
-
return join(resolveNoloHome(env, homeDir), "updates", UPDATE_STATE_FILE);
|
|
49
|
-
}
|
|
50
43
|
function resolveTrustedWindowsToolPaths(env, execPath, overrides) {
|
|
51
44
|
const nodePath = overrides.nodePath ?? execPath;
|
|
52
45
|
const npmCliPath = overrides.npmCliPath ?? join(dirname(nodePath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
53
|
-
const windowsRoot = env.SystemRoot?.trim() || env.WINDIR?.trim();
|
|
54
|
-
const powershellPath = overrides.powershellPath ?? (windowsRoot ? join(
|
|
55
|
-
windowsRoot,
|
|
56
|
-
"System32",
|
|
57
|
-
"WindowsPowerShell",
|
|
58
|
-
"v1.0",
|
|
59
|
-
"powershell.exe"
|
|
60
|
-
) : "");
|
|
61
46
|
for (const [label, path] of Object.entries({
|
|
62
47
|
nodePath,
|
|
63
|
-
npmCliPath
|
|
64
|
-
powershellPath
|
|
48
|
+
npmCliPath
|
|
65
49
|
})) {
|
|
66
50
|
if (!path || !isAbsolute(path) || !existsSync(path)) {
|
|
67
51
|
throw new Error(
|
|
@@ -69,7 +53,7 @@ function resolveTrustedWindowsToolPaths(env, execPath, overrides) {
|
|
|
69
53
|
);
|
|
70
54
|
}
|
|
71
55
|
}
|
|
72
|
-
return { nodePath, npmCliPath
|
|
56
|
+
return { nodePath, npmCliPath };
|
|
73
57
|
}
|
|
74
58
|
function removeOwnedLock(lockPath, updateId) {
|
|
75
59
|
try {
|
|
@@ -81,188 +65,20 @@ function removeOwnedLock(lockPath, updateId) {
|
|
|
81
65
|
} catch {
|
|
82
66
|
}
|
|
83
67
|
}
|
|
84
|
-
function
|
|
85
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
86
|
-
const tempPath = `${path}.${process.pid}.tmp`;
|
|
87
|
-
writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}
|
|
88
|
-
`, "utf8");
|
|
89
|
-
renameSync(tempPath, path);
|
|
90
|
-
}
|
|
91
|
-
function readWindowsUpdateState(env = process.env, homeDir = homedir()) {
|
|
92
|
-
const statePath = resolveWindowsUpdateStatePath(env, homeDir);
|
|
93
|
-
if (!existsSync(statePath)) return null;
|
|
94
|
-
try {
|
|
95
|
-
const parsed = JSON.parse(
|
|
96
|
-
readFileSync(statePath, "utf8")
|
|
97
|
-
);
|
|
98
|
-
if (!parsed || !["pending", "success", "failed"].includes(parsed.status)) {
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
return parsed;
|
|
102
|
-
} catch {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
function assessWindowsUpdatePending(state, now, statePath) {
|
|
107
|
-
const startedAtMs = Date.parse(state.startedAt);
|
|
108
|
-
const MAX_CLOCK_SKEW_MS = 5 * 60 * 1e3;
|
|
109
|
-
if (Number.isFinite(startedAtMs) && startedAtMs - now <= MAX_CLOCK_SKEW_MS) {
|
|
110
|
-
const ageMs = now - startedAtMs;
|
|
111
|
-
const stale = ageMs >= UPDATE_STALE_MS;
|
|
112
|
-
return {
|
|
113
|
-
classification: stale ? "stale" : "active",
|
|
114
|
-
ageMs,
|
|
115
|
-
startedAtUsable: true,
|
|
116
|
-
blocksStartup: !stale
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
let mtimeAge = Number.NaN;
|
|
120
|
-
try {
|
|
121
|
-
mtimeAge = now - statSync(statePath).mtimeMs;
|
|
122
|
-
} catch {
|
|
123
|
-
}
|
|
124
|
-
if (!Number.isFinite(mtimeAge) || mtimeAge < UPDATE_CORRUPT_MAX_AGE_MS) {
|
|
125
|
-
return {
|
|
126
|
-
classification: "corrupt-unageable",
|
|
127
|
-
ageMs: mtimeAge,
|
|
128
|
-
startedAtUsable: false,
|
|
129
|
-
blocksStartup: true
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
classification: "stale",
|
|
134
|
-
ageMs: mtimeAge,
|
|
135
|
-
startedAtUsable: false,
|
|
136
|
-
blocksStartup: false
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
function consumeWindowsUpdateStartupNotice(env = process.env, options = {}) {
|
|
68
|
+
function getWindowsUpdateStatus(env = process.env, options = {}) {
|
|
140
69
|
const homeDir = options.homeDir ?? homedir();
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
if (state.status === "pending") {
|
|
145
|
-
if (options.ownerUpdateId && state.updateId && options.ownerUpdateId === state.updateId) {
|
|
146
|
-
return null;
|
|
147
|
-
}
|
|
148
|
-
const now = options.now ?? Date.now();
|
|
149
|
-
const assessment = assessWindowsUpdatePending(state, now, statePath);
|
|
150
|
-
if (assessment.blocksStartup) {
|
|
151
|
-
return {
|
|
152
|
-
blocking: true,
|
|
153
|
-
text: `A Nolo update may still be in progress (${describeUpdatePhase(state)}). Run "nolo update status" for details, or retry shortly.
|
|
154
|
-
Log: ${state.logPath}`
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
if (state.lockPath && state.updateId) {
|
|
158
|
-
removeOwnedLock(state.lockPath, state.updateId);
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
blocking: false,
|
|
162
|
-
text: `The previous Nolo update helper stopped unexpectedly and its lock was released. Verify with "nolo -v" / "nolo update status"; a manual npm repair may be required.
|
|
163
|
-
Log: ${state.logPath}`
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
if (state.noticeAcknowledgedAt) {
|
|
167
|
-
return null;
|
|
168
|
-
}
|
|
169
|
-
const acknowledged = {
|
|
170
|
-
...state,
|
|
171
|
-
noticeAcknowledgedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
172
|
-
};
|
|
70
|
+
const installedVersion = options.currentVersion ?? "unknown";
|
|
71
|
+
const logDir = join(resolveNoloHome(env, homeDir), "updates");
|
|
72
|
+
let lastLogPath;
|
|
173
73
|
try {
|
|
174
|
-
|
|
74
|
+
const newest = readdirSync(logDir).filter((name) => name.startsWith("update-") && name.endsWith(".log")).sort().at(-1);
|
|
75
|
+
if (newest) lastLogPath = join(logDir, newest);
|
|
175
76
|
} catch {
|
|
176
77
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
Log: ${state.logPath}`
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
return {
|
|
185
|
-
blocking: false,
|
|
186
|
-
text: `Nolo update failed. Verify with "nolo -v"; npm may require a manual repair.${state.message ? `
|
|
187
|
-
${state.message}` : ""}
|
|
188
|
-
Log: ${state.logPath}`
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
function describeUpdatePhase(state) {
|
|
192
|
-
if (state.phase) {
|
|
193
|
-
return `phase: ${state.phase}`;
|
|
194
|
-
}
|
|
195
|
-
return "helper running";
|
|
196
|
-
}
|
|
197
|
-
function getWindowsUpdateStatus(env = process.env, options = {}) {
|
|
198
|
-
const homeDir = options.homeDir ?? homedir();
|
|
199
|
-
const state = readWindowsUpdateState(env, homeDir);
|
|
200
|
-
const now = options.now ?? Date.now();
|
|
201
|
-
const version = options.currentVersion ?? "unknown";
|
|
202
|
-
if (!state) {
|
|
203
|
-
return {
|
|
204
|
-
status: "none",
|
|
205
|
-
alive: false,
|
|
206
|
-
text: `No recorded Nolo update.
|
|
207
|
-
Installed version: ${version}`
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
const lines = [];
|
|
211
|
-
lines.push(`Update status: ${state.status}`);
|
|
212
|
-
lines.push(`Installed version: ${version}`);
|
|
213
|
-
lines.push(`Channel: ${state.channel}`);
|
|
214
|
-
lines.push(`From version: ${state.currentVersion}`);
|
|
215
|
-
if (state.targetVersion) lines.push(`Target version: ${state.targetVersion}`);
|
|
216
|
-
if (state.phase) lines.push(`Phase: ${state.phase}`);
|
|
217
|
-
lines.push(`Started: ${state.startedAt}`);
|
|
218
|
-
if (state.phaseStartedAt) lines.push(`Phase started: ${state.phaseStartedAt}`);
|
|
219
|
-
if (state.phaseDeadline) lines.push(`Phase deadline: ${state.phaseDeadline}`);
|
|
220
|
-
if (state.finishedAt) lines.push(`Finished: ${state.finishedAt}`);
|
|
221
|
-
if (state.helperPid) lines.push(`Helper pid: ${state.helperPid}`);
|
|
222
|
-
if (state.message) lines.push(`Message: ${state.message}`);
|
|
223
|
-
lines.push(`Log: ${state.logPath}`);
|
|
224
|
-
if (state.status === "pending") {
|
|
225
|
-
const statePath = resolveWindowsUpdateStatePath(env, homeDir);
|
|
226
|
-
const assessment = assessWindowsUpdatePending(state, now, statePath);
|
|
227
|
-
const deadline = state.phaseDeadline ? Date.parse(state.phaseDeadline) : NaN;
|
|
228
|
-
const pastDeadline = Number.isFinite(deadline) && now > deadline;
|
|
229
|
-
if (pastDeadline) {
|
|
230
|
-
lines.push(
|
|
231
|
-
`Phase deadline elapsed: the recorded phase has run past its soft ceiling. This can be a hung helper, but it can also be a slow install \u2014 the lock is NOT auto-released on this evidence alone.`
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
switch (assessment.classification) {
|
|
235
|
-
case "active":
|
|
236
|
-
lines.push(
|
|
237
|
-
`Diagnosis: update record is fresh (${describeUpdatePhase(state)}). The helper process has not been verified alive; the record simply has not outlived its bound, so startup is still blocked.`
|
|
238
|
-
);
|
|
239
|
-
break;
|
|
240
|
-
case "corrupt-unageable":
|
|
241
|
-
lines.push(
|
|
242
|
-
`Diagnosis: update record has an unreadable start time and cannot be aged reliably. Startup stays conservatively blocked until the record outlives its hard bound; inspect the log to tell a hung helper from a live one.`
|
|
243
|
-
);
|
|
244
|
-
break;
|
|
245
|
-
case "stale":
|
|
246
|
-
lines.push(
|
|
247
|
-
`Diagnosis: update record has outlived its bound; the helper is presumed stopped. The startup guard releases the stale lock on next start, then this record remains here for diagnosis.`
|
|
248
|
-
);
|
|
249
|
-
break;
|
|
250
|
-
}
|
|
251
|
-
return {
|
|
252
|
-
status: "pending",
|
|
253
|
-
alive: assessment.blocksStartup,
|
|
254
|
-
pendingClassification: assessment.classification,
|
|
255
|
-
text: lines.join("\n")
|
|
256
|
-
};
|
|
257
|
-
}
|
|
258
|
-
if (state.status === "success") {
|
|
259
|
-
lines.push(
|
|
260
|
-
state.noticeAcknowledgedAt ? `Diagnosis: update applied and already acknowledged.` : `Diagnosis: update applied; notice pending on next start.`
|
|
261
|
-
);
|
|
262
|
-
} else {
|
|
263
|
-
lines.push(`Diagnosis: update failed; see the log above.`);
|
|
264
|
-
}
|
|
265
|
-
return { status: state.status, alive: false, text: lines.join("\n") };
|
|
78
|
+
const text = lastLogPath ? `Installed version: ${installedVersion}
|
|
79
|
+
Last update log: ${lastLogPath}` : `Installed version: ${installedVersion}
|
|
80
|
+
No recorded update log.`;
|
|
81
|
+
return { installedVersion, lastLogPath, text };
|
|
266
82
|
}
|
|
267
83
|
function defaultLaunchWindowsUpdateHelper(input) {
|
|
268
84
|
const child = spawn(input.execPath, [input.helperPath, input.payload], {
|
|
@@ -284,26 +100,42 @@ const { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirS
|
|
|
284
100
|
const { dirname, isAbsolute, join, normalize, relative } = require("node:path");
|
|
285
101
|
|
|
286
102
|
const payload = JSON.parse(Buffer.from(process.argv[2], "base64url").toString("utf8"));
|
|
287
|
-
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
288
103
|
const log = (line) => appendFileSync(payload.logPath, "[" + new Date().toISOString() + "] " + line + "\n", "utf8");
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
writeFileSync(temp, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
293
|
-
renameSync(temp, payload.statePath);
|
|
294
|
-
};
|
|
295
|
-
// Record the current helper step with a bounded wall-clock deadline so the
|
|
296
|
-
// startup guard can tell a live phase from a hung one. Deadline is a soft
|
|
297
|
-
// ceiling: a phase that overruns it is reported stale, not killed.
|
|
298
|
-
const setPhase = (base, phase, budgetMs) => {
|
|
299
|
-
const phaseStartedAt = new Date().toISOString();
|
|
300
|
-
const phaseDeadline = new Date(Date.now() + budgetMs).toISOString();
|
|
104
|
+
// 完成通知:best-effort。通知失败(无 PowerShell / 通知被禁用 / 无 notify-send)
|
|
105
|
+
// 一律静默,绝不能影响更新结果本身。
|
|
106
|
+
const notify = (title, message) => {
|
|
301
107
|
try {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
108
|
+
if (process.platform === "win32") {
|
|
109
|
+
const psPath = join(process.env.SystemRoot || process.env.WINDIR || "C:\\\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
110
|
+
// 脚本里不含任何动态文本:title/message 经环境变量传入。
|
|
111
|
+
// 拼接转义永远追不上解释器认的引号集合(ASCII 单引号、弯引号 ‘ ’ 都会被当成引号),
|
|
112
|
+
// 所以这里不用引号包裹,直接用 $env: 取值。
|
|
113
|
+
const ps = [
|
|
114
|
+
"Add-Type -AssemblyName System.Windows.Forms",
|
|
115
|
+
"Add-Type -AssemblyName System.Drawing",
|
|
116
|
+
"$n = New-Object System.Windows.Forms.NotifyIcon",
|
|
117
|
+
"$n.Icon = [System.Drawing.SystemIcons]::Information",
|
|
118
|
+
"$n.Visible = $true",
|
|
119
|
+
"$n.ShowBalloonTip(6000, $env:NOLO_NOTIFY_TITLE, $env:NOLO_NOTIFY_MESSAGE, 'Info')",
|
|
120
|
+
"Start-Sleep -Milliseconds 800",
|
|
121
|
+
"$n.Dispose()",
|
|
122
|
+
].join("; ");
|
|
123
|
+
spawnSync(psPath, ["-NoProfile", "-NonInteractive", "-Command", ps], {
|
|
124
|
+
windowsHide: true,
|
|
125
|
+
timeout: 15000,
|
|
126
|
+
env: { ...process.env, NOLO_NOTIFY_TITLE: title, NOLO_NOTIFY_MESSAGE: message },
|
|
127
|
+
});
|
|
128
|
+
} else if (process.platform === "darwin") {
|
|
129
|
+
// 同理:AppleScript 通过 argv 取参,脚本正文是常量。
|
|
130
|
+
spawnSync(
|
|
131
|
+
"osascript",
|
|
132
|
+
["-e", "on run argv", "-e", "display notification (item 2 of argv) with title (item 1 of argv)", "-e", "end run", title, message],
|
|
133
|
+
{ timeout: 10000 },
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
spawnSync("notify-send", [title, message], { timeout: 10000 });
|
|
137
|
+
}
|
|
138
|
+
} catch {}
|
|
307
139
|
};
|
|
308
140
|
const runNpm = (args) => {
|
|
309
141
|
const command = [payload.nodePath, payload.npmCliPath, ...args].join(" ");
|
|
@@ -320,20 +152,6 @@ const runNpm = (args) => {
|
|
|
320
152
|
if (result.error) throw result.error;
|
|
321
153
|
return { code: result.status == null ? 1 : result.status, stdout: String(result.stdout || "").trim() };
|
|
322
154
|
};
|
|
323
|
-
const listOtherNoloPids = () => {
|
|
324
|
-
const script = "$selfPid=" + process.pid + "; Get-CimInstance Win32_Process | Where-Object { $_.ProcessId -ne $selfPid -and ($_.Name -ieq 'node.exe' -or $_.Name -ieq 'bun.exe') -and $_.CommandLine -and $_.CommandLine -match '\\\\node_modules\\\\nolo-cli\\\\index\\.js' } | ForEach-Object { $_.ProcessId }";
|
|
325
|
-
const result = spawnSync(payload.powershellPath, ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
326
|
-
encoding: "utf8",
|
|
327
|
-
windowsHide: true,
|
|
328
|
-
env: process.env,
|
|
329
|
-
cwd: payload.updateDir,
|
|
330
|
-
timeout: 10 * 1000,
|
|
331
|
-
});
|
|
332
|
-
if (result.error || result.status !== 0) {
|
|
333
|
-
throw new Error("Could not verify that all other Nolo windows are closed");
|
|
334
|
-
}
|
|
335
|
-
return String(result.stdout || "").split(/\r?\n/).map((value) => value.trim()).filter(Boolean);
|
|
336
|
-
};
|
|
337
155
|
const parseVersion = (raw) => {
|
|
338
156
|
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(String(raw).trim());
|
|
339
157
|
return match ? { main: [Number(match[1]), Number(match[2]), Number(match[3])], pre: match[4] ? match[4].split(".") : [] } : null;
|
|
@@ -478,33 +296,13 @@ const switchWindowsShims = (shimDir, entrypoint) => {
|
|
|
478
296
|
updateId: payload.updateId,
|
|
479
297
|
lockPath: payload.lockPath,
|
|
480
298
|
};
|
|
481
|
-
let
|
|
299
|
+
let updateSucceeded = false;
|
|
300
|
+
let alreadyCurrent = false;
|
|
482
301
|
try {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
try { process.kill(payload.parentPid, 0); await delay(250); }
|
|
488
|
-
catch { parentExited = true; break; }
|
|
489
|
-
}
|
|
490
|
-
if (!parentExited) throw new Error("Timed out waiting for the current Nolo process to exit");
|
|
491
|
-
await delay(250);
|
|
492
|
-
|
|
493
|
-
progress = setPhase(progress, "wait-other-windows", 120 * 1000 + 30 * 1000);
|
|
494
|
-
const otherProcessDeadline = Date.now() + 120 * 1000;
|
|
495
|
-
let otherPids = listOtherNoloPids();
|
|
496
|
-
let checks = 0;
|
|
497
|
-
while (otherPids.length > 0 && Date.now() < otherProcessDeadline) {
|
|
498
|
-
if (checks === 0 || checks % 10 === 0) log("Waiting for other Nolo processes: " + otherPids.join(", "));
|
|
499
|
-
await delay(1000);
|
|
500
|
-
otherPids = listOtherNoloPids();
|
|
501
|
-
checks++;
|
|
502
|
-
}
|
|
503
|
-
if (otherPids.length > 0) {
|
|
504
|
-
throw new Error("Other Nolo processes are still running: " + otherPids.join(", "));
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
progress = setPhase(progress, "resolve-prefix", 30 * 1000);
|
|
302
|
+
// 2026-10-27 减法:不再等待父进程或其它 Nolo 窗口退出。
|
|
303
|
+
// 新版本写入独立版本目录、最后只切换入口,因此其它进程继续运行旧版本是安全的;
|
|
304
|
+
// 扫描 PID 也不构成真正的写入互斥(扫描结束后照样可以再启动一个进程)。
|
|
305
|
+
log("phase: resolve-prefix");
|
|
508
306
|
const prefixResult = runNpm(["prefix", "-g"]);
|
|
509
307
|
if (prefixResult.code !== 0 || !prefixResult.stdout) throw new Error("Could not resolve npm global prefix");
|
|
510
308
|
const prefix = prefixResult.stdout.split(/\r?\n/).at(-1).trim();
|
|
@@ -514,8 +312,7 @@ const switchWindowsShims = (shimDir, entrypoint) => {
|
|
|
514
312
|
if (!isWithin(expectedRoot, oldEntrypoint) && !isWithin(managedRoot, oldEntrypoint)) {
|
|
515
313
|
throw new Error("Refusing to update a different npm prefix. Current entrypoint: " + payload.entrypointPath + "; npm prefix: " + prefix);
|
|
516
314
|
}
|
|
517
|
-
|
|
518
|
-
progress = setPhase(progress, "resolve-target-version", 30 * 1000);
|
|
315
|
+
log("phase: resolve-target-version");
|
|
519
316
|
const targetResult = runNpm(["view", "nolo-cli@" + payload.channel, "version"]);
|
|
520
317
|
if (targetResult.code !== 0 || !parseVersion(targetResult.stdout)) {
|
|
521
318
|
throw new Error("Could not resolve nolo-cli@" + payload.channel + " target version");
|
|
@@ -526,7 +323,7 @@ const switchWindowsShims = (shimDir, entrypoint) => {
|
|
|
526
323
|
}
|
|
527
324
|
|
|
528
325
|
if (compare(targetVersion, payload.currentVersion) > 0) {
|
|
529
|
-
|
|
326
|
+
log("phase: install-target");
|
|
530
327
|
const versionPrefix = join(payload.managedRoot, targetVersion + "-" + payload.updateId);
|
|
531
328
|
uncommittedVersionPrefix = versionPrefix;
|
|
532
329
|
mkdirSync(versionPrefix, { recursive: true });
|
|
@@ -542,12 +339,12 @@ const switchWindowsShims = (shimDir, entrypoint) => {
|
|
|
542
339
|
const stagedEntrypoint = join(versionPrefix, "node_modules", "nolo-cli", "index.js");
|
|
543
340
|
if (!existsSync(stagedEntrypoint)) throw new Error("Staged nolo entrypoint is missing");
|
|
544
341
|
assertLockOwnership();
|
|
545
|
-
|
|
342
|
+
log("phase: switch-shims");
|
|
546
343
|
const switchedShims = switchWindowsShims(prefix, stagedEntrypoint);
|
|
547
344
|
const verify = spawnSync(payload.nodePath, [stagedEntrypoint, "-v"], {
|
|
548
345
|
encoding: "utf8",
|
|
549
346
|
windowsHide: true,
|
|
550
|
-
env:
|
|
347
|
+
env: process.env,
|
|
551
348
|
cwd: payload.updateDir,
|
|
552
349
|
timeout: 30 * 1000,
|
|
553
350
|
});
|
|
@@ -580,8 +377,9 @@ const switchWindowsShims = (shimDir, entrypoint) => {
|
|
|
580
377
|
log("Verified nolo-cli " + targetVersion + " at " + stagedEntrypoint);
|
|
581
378
|
} else {
|
|
582
379
|
log("Already current at " + targetVersion);
|
|
380
|
+
alreadyCurrent = true;
|
|
583
381
|
}
|
|
584
|
-
|
|
382
|
+
updateSucceeded = true;
|
|
585
383
|
} catch (error) {
|
|
586
384
|
const message = error && error.message ? error.message : String(error);
|
|
587
385
|
log("ERROR: " + message);
|
|
@@ -592,11 +390,6 @@ const switchWindowsShims = (shimDir, entrypoint) => {
|
|
|
592
390
|
try { rmSync(uncommittedVersionPrefix, { recursive: true, force: true }); } catch {}
|
|
593
391
|
}
|
|
594
392
|
}
|
|
595
|
-
if (lockOwnershipLost) {
|
|
596
|
-
log("Not writing a failed status record: the update lock belongs to another updater");
|
|
597
|
-
} else {
|
|
598
|
-
writeState({ ...progress, status: "failed", finishedAt: new Date().toISOString(), message });
|
|
599
|
-
}
|
|
600
393
|
process.exitCode = 1;
|
|
601
394
|
} finally {
|
|
602
395
|
try {
|
|
@@ -606,6 +399,12 @@ const switchWindowsShims = (shimDir, entrypoint) => {
|
|
|
606
399
|
}
|
|
607
400
|
} catch {}
|
|
608
401
|
try { rmSync(__filename, { force: true }); } catch {}
|
|
402
|
+
// 通知放在锁释放之后:同步通知最多阻塞十几秒,不能拖住下一次更新。
|
|
403
|
+
if (updateSucceeded) {
|
|
404
|
+
notify("Nolo", alreadyCurrent ? "Nolo is already up to date" : "Update finished - the next Nolo launch uses the new version");
|
|
405
|
+
} else {
|
|
406
|
+
notify("Nolo", "Update failed - see log: " + payload.logPath);
|
|
407
|
+
}
|
|
609
408
|
}
|
|
610
409
|
})();
|
|
611
410
|
`;
|
|
@@ -613,8 +412,7 @@ const switchWindowsShims = (shimDir, entrypoint) => {
|
|
|
613
412
|
function scheduleWindowsSelfUpdate(options) {
|
|
614
413
|
const env = options.env ?? process.env;
|
|
615
414
|
const homeDir = options.homeDir ?? homedir();
|
|
616
|
-
const
|
|
617
|
-
const updateDir = dirname(statePath);
|
|
415
|
+
const updateDir = join(resolveNoloHome(env, homeDir), "updates");
|
|
618
416
|
const managedRoot = join(resolveNoloHome(env, homeDir), "cli", "versions");
|
|
619
417
|
const lockPath = join(updateDir, UPDATE_LOCK_FILE);
|
|
620
418
|
const updateId = randomUUID();
|
|
@@ -630,29 +428,14 @@ function scheduleWindowsSelfUpdate(options) {
|
|
|
630
428
|
try {
|
|
631
429
|
const lockFd = openSync(lockPath, "wx");
|
|
632
430
|
try {
|
|
633
|
-
writeSync(
|
|
634
|
-
lockFd,
|
|
635
|
-
JSON.stringify({ updateId, startedAt }),
|
|
636
|
-
void 0,
|
|
637
|
-
"utf8"
|
|
638
|
-
);
|
|
431
|
+
writeSync(lockFd, JSON.stringify({ updateId, startedAt }), void 0, "utf8");
|
|
639
432
|
} finally {
|
|
640
433
|
closeSync(lockFd);
|
|
641
434
|
}
|
|
642
435
|
} catch (error) {
|
|
643
|
-
if (!readWindowsUpdateState(env, homeDir)) {
|
|
644
|
-
try {
|
|
645
|
-
const lock = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
646
|
-
const age = Date.now() - Date.parse(lock.startedAt ?? "");
|
|
647
|
-
if (Number.isFinite(age) && age >= UPDATE_STALE_MS) {
|
|
648
|
-
rmSync(lockPath, { force: true });
|
|
649
|
-
return scheduleWindowsSelfUpdate(options);
|
|
650
|
-
}
|
|
651
|
-
} catch {
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
436
|
throw new Error(
|
|
655
|
-
`Another Nolo update is already pending (${lockPath}): ${error instanceof Error ? error.message : String(error)}
|
|
437
|
+
`Another Nolo update is already pending (${lockPath}): ${error instanceof Error ? error.message : String(error)}
|
|
438
|
+
\u9501\u6587\u4EF6\u91CC\u8BB0\u5F55\u4E86\u6301\u6709\u8005\u7684 updateId \u4E0E\u5F00\u59CB\u65F6\u95F4\uFF1B\u66F4\u65B0\u65E5\u5FD7\u5728 ${updateDir} \u4E0B\uFF0C\u6309\u65F6\u95F4\u6392\u5E8F\u53D6\u6700\u65B0\u90A3\u4E2A\u3002\u786E\u8BA4\u6301\u6709\u5B83\u7684\u66F4\u65B0\u8FDB\u7A0B\u5DF2\u7ED3\u675F\u540E\uFF0C\u5220\u9664\u4E0A\u9762\u7684\u9501\u6587\u4EF6\u518D\u91CD\u8BD5\u3002`
|
|
656
439
|
);
|
|
657
440
|
}
|
|
658
441
|
const execPath = options.execPath ?? process.execPath;
|
|
@@ -663,20 +446,10 @@ function scheduleWindowsSelfUpdate(options) {
|
|
|
663
446
|
removeOwnedLock(lockPath, updateId);
|
|
664
447
|
throw error;
|
|
665
448
|
}
|
|
666
|
-
const pending = {
|
|
667
|
-
status: "pending",
|
|
668
|
-
currentVersion: options.currentVersion,
|
|
669
|
-
channel: options.channel,
|
|
670
|
-
startedAt,
|
|
671
|
-
logPath,
|
|
672
|
-
updateId,
|
|
673
|
-
lockPath
|
|
674
|
-
};
|
|
675
449
|
try {
|
|
676
450
|
writeFileSync(helperPath, buildWindowsUpdateHelperSource(), "utf8");
|
|
677
451
|
writeFileSync(logPath, `[${startedAt}] Windows update scheduled
|
|
678
452
|
`, "utf8");
|
|
679
|
-
writeJsonAtomic(statePath, pending);
|
|
680
453
|
} catch (error) {
|
|
681
454
|
removeOwnedLock(lockPath, updateId);
|
|
682
455
|
rmSync(helperPath, { force: true });
|
|
@@ -684,12 +457,10 @@ function scheduleWindowsSelfUpdate(options) {
|
|
|
684
457
|
}
|
|
685
458
|
const payload = Buffer.from(
|
|
686
459
|
JSON.stringify({
|
|
687
|
-
parentPid: options.parentPid ?? process.pid,
|
|
688
460
|
currentVersion: options.currentVersion,
|
|
689
461
|
channel: options.channel,
|
|
690
462
|
entrypointPath: normalize(options.entrypointPath),
|
|
691
463
|
startedAt,
|
|
692
|
-
statePath,
|
|
693
464
|
logPath,
|
|
694
465
|
updateDir,
|
|
695
466
|
updateId,
|
|
@@ -709,28 +480,11 @@ function scheduleWindowsSelfUpdate(options) {
|
|
|
709
480
|
env
|
|
710
481
|
});
|
|
711
482
|
} catch (error) {
|
|
712
|
-
const state = readWindowsUpdateState(env, homeDir);
|
|
713
|
-
if (state?.updateId === updateId) {
|
|
714
|
-
rmSync(statePath, { force: true });
|
|
715
|
-
}
|
|
716
483
|
removeOwnedLock(lockPath, updateId);
|
|
717
484
|
rmSync(helperPath, { force: true });
|
|
718
485
|
throw error;
|
|
719
486
|
}
|
|
720
|
-
|
|
721
|
-
writeJsonAtomic(statePath, { ...pending, helperPid });
|
|
722
|
-
} catch (error) {
|
|
723
|
-
try {
|
|
724
|
-
writeFileSync(
|
|
725
|
-
logPath,
|
|
726
|
-
`[${(/* @__PURE__ */ new Date()).toISOString()}] Could not persist helper pid: ${error instanceof Error ? error.message : String(error)}
|
|
727
|
-
`,
|
|
728
|
-
{ encoding: "utf8", flag: "a" }
|
|
729
|
-
);
|
|
730
|
-
} catch {
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
return { helperPid, statePath, logPath };
|
|
487
|
+
return { helperPid, logPath };
|
|
734
488
|
}
|
|
735
489
|
|
|
736
490
|
// packages/cli/updateCommands.ts
|
|
@@ -1207,13 +961,13 @@ Nolo Agent CLI Installer & Updater
|
|
|
1207
961
|
`);
|
|
1208
962
|
output.write(`\u2713 Target channel: ${channel} (nolo-cli@${channel})
|
|
1209
963
|
`);
|
|
1210
|
-
output.write(`\u2713
|
|
964
|
+
output.write(`\u2713 Update scheduled in background (helper pid ${scheduled.helperPid}).
|
|
1211
965
|
`);
|
|
1212
|
-
output.write(`\u25B8
|
|
966
|
+
output.write(`\u25B8 You can keep using Nolo \u2014 no need to close this or any other window.
|
|
1213
967
|
`);
|
|
1214
|
-
output.write(`\u25B8
|
|
968
|
+
output.write(`\u25B8 Installation continues in the background; once it finishes, the next Nolo launch uses the new version.
|
|
1215
969
|
`);
|
|
1216
|
-
output.write(` Log: ${scheduled.logPath}
|
|
970
|
+
output.write(` Check version: nolo -v Log: ${scheduled.logPath}
|
|
1217
971
|
|
|
1218
972
|
`);
|
|
1219
973
|
return { exitCode: 0, disposition: "scheduled" };
|
|
@@ -1277,7 +1031,6 @@ async function runSelfUpdate(outputOrOptions) {
|
|
|
1277
1031
|
}
|
|
1278
1032
|
|
|
1279
1033
|
export {
|
|
1280
|
-
consumeWindowsUpdateStartupNotice,
|
|
1281
1034
|
getWindowsUpdateStatus,
|
|
1282
1035
|
getCliInstallChannel,
|
|
1283
1036
|
renderProgressBar,
|
|
@@ -546,7 +546,7 @@ function getSystemInternalCommandEntries(renderHelpText2) {
|
|
|
546
546
|
return runDoctorRuntimeCommand(args, deps);
|
|
547
547
|
}),
|
|
548
548
|
createContextCommand(["doctor"], "Show CLI doctor information", async (_args, ctx) => {
|
|
549
|
-
const { buildCliDoctorText, getCliInstallChannel, resolveSelfUpdateServerUrl } = await import("./updateCommands-
|
|
549
|
+
const { buildCliDoctorText, getCliInstallChannel, resolveSelfUpdateServerUrl } = await import("./updateCommands-BXGFH4VX.js");
|
|
550
550
|
const { isCompiledBinary } = await import("./cliEnvHelpers-BVYTMUPJ.js");
|
|
551
551
|
const serverUrl = resolveSelfUpdateServerUrl(ctx.env);
|
|
552
552
|
console.log(
|
|
@@ -563,7 +563,7 @@ function getSystemInternalCommandEntries(renderHelpText2) {
|
|
|
563
563
|
return 0;
|
|
564
564
|
}),
|
|
565
565
|
createContextCommand(["update"], "Update the CLI", async (_args, ctx) => {
|
|
566
|
-
const { resolveSelfUpdateServerUrl, runSelfUpdate } = await import("./updateCommands-
|
|
566
|
+
const { resolveSelfUpdateServerUrl, runSelfUpdate } = await import("./updateCommands-BXGFH4VX.js");
|
|
567
567
|
return runSelfUpdate({
|
|
568
568
|
entrypointPath: ctx.entrypointPath,
|
|
569
569
|
serverUrl: resolveSelfUpdateServerUrl(ctx.env),
|
|
@@ -576,17 +576,17 @@ function getSystemInternalCommandEntries(renderHelpText2) {
|
|
|
576
576
|
return process.exitCode ?? 0;
|
|
577
577
|
}),
|
|
578
578
|
createContextCommand(["version"], "Show CLI version", async (_args, ctx) => {
|
|
579
|
-
const { buildCliVersionText } = await import("./updateCommands-
|
|
579
|
+
const { buildCliVersionText } = await import("./updateCommands-BXGFH4VX.js");
|
|
580
580
|
console.log(buildCliVersionText(ctx.packageInfo));
|
|
581
581
|
return 0;
|
|
582
582
|
}),
|
|
583
583
|
createContextCommand(["--version"], "Show CLI version", async (_args, ctx) => {
|
|
584
|
-
const { buildCliVersionText } = await import("./updateCommands-
|
|
584
|
+
const { buildCliVersionText } = await import("./updateCommands-BXGFH4VX.js");
|
|
585
585
|
console.log(buildCliVersionText(ctx.packageInfo));
|
|
586
586
|
return 0;
|
|
587
587
|
}),
|
|
588
588
|
createContextCommand(["-v"], "Show CLI version", async (_args, ctx) => {
|
|
589
|
-
const { buildCliVersionText } = await import("./updateCommands-
|
|
589
|
+
const { buildCliVersionText } = await import("./updateCommands-BXGFH4VX.js");
|
|
590
590
|
console.log(buildCliVersionText(ctx.packageInfo));
|
|
591
591
|
return 0;
|
|
592
592
|
}),
|
package/index.js
CHANGED
|
@@ -4,10 +4,9 @@ import {
|
|
|
4
4
|
initializeDiagnostics
|
|
5
5
|
} from "./chunk-EYLGAEB4.js";
|
|
6
6
|
import {
|
|
7
|
-
consumeWindowsUpdateStartupNotice,
|
|
8
7
|
getWindowsUpdateStatus,
|
|
9
8
|
readPackageInfo
|
|
10
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-6MXCQLLB.js";
|
|
11
10
|
import {
|
|
12
11
|
isCompiledBinary
|
|
13
12
|
} from "./chunk-WACI5I6V.js";
|
|
@@ -21,8 +20,34 @@ import "./chunk-G3SUCT4W.js";
|
|
|
21
20
|
import "./chunk-4VNS5WPM.js";
|
|
22
21
|
|
|
23
22
|
// packages/cli/index.ts
|
|
24
|
-
import { dirname, join } from "node:path";
|
|
23
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
25
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
// packages/cli/versionChangeNotice.ts
|
|
27
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { homedir } from "node:os";
|
|
29
|
+
import { dirname, join } from "node:path";
|
|
30
|
+
var resolveNoticePath = (env, homeDir) => join(env.NOLO_HOME?.trim() || join(homeDir, ".nolo"), "cli", "last-run-version");
|
|
31
|
+
function takeVersionChangeNotice(currentVersion, options = {}) {
|
|
32
|
+
const env = options.env ?? process.env;
|
|
33
|
+
const homeDir = options.homeDir ?? homedir();
|
|
34
|
+
const noticePath = resolveNoticePath(env, homeDir);
|
|
35
|
+
let previous = "";
|
|
36
|
+
try {
|
|
37
|
+
previous = readFileSync(noticePath, "utf8").trim();
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
if (previous === currentVersion) return null;
|
|
41
|
+
try {
|
|
42
|
+
mkdirSync(dirname(noticePath), { recursive: true });
|
|
43
|
+
writeFileSync(noticePath, `${currentVersion}
|
|
44
|
+
`, "utf8");
|
|
45
|
+
} catch {
|
|
46
|
+
}
|
|
47
|
+
return previous ? { from: previous, to: currentVersion } : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// packages/cli/index.ts
|
|
26
51
|
var cliArgs = process.argv.slice(2);
|
|
27
52
|
var diagnostics = initializeDiagnostics(detectMode(cliArgs, process.env), process.env);
|
|
28
53
|
var logger = diagnostics.logger;
|
|
@@ -41,26 +66,26 @@ if (process.platform === "win32") {
|
|
|
41
66
|
`);
|
|
42
67
|
process.exit(0);
|
|
43
68
|
}
|
|
44
|
-
const updateNotice = consumeWindowsUpdateStartupNotice(process.env, {
|
|
45
|
-
ownerUpdateId: process.env.NOLO_UPDATE_OWNER_ID
|
|
46
|
-
});
|
|
47
|
-
if (updateNotice) {
|
|
48
|
-
process.stderr.write(`${updateNotice.text}
|
|
49
|
-
`);
|
|
50
|
-
if (updateNotice.blocking) {
|
|
51
|
-
process.exit(75);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
69
|
}
|
|
55
|
-
var SOURCE_CLI_DIR =
|
|
56
|
-
var CLI_DIR = isCompiledBinary() ?
|
|
57
|
-
var ROOT_DIR =
|
|
58
|
-
var SCRIPT_DIR =
|
|
70
|
+
var SOURCE_CLI_DIR = dirname2(fileURLToPath(import.meta.url));
|
|
71
|
+
var CLI_DIR = isCompiledBinary() ? dirname2(process.execPath) : SOURCE_CLI_DIR;
|
|
72
|
+
var ROOT_DIR = join2(CLI_DIR, "..", "..");
|
|
73
|
+
var SCRIPT_DIR = join2(ROOT_DIR, "scripts");
|
|
59
74
|
var packageInfo = readPackageInfo();
|
|
75
|
+
var isScriptLikeArgs = cliArgs[0] === "update" || cliArgs[0] === "-v" || cliArgs[0] === "--version" || cliArgs.includes("--json");
|
|
76
|
+
if (process.stdout.isTTY && !isScriptLikeArgs) {
|
|
77
|
+
const change = takeVersionChangeNotice(packageInfo.version);
|
|
78
|
+
if (change) {
|
|
79
|
+
process.stderr.write(
|
|
80
|
+
`\u25B8 Nolo version changed: ${change.from} \u2192 ${change.to}
|
|
81
|
+
`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
60
85
|
var args = cliArgs;
|
|
61
86
|
async function runScript(script, forwardedArgs, env) {
|
|
62
87
|
const { spawnProcess } = await import("./processSpawn-SOAV3FEV.js");
|
|
63
|
-
const scriptPath =
|
|
88
|
+
const scriptPath = join2(SCRIPT_DIR, script);
|
|
64
89
|
const proc = spawnProcess({
|
|
65
90
|
cmd: [process.execPath, scriptPath, ...forwardedArgs],
|
|
66
91
|
stdin: "inherit",
|
|
@@ -71,7 +96,7 @@ async function runScript(script, forwardedArgs, env) {
|
|
|
71
96
|
return proc.exited;
|
|
72
97
|
}
|
|
73
98
|
async function launchTuiWorkspace(args2) {
|
|
74
|
-
const { startTuiWorkspace } = await import("./readlineWorkspace-
|
|
99
|
+
const { startTuiWorkspace } = await import("./readlineWorkspace-PSJS3ZS2.js");
|
|
75
100
|
const { createTuiSummaryLlmCaller } = await import("./tuiSummaryLlmCaller-A6T54UJ3.js");
|
|
76
101
|
const { setCliAutoCompactionSummaryFallback } = await import("./localRuntimeAdapter-CQH4RFZC.js");
|
|
77
102
|
setCliAutoCompactionSummaryFallback(
|
|
@@ -85,7 +110,7 @@ async function launchTuiWorkspace(args2) {
|
|
|
85
110
|
});
|
|
86
111
|
}
|
|
87
112
|
async function main() {
|
|
88
|
-
const { createCliRuntimeContext, looksLikeDaemonShortcut, renderHelpText, resolveCommand, runResolvedCommand } = await import("./commandRegistry-
|
|
113
|
+
const { createCliRuntimeContext, looksLikeDaemonShortcut, renderHelpText, resolveCommand, runResolvedCommand } = await import("./commandRegistry-OW2MIVIT.js");
|
|
89
114
|
const { buildCliRuntimeEnv, loadProfileConfig } = await import("./profileConfig-KN3N2BLF.js");
|
|
90
115
|
const { resolveTuiLaunchMode } = await import("./runtimeModeArgs-2Y7BALTO.js");
|
|
91
116
|
const runtimeEnv = {
|
package/package.json
CHANGED