nolo-cli 0.35.0-alpha.1 → 0.36.0-alpha.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/{_executeModel-RV52PPE3.js → _executeModel-2S6HNTBF.js} +11 -11
- package/{agentDeleteCommand-FBBW3IIM.js → agentDeleteCommand-EJ6RRIGV.js} +2 -2
- package/{agentEmailCommands-7F4PCABZ.js → agentEmailCommands-APU6G6PK.js} +11 -11
- package/{agentListCommands-ZQXPSB6U.js → agentListCommands-HSWVSDLI.js} +2 -2
- package/{agentMachineCommands-E3NB67GM.js → agentMachineCommands-G7JQUUDC.js} +3 -3
- package/{agentRecordCommands-7PXWHCQM.js → agentRecordCommands-PUFDQBQ5.js} +11 -11
- package/{agentRunCommand-Z4654ONK.js → agentRunCommand-AS4N52UF.js} +11 -11
- package/{agentRuntimeLocal-OBKWKEKW.js → agentRuntimeLocal-GTNYV7LH.js} +2 -2
- package/{agentSlice-B2IMSPH5.js → agentSlice-LAILHBMM.js} +1 -1
- package/{appDeleteCommands-72BE7DSA.js → appDeleteCommands-YDXSR75I.js} +2 -2
- package/{appDeployCommands-XJBLKCYO.js → appDeployCommands-4X5SN3EF.js} +2 -2
- package/{appDeployStatusCommands-EC4HWCBQ.js → appDeployStatusCommands-TK3JKBHH.js} +2 -2
- package/{appGetCommands-62BAATPL.js → appGetCommands-ZKQHHJYB.js} +2 -2
- package/{appListCommands-5EZCONM7.js → appListCommands-JRDOEEGV.js} +2 -2
- package/{chatQueueReduxAdapter-SSF3DNBX.js → chatQueueReduxAdapter-DQWFZYNL.js} +2 -2
- package/{chunk-ZOEE4OVX.js → chunk-2BKJBSQ4.js} +9 -5
- package/{chunk-Z7D7MBWR.js → chunk-3SNGK3FB.js} +1 -1
- package/{chunk-7KY6U7MW.js → chunk-ARFXKWJH.js} +8 -8
- package/{chunk-55C3UZSC.js → chunk-CVXDBLYH.js} +3 -3
- package/{chunk-OCJNJUR4.js → chunk-D72M3G32.js} +2 -2
- package/{chunk-KPJYUEPE.js → chunk-EJAL5LSG.js} +1 -1
- package/{chunk-RUVWTAK7.js → chunk-FXZBSLFK.js} +4 -4
- package/{chunk-LFOILL6R.js → chunk-HRSIJO5A.js} +1 -1
- package/{chunk-5Q4DXT47.js → chunk-JK7UM3LT.js} +9 -9
- package/{chunk-INXNBCEY.js → chunk-KR74XLMV.js} +2 -2
- package/{chunk-EFN2AFWF.js → chunk-LBZHTQTD.js} +164 -10
- package/{chunk-P62APAXL.js → chunk-NT3QRCVS.js} +2 -2
- package/{chunk-OGPQ3AMC.js → chunk-RPPRK6EZ.js} +6 -6
- package/{chunk-M3JQWYBC.js → chunk-T2BZICLK.js} +3 -1
- package/chunk-T5HJEUQN.js +956 -0
- package/{chunk-SDAPQUSO.js → chunk-YNAGAUJT.js} +6 -6
- package/{chunk-ZZDWMZFU.js → chunk-ZQ3IDY66.js} +3 -3
- package/{clipboardy-PRUHGPGC.js → clipboardy-BX4NMYHE.js} +7 -5
- package/{createAgentAutomationAction-IDIY6K45.js → createAgentAutomationAction-767ZITQJ.js} +3 -3
- package/{createDialogAction-5LB4YBPS.js → createDialogAction-H75NDXYH.js} +4 -4
- package/{createPageAction-TJWVDPZD.js → createPageAction-KYJYZPLC.js} +3 -3
- package/{dialogCommands-44NGBPJJ.js → dialogCommands-VYH2BY5H.js} +2 -2
- package/{dialogSlice-62JC4ZMX.js → dialogSlice-YJV3AVFM.js} +2 -2
- package/{docCreateCommands-IOGYJ7F7.js → docCreateCommands-JS7A3OHQ.js} +2 -2
- package/{docDeleteCommands-RO4BZWFP.js → docDeleteCommands-HG5HNKZR.js} +2 -2
- package/{docReadCommands-F2KNOJFW.js → docReadCommands-BH4WN6HH.js} +2 -2
- package/{docStore-ZRNKZZXU.js → docStore-JOODSWDW.js} +4 -4
- package/{docUpdateCommands-2VG4AYOC.js → docUpdateCommands-TAXC7LHJ.js} +2 -2
- package/{handleSendMessageAction-YB64A32O.js → handleSendMessageAction-DGL6H4E4.js} +2 -2
- package/index.js +59 -46
- package/{machineCommands-6WPICTON.js → machineCommands-DSFTSYUL.js} +11 -11
- package/{machineWsRunDispatch-4OGEZTYG.js → machineWsRunDispatch-ZOAJQHTU.js} +11 -11
- package/package.json +1 -1
- package/{readlineWorkspace-BZAZ5YXM.js → readlineWorkspace-5G3UFHYG.js} +53 -30
- package/{runAgentClientLoop-SOZS3YTQ.js → runAgentClientLoop-B3TTKXIX.js} +9 -9
- package/{spaceCommands-WEASK3IV.js → spaceCommands-Y33JLAKF.js} +2 -2
- package/{spaceSlice-2QNUKRN6.js → spaceSlice-ZKUZSVYD.js} +3 -3
- package/{streamAgentChatTurn-BYI7UXAA.js → streamAgentChatTurn-PLTB7RMV.js} +10 -10
- package/{tools-E6FEUNZY.js → tools-KQXDW66A.js} +6 -6
- package/{updateCommands-MNDUIKN7.js → updateCommands-IYKLF3JX.js} +3 -1
- package/{updateDialogTitleAction-2A3BEXSP.js → updateDialogTitleAction-OTYEQJAK.js} +3 -3
- package/chunk-VFVCYIRQ.js +0 -407
|
@@ -0,0 +1,956 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadProfileConfig
|
|
3
|
+
} from "./chunk-UNZDCI3O.js";
|
|
4
|
+
import {
|
|
5
|
+
resolveDefaultSpawn
|
|
6
|
+
} from "./chunk-IDZTNRP2.js";
|
|
7
|
+
import {
|
|
8
|
+
isCompiledBinary
|
|
9
|
+
} from "./chunk-A7GB5UGH.js";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_NOLO_SERVER_URL
|
|
12
|
+
} from "./chunk-OLZBQWZU.js";
|
|
13
|
+
import {
|
|
14
|
+
normalizeServerOrigin
|
|
15
|
+
} from "./chunk-CF7PE763.js";
|
|
16
|
+
|
|
17
|
+
// packages/cli/updateCommands.ts
|
|
18
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, accessSync, constants } from "node:fs";
|
|
19
|
+
import { homedir as homedir2 } from "node:os";
|
|
20
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
// packages/cli/windowsSelfUpdate.ts
|
|
24
|
+
import { spawn } from "node:child_process";
|
|
25
|
+
import {
|
|
26
|
+
closeSync,
|
|
27
|
+
existsSync,
|
|
28
|
+
mkdirSync,
|
|
29
|
+
openSync,
|
|
30
|
+
readFileSync,
|
|
31
|
+
renameSync,
|
|
32
|
+
rmSync,
|
|
33
|
+
writeSync,
|
|
34
|
+
writeFileSync
|
|
35
|
+
} from "node:fs";
|
|
36
|
+
import { homedir, tmpdir } from "node:os";
|
|
37
|
+
import { dirname, isAbsolute, join, normalize } from "node:path";
|
|
38
|
+
import { randomUUID } from "node:crypto";
|
|
39
|
+
var UPDATE_STATE_FILE = "update-result.json";
|
|
40
|
+
var UPDATE_STALE_MS = 15 * 60 * 1e3;
|
|
41
|
+
var UPDATE_LOCK_FILE = "update.lock";
|
|
42
|
+
function resolveNoloHome(env, homeDir = homedir()) {
|
|
43
|
+
return env.NOLO_HOME?.trim() || join(homeDir, ".nolo");
|
|
44
|
+
}
|
|
45
|
+
function resolveWindowsUpdateStatePath(env = process.env, homeDir = homedir()) {
|
|
46
|
+
return join(resolveNoloHome(env, homeDir), "updates", UPDATE_STATE_FILE);
|
|
47
|
+
}
|
|
48
|
+
function resolveTrustedWindowsToolPaths(env, execPath, overrides) {
|
|
49
|
+
const nodePath = overrides.nodePath ?? execPath;
|
|
50
|
+
const npmCliPath = overrides.npmCliPath ?? join(dirname(nodePath), "node_modules", "npm", "bin", "npm-cli.js");
|
|
51
|
+
const windowsRoot = env.SystemRoot?.trim() || env.WINDIR?.trim();
|
|
52
|
+
const powershellPath = overrides.powershellPath ?? (windowsRoot ? join(
|
|
53
|
+
windowsRoot,
|
|
54
|
+
"System32",
|
|
55
|
+
"WindowsPowerShell",
|
|
56
|
+
"v1.0",
|
|
57
|
+
"powershell.exe"
|
|
58
|
+
) : "");
|
|
59
|
+
for (const [label, path] of Object.entries({
|
|
60
|
+
nodePath,
|
|
61
|
+
npmCliPath,
|
|
62
|
+
powershellPath
|
|
63
|
+
})) {
|
|
64
|
+
if (!path || !isAbsolute(path) || !existsSync(path)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Could not resolve trusted ${label}: ${path || "missing"}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { nodePath, npmCliPath, powershellPath };
|
|
71
|
+
}
|
|
72
|
+
function removeOwnedLock(lockPath, updateId) {
|
|
73
|
+
try {
|
|
74
|
+
const raw = readFileSync(lockPath, "utf8").trim();
|
|
75
|
+
const owner = raw.startsWith("{") ? JSON.parse(raw).updateId : raw;
|
|
76
|
+
if (owner === updateId) {
|
|
77
|
+
rmSync(lockPath, { force: true });
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function writeJsonAtomic(path, value) {
|
|
83
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
84
|
+
const tempPath = `${path}.${process.pid}.tmp`;
|
|
85
|
+
writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}
|
|
86
|
+
`, "utf8");
|
|
87
|
+
renameSync(tempPath, path);
|
|
88
|
+
}
|
|
89
|
+
function readWindowsUpdateState(env = process.env, homeDir = homedir()) {
|
|
90
|
+
const statePath = resolveWindowsUpdateStatePath(env, homeDir);
|
|
91
|
+
if (!existsSync(statePath)) return null;
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(
|
|
94
|
+
readFileSync(statePath, "utf8")
|
|
95
|
+
);
|
|
96
|
+
if (!parsed || !["pending", "success", "failed"].includes(parsed.status)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function consumeWindowsUpdateStartupNotice(env = process.env, options = {}) {
|
|
105
|
+
const homeDir = options.homeDir ?? homedir();
|
|
106
|
+
const statePath = resolveWindowsUpdateStatePath(env, homeDir);
|
|
107
|
+
const state = readWindowsUpdateState(env, homeDir);
|
|
108
|
+
if (!state) return null;
|
|
109
|
+
if (state.status === "pending") {
|
|
110
|
+
if (options.ownerUpdateId && state.updateId && options.ownerUpdateId === state.updateId) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const startedAt = Date.parse(state.startedAt);
|
|
114
|
+
const age = (options.now ?? Date.now()) - startedAt;
|
|
115
|
+
if (!Number.isFinite(age) || age < UPDATE_STALE_MS) {
|
|
116
|
+
return {
|
|
117
|
+
blocking: true,
|
|
118
|
+
text: `Nolo update is still running. Try again shortly.
|
|
119
|
+
Log: ${state.logPath}`
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (state.lockPath && state.updateId) {
|
|
123
|
+
removeOwnedLock(state.lockPath, state.updateId);
|
|
124
|
+
}
|
|
125
|
+
rmSync(statePath, { force: true });
|
|
126
|
+
return {
|
|
127
|
+
blocking: false,
|
|
128
|
+
text: `The previous Nolo update helper stopped unexpectedly. Verify with "nolo -v"; a manual npm repair may be required.
|
|
129
|
+
Log: ${state.logPath}`
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
rmSync(statePath, { force: true });
|
|
133
|
+
if (state.status === "success") {
|
|
134
|
+
return {
|
|
135
|
+
blocking: false,
|
|
136
|
+
text: `Nolo updated successfully: ${state.currentVersion} -> ${state.targetVersion ?? "latest"}.
|
|
137
|
+
Log: ${state.logPath}`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
blocking: false,
|
|
142
|
+
text: `Nolo update failed. Verify with "nolo -v"; npm may require a manual repair.${state.message ? `
|
|
143
|
+
${state.message}` : ""}
|
|
144
|
+
Log: ${state.logPath}`
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function defaultLaunchWindowsUpdateHelper(input) {
|
|
148
|
+
const child = spawn(input.execPath, [input.helperPath, input.payload], {
|
|
149
|
+
detached: true,
|
|
150
|
+
stdio: "ignore",
|
|
151
|
+
windowsHide: true,
|
|
152
|
+
env: input.env
|
|
153
|
+
});
|
|
154
|
+
child.unref();
|
|
155
|
+
if (!child.pid) {
|
|
156
|
+
throw new Error("Windows update helper did not start");
|
|
157
|
+
}
|
|
158
|
+
return child.pid;
|
|
159
|
+
}
|
|
160
|
+
function buildWindowsUpdateHelperSource() {
|
|
161
|
+
return String.raw`"use strict";
|
|
162
|
+
const { spawnSync } = require("node:child_process");
|
|
163
|
+
const { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } = require("node:fs");
|
|
164
|
+
const { dirname, isAbsolute, join, normalize, relative } = require("node:path");
|
|
165
|
+
|
|
166
|
+
const payload = JSON.parse(Buffer.from(process.argv[2], "base64url").toString("utf8"));
|
|
167
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
168
|
+
const log = (line) => appendFileSync(payload.logPath, "[" + new Date().toISOString() + "] " + line + "\n", "utf8");
|
|
169
|
+
const writeState = (next) => {
|
|
170
|
+
mkdirSync(dirname(payload.statePath), { recursive: true });
|
|
171
|
+
const temp = payload.statePath + "." + payload.updateId + ".tmp";
|
|
172
|
+
writeFileSync(temp, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
173
|
+
renameSync(temp, payload.statePath);
|
|
174
|
+
};
|
|
175
|
+
const runNpm = (args) => {
|
|
176
|
+
const command = [payload.nodePath, payload.npmCliPath, ...args].join(" ");
|
|
177
|
+
log("> " + command);
|
|
178
|
+
const result = spawnSync(payload.nodePath, [payload.npmCliPath, ...args], {
|
|
179
|
+
encoding: "utf8",
|
|
180
|
+
windowsHide: true,
|
|
181
|
+
env: process.env,
|
|
182
|
+
cwd: payload.updateDir,
|
|
183
|
+
timeout: args[0] === "install" ? 5 * 60 * 1000 : 30 * 1000,
|
|
184
|
+
});
|
|
185
|
+
if (result.stdout) appendFileSync(payload.logPath, result.stdout, "utf8");
|
|
186
|
+
if (result.stderr) appendFileSync(payload.logPath, result.stderr, "utf8");
|
|
187
|
+
if (result.error) throw result.error;
|
|
188
|
+
return { code: result.status == null ? 1 : result.status, stdout: String(result.stdout || "").trim() };
|
|
189
|
+
};
|
|
190
|
+
const listOtherNoloPids = () => {
|
|
191
|
+
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 }";
|
|
192
|
+
const result = spawnSync(payload.powershellPath, ["-NoProfile", "-NonInteractive", "-Command", script], {
|
|
193
|
+
encoding: "utf8",
|
|
194
|
+
windowsHide: true,
|
|
195
|
+
env: process.env,
|
|
196
|
+
cwd: payload.updateDir,
|
|
197
|
+
timeout: 10 * 1000,
|
|
198
|
+
});
|
|
199
|
+
if (result.error || result.status !== 0) {
|
|
200
|
+
throw new Error("Could not verify that all other Nolo windows are closed");
|
|
201
|
+
}
|
|
202
|
+
return String(result.stdout || "").split(/\r?\n/).map((value) => value.trim()).filter(Boolean);
|
|
203
|
+
};
|
|
204
|
+
const parseVersion = (raw) => {
|
|
205
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(String(raw).trim());
|
|
206
|
+
return match ? { main: [Number(match[1]), Number(match[2]), Number(match[3])], pre: match[4] ? match[4].split(".") : [] } : null;
|
|
207
|
+
};
|
|
208
|
+
const compare = (a, b) => {
|
|
209
|
+
const pa = parseVersion(a); const pb = parseVersion(b);
|
|
210
|
+
if (!pa || !pb) throw new Error("Invalid version comparison: " + a + " / " + b);
|
|
211
|
+
for (let i = 0; i < 3; i++) if (pa.main[i] !== pb.main[i]) return pa.main[i] < pb.main[i] ? -1 : 1;
|
|
212
|
+
if (!pa.pre.length && !pb.pre.length) return 0;
|
|
213
|
+
if (!pa.pre.length) return 1;
|
|
214
|
+
if (!pb.pre.length) return -1;
|
|
215
|
+
for (let i = 0; i < Math.max(pa.pre.length, pb.pre.length); i++) {
|
|
216
|
+
const x = pa.pre[i], y = pb.pre[i];
|
|
217
|
+
if (x === undefined) return -1; if (y === undefined) return 1; if (x === y) continue;
|
|
218
|
+
const xn = /^\d+$/.test(x) ? Number(x) : NaN, yn = /^\d+$/.test(y) ? Number(y) : NaN;
|
|
219
|
+
if (!Number.isNaN(xn) && !Number.isNaN(yn)) return xn < yn ? -1 : 1;
|
|
220
|
+
if (!Number.isNaN(xn)) return -1; if (!Number.isNaN(yn)) return 1;
|
|
221
|
+
return x < y ? -1 : 1;
|
|
222
|
+
}
|
|
223
|
+
return 0;
|
|
224
|
+
};
|
|
225
|
+
const isWithin = (root, candidate) => {
|
|
226
|
+
const rel = relative(normalize(root), normalize(candidate));
|
|
227
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
228
|
+
};
|
|
229
|
+
const quoteCmd = (value) => '"' + String(value).replace(/"/g, '""') + '"';
|
|
230
|
+
const quotePowerShell = (value) => "'" + String(value).replace(/'/g, "''") + "'";
|
|
231
|
+
const quoteSh = (value) => "'" + String(value).replace(/'/g, "'\\''") + "'";
|
|
232
|
+
const switchWindowsShims = (shimDir, entrypoint) => {
|
|
233
|
+
const shims = [
|
|
234
|
+
{ path: join(shimDir, "nolo.cmd"), content: "@ECHO off\r\n" + quoteCmd(payload.nodePath) + " " + quoteCmd(entrypoint) + " %*\r\n" },
|
|
235
|
+
{ path: join(shimDir, "nolo.ps1"), content: "& " + quotePowerShell(payload.nodePath) + " " + quotePowerShell(entrypoint) + " $args\r\nexit $LASTEXITCODE\r\n" },
|
|
236
|
+
{ path: join(shimDir, "nolo"), content: "#!/bin/sh\nexec " + quoteSh(payload.nodePath.replace(/\\/g, "/")) + " " + quoteSh(entrypoint.replace(/\\/g, "/")) + " \"$@\"\n" },
|
|
237
|
+
];
|
|
238
|
+
mkdirSync(shimDir, { recursive: true });
|
|
239
|
+
const prepared = shims.map((shim) => {
|
|
240
|
+
const temp = shim.path + "." + payload.updateId + ".new";
|
|
241
|
+
const backup = shim.path + "." + payload.updateId + ".bak";
|
|
242
|
+
writeFileSync(temp, shim.content, "utf8");
|
|
243
|
+
return { ...shim, temp, backup, existed: existsSync(shim.path) };
|
|
244
|
+
});
|
|
245
|
+
const activated = [];
|
|
246
|
+
try {
|
|
247
|
+
for (const shim of prepared) {
|
|
248
|
+
if (shim.existed) renameSync(shim.path, shim.backup);
|
|
249
|
+
try {
|
|
250
|
+
renameSync(shim.temp, shim.path);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (shim.existed) {
|
|
253
|
+
try { renameSync(shim.backup, shim.path); } catch {}
|
|
254
|
+
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
try { chmodSync(shim.path, 0o755); } catch {}
|
|
258
|
+
activated.push(shim);
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
for (const shim of [...activated].reverse()) {
|
|
262
|
+
try { rmSync(shim.path, { force: true }); } catch {}
|
|
263
|
+
if (shim.existed) {
|
|
264
|
+
try { renameSync(shim.backup, shim.path); } catch {}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const shim of prepared) try { rmSync(shim.temp, { force: true }); } catch {}
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
rollback: () => {
|
|
272
|
+
for (const shim of [...activated].reverse()) {
|
|
273
|
+
try { rmSync(shim.path, { force: true }); } catch {}
|
|
274
|
+
if (shim.existed) {
|
|
275
|
+
try { renameSync(shim.backup, shim.path); } catch {}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
commit: () => {
|
|
280
|
+
for (const shim of prepared) {
|
|
281
|
+
try { rmSync(shim.backup, { force: true }); } catch {}
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
(async () => {
|
|
288
|
+
let uncommittedVersionPrefix = null;
|
|
289
|
+
const base = {
|
|
290
|
+
status: "pending",
|
|
291
|
+
currentVersion: payload.currentVersion,
|
|
292
|
+
channel: payload.channel,
|
|
293
|
+
startedAt: payload.startedAt,
|
|
294
|
+
logPath: payload.logPath,
|
|
295
|
+
helperPid: process.pid,
|
|
296
|
+
updateId: payload.updateId,
|
|
297
|
+
lockPath: payload.lockPath,
|
|
298
|
+
};
|
|
299
|
+
try {
|
|
300
|
+
log("Waiting for parent process " + payload.parentPid + " to exit");
|
|
301
|
+
let parentExited = false;
|
|
302
|
+
for (let waited = 0; waited < 480; waited++) {
|
|
303
|
+
try { process.kill(payload.parentPid, 0); await delay(250); }
|
|
304
|
+
catch { parentExited = true; break; }
|
|
305
|
+
}
|
|
306
|
+
if (!parentExited) throw new Error("Timed out waiting for the current Nolo process to exit");
|
|
307
|
+
await delay(250);
|
|
308
|
+
|
|
309
|
+
const otherProcessDeadline = Date.now() + 120 * 1000;
|
|
310
|
+
let otherPids = listOtherNoloPids();
|
|
311
|
+
let checks = 0;
|
|
312
|
+
while (otherPids.length > 0 && Date.now() < otherProcessDeadline) {
|
|
313
|
+
if (checks === 0 || checks % 10 === 0) log("Waiting for other Nolo processes: " + otherPids.join(", "));
|
|
314
|
+
await delay(1000);
|
|
315
|
+
otherPids = listOtherNoloPids();
|
|
316
|
+
checks++;
|
|
317
|
+
}
|
|
318
|
+
if (otherPids.length > 0) {
|
|
319
|
+
throw new Error("Other Nolo processes are still running: " + otherPids.join(", "));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const prefixResult = runNpm(["prefix", "-g"]);
|
|
323
|
+
if (prefixResult.code !== 0 || !prefixResult.stdout) throw new Error("Could not resolve npm global prefix");
|
|
324
|
+
const prefix = prefixResult.stdout.split(/\r?\n/).at(-1).trim();
|
|
325
|
+
const expectedRoot = normalize(join(prefix, "node_modules", "nolo-cli")).toLowerCase();
|
|
326
|
+
const managedRoot = normalize(payload.managedRoot).toLowerCase();
|
|
327
|
+
const oldEntrypoint = normalize(payload.entrypointPath).toLowerCase();
|
|
328
|
+
if (!isWithin(expectedRoot, oldEntrypoint) && !isWithin(managedRoot, oldEntrypoint)) {
|
|
329
|
+
throw new Error("Refusing to update a different npm prefix. Current entrypoint: " + payload.entrypointPath + "; npm prefix: " + prefix);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const targetResult = runNpm(["view", "nolo-cli@" + payload.channel, "version"]);
|
|
333
|
+
if (targetResult.code !== 0 || !parseVersion(targetResult.stdout)) {
|
|
334
|
+
throw new Error("Could not resolve nolo-cli@" + payload.channel + " target version");
|
|
335
|
+
}
|
|
336
|
+
const targetVersion = targetResult.stdout.split(/\r?\n/).at(-1).trim();
|
|
337
|
+
if (compare(targetVersion, payload.currentVersion) < 0) {
|
|
338
|
+
throw new Error("Refusing to downgrade " + payload.currentVersion + " to " + targetVersion);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (compare(targetVersion, payload.currentVersion) > 0) {
|
|
342
|
+
const versionPrefix = join(payload.managedRoot, targetVersion + "-" + payload.updateId);
|
|
343
|
+
uncommittedVersionPrefix = versionPrefix;
|
|
344
|
+
mkdirSync(versionPrefix, { recursive: true });
|
|
345
|
+
const install = runNpm(["install", "-g", "--prefix", versionPrefix, "nolo-cli@" + payload.channel, "--force", "--progress"]);
|
|
346
|
+
if (install.code !== 0) throw new Error("npm install exited with code " + install.code);
|
|
347
|
+
const stagedPackagePath = join(versionPrefix, "node_modules", "nolo-cli", "package.json");
|
|
348
|
+
const staged = JSON.parse(readFileSync(stagedPackagePath, "utf8"));
|
|
349
|
+
if (staged.version !== targetVersion) {
|
|
350
|
+
throw new Error("Staged version mismatch: expected " + targetVersion + ", got " + (staged.version || "unknown"));
|
|
351
|
+
}
|
|
352
|
+
const stagedEntrypoint = join(versionPrefix, "node_modules", "nolo-cli", "index.js");
|
|
353
|
+
if (!existsSync(stagedEntrypoint)) throw new Error("Staged nolo entrypoint is missing");
|
|
354
|
+
const switchedShims = switchWindowsShims(prefix, stagedEntrypoint);
|
|
355
|
+
const verify = spawnSync(payload.nodePath, [stagedEntrypoint, "-v"], {
|
|
356
|
+
encoding: "utf8",
|
|
357
|
+
windowsHide: true,
|
|
358
|
+
env: { ...process.env, NOLO_UPDATE_OWNER_ID: payload.updateId },
|
|
359
|
+
cwd: payload.updateDir,
|
|
360
|
+
timeout: 30 * 1000,
|
|
361
|
+
});
|
|
362
|
+
if (verify.error || verify.status !== 0 || !String(verify.stdout || "").includes(targetVersion)) {
|
|
363
|
+
switchedShims.rollback();
|
|
364
|
+
throw new Error("The staged CLI failed its post-switch version check; the previous launch shims were restored");
|
|
365
|
+
}
|
|
366
|
+
switchedShims.commit();
|
|
367
|
+
uncommittedVersionPrefix = null;
|
|
368
|
+
for (const entry of readdirSync(payload.managedRoot)) {
|
|
369
|
+
const candidate = join(payload.managedRoot, entry);
|
|
370
|
+
if (normalize(candidate) !== normalize(versionPrefix)) {
|
|
371
|
+
try { rmSync(candidate, { recursive: true, force: true }); } catch {}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
log("Verified nolo-cli " + targetVersion + " at " + stagedEntrypoint);
|
|
375
|
+
} else {
|
|
376
|
+
log("Already current at " + targetVersion);
|
|
377
|
+
}
|
|
378
|
+
writeState({ ...base, status: "success", targetVersion, finishedAt: new Date().toISOString() });
|
|
379
|
+
} catch (error) {
|
|
380
|
+
const message = error && error.message ? error.message : String(error);
|
|
381
|
+
log("ERROR: " + message);
|
|
382
|
+
if (uncommittedVersionPrefix) {
|
|
383
|
+
try { rmSync(uncommittedVersionPrefix, { recursive: true, force: true }); } catch {}
|
|
384
|
+
}
|
|
385
|
+
writeState({ ...base, status: "failed", finishedAt: new Date().toISOString(), message });
|
|
386
|
+
process.exitCode = 1;
|
|
387
|
+
} finally {
|
|
388
|
+
try {
|
|
389
|
+
const lock = JSON.parse(readFileSync(payload.lockPath, "utf8"));
|
|
390
|
+
if (lock.updateId === payload.updateId) {
|
|
391
|
+
rmSync(payload.lockPath, { force: true });
|
|
392
|
+
}
|
|
393
|
+
} catch {}
|
|
394
|
+
try { rmSync(__filename, { force: true }); } catch {}
|
|
395
|
+
}
|
|
396
|
+
})();
|
|
397
|
+
`;
|
|
398
|
+
}
|
|
399
|
+
function scheduleWindowsSelfUpdate(options) {
|
|
400
|
+
const env = options.env ?? process.env;
|
|
401
|
+
const homeDir = options.homeDir ?? homedir();
|
|
402
|
+
const statePath = resolveWindowsUpdateStatePath(env, homeDir);
|
|
403
|
+
const updateDir = dirname(statePath);
|
|
404
|
+
const managedRoot = join(resolveNoloHome(env, homeDir), "cli", "versions");
|
|
405
|
+
const lockPath = join(updateDir, UPDATE_LOCK_FILE);
|
|
406
|
+
const updateId = randomUUID();
|
|
407
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
408
|
+
const logPath = join(updateDir, `update-${stamp}.log`);
|
|
409
|
+
const helperPath = join(
|
|
410
|
+
options.tempDir ?? tmpdir(),
|
|
411
|
+
`nolo-update-${randomUUID()}.cjs`
|
|
412
|
+
);
|
|
413
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
414
|
+
mkdirSync(updateDir, { recursive: true });
|
|
415
|
+
mkdirSync(dirname(helperPath), { recursive: true });
|
|
416
|
+
try {
|
|
417
|
+
const lockFd = openSync(lockPath, "wx");
|
|
418
|
+
try {
|
|
419
|
+
writeSync(
|
|
420
|
+
lockFd,
|
|
421
|
+
JSON.stringify({ updateId, startedAt }),
|
|
422
|
+
void 0,
|
|
423
|
+
"utf8"
|
|
424
|
+
);
|
|
425
|
+
} finally {
|
|
426
|
+
closeSync(lockFd);
|
|
427
|
+
}
|
|
428
|
+
} catch (error) {
|
|
429
|
+
if (!readWindowsUpdateState(env, homeDir)) {
|
|
430
|
+
try {
|
|
431
|
+
const lock = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
432
|
+
const age = Date.now() - Date.parse(lock.startedAt ?? "");
|
|
433
|
+
if (Number.isFinite(age) && age >= UPDATE_STALE_MS) {
|
|
434
|
+
rmSync(lockPath, { force: true });
|
|
435
|
+
return scheduleWindowsSelfUpdate(options);
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
throw new Error(
|
|
441
|
+
`Another Nolo update is already pending (${lockPath}): ${error instanceof Error ? error.message : String(error)}`
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
const execPath = options.execPath ?? process.execPath;
|
|
445
|
+
let trustedTools;
|
|
446
|
+
try {
|
|
447
|
+
trustedTools = resolveTrustedWindowsToolPaths(env, execPath, options);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
removeOwnedLock(lockPath, updateId);
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
452
|
+
const pending = {
|
|
453
|
+
status: "pending",
|
|
454
|
+
currentVersion: options.currentVersion,
|
|
455
|
+
channel: options.channel,
|
|
456
|
+
startedAt,
|
|
457
|
+
logPath,
|
|
458
|
+
updateId,
|
|
459
|
+
lockPath
|
|
460
|
+
};
|
|
461
|
+
try {
|
|
462
|
+
writeFileSync(helperPath, buildWindowsUpdateHelperSource(), "utf8");
|
|
463
|
+
writeFileSync(logPath, `[${startedAt}] Windows update scheduled
|
|
464
|
+
`, "utf8");
|
|
465
|
+
writeJsonAtomic(statePath, pending);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
removeOwnedLock(lockPath, updateId);
|
|
468
|
+
rmSync(helperPath, { force: true });
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
const payload = Buffer.from(
|
|
472
|
+
JSON.stringify({
|
|
473
|
+
parentPid: options.parentPid ?? process.pid,
|
|
474
|
+
currentVersion: options.currentVersion,
|
|
475
|
+
channel: options.channel,
|
|
476
|
+
entrypointPath: normalize(options.entrypointPath),
|
|
477
|
+
startedAt,
|
|
478
|
+
statePath,
|
|
479
|
+
logPath,
|
|
480
|
+
updateDir,
|
|
481
|
+
updateId,
|
|
482
|
+
lockPath,
|
|
483
|
+
managedRoot,
|
|
484
|
+
...trustedTools
|
|
485
|
+
}),
|
|
486
|
+
"utf8"
|
|
487
|
+
).toString("base64url");
|
|
488
|
+
const launch = options.launchDetached ?? defaultLaunchWindowsUpdateHelper;
|
|
489
|
+
let helperPid;
|
|
490
|
+
try {
|
|
491
|
+
helperPid = launch({
|
|
492
|
+
execPath: options.execPath ?? process.execPath,
|
|
493
|
+
helperPath,
|
|
494
|
+
payload,
|
|
495
|
+
env
|
|
496
|
+
});
|
|
497
|
+
} catch (error) {
|
|
498
|
+
const state = readWindowsUpdateState(env, homeDir);
|
|
499
|
+
if (state?.updateId === updateId) {
|
|
500
|
+
rmSync(statePath, { force: true });
|
|
501
|
+
}
|
|
502
|
+
removeOwnedLock(lockPath, updateId);
|
|
503
|
+
rmSync(helperPath, { force: true });
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
writeJsonAtomic(statePath, { ...pending, helperPid });
|
|
508
|
+
} catch (error) {
|
|
509
|
+
try {
|
|
510
|
+
writeFileSync(
|
|
511
|
+
logPath,
|
|
512
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] Could not persist helper pid: ${error instanceof Error ? error.message : String(error)}
|
|
513
|
+
`,
|
|
514
|
+
{ encoding: "utf8", flag: "a" }
|
|
515
|
+
);
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return { helperPid, statePath, logPath };
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// packages/cli/updateCommands.ts
|
|
523
|
+
function resolvePackageJsonPath() {
|
|
524
|
+
if (isCompiledBinary()) {
|
|
525
|
+
const execDir = dirname2(process.execPath);
|
|
526
|
+
const candidate2 = join2(execDir, "package.json");
|
|
527
|
+
if (existsSync2(candidate2)) return candidate2;
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
const CLI_DIR = dirname2(fileURLToPath(import.meta.url));
|
|
531
|
+
const candidate = join2(CLI_DIR, "package.json");
|
|
532
|
+
return existsSync2(candidate) ? candidate : null;
|
|
533
|
+
}
|
|
534
|
+
var PACKAGE_JSON_PATH = resolvePackageJsonPath();
|
|
535
|
+
function getCliInstallChannel(serverUrl) {
|
|
536
|
+
const normalized = normalizeServerOrigin(serverUrl);
|
|
537
|
+
if (!normalized) {
|
|
538
|
+
return "latest";
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
const hostname = new URL(normalized).hostname.toLowerCase();
|
|
542
|
+
if (hostname === "us.nolo.chat" || hostname.endsWith(".us.nolo.chat")) {
|
|
543
|
+
return "alpha";
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
if (/us\.nolo\.chat/i.test(normalized)) {
|
|
547
|
+
return "alpha";
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return "latest";
|
|
551
|
+
}
|
|
552
|
+
function renderProgressBar(percent, width = 30) {
|
|
553
|
+
const pct = Math.max(0, Math.min(100, percent));
|
|
554
|
+
const filledLen = Math.round(width * pct / 100);
|
|
555
|
+
const emptyLen = width - filledLen;
|
|
556
|
+
const bar = "#".repeat(filledLen) + " ".repeat(emptyLen);
|
|
557
|
+
const pctText = pct.toFixed(1).padStart(5, " ");
|
|
558
|
+
return `[${bar}] ${pctText}%`;
|
|
559
|
+
}
|
|
560
|
+
function buildNpmSelfUpdateCommand(channel = "latest") {
|
|
561
|
+
return ["npm", "install", "-g", `nolo-cli@${channel}`, "--force", "--progress"];
|
|
562
|
+
}
|
|
563
|
+
function resolveStandaloneBundlePlatform(platform = process.platform, arch = process.arch) {
|
|
564
|
+
if (platform === "linux" && arch === "x64") {
|
|
565
|
+
return { tarballName: "nolo-linux-x64.tar.gz", extractSubdir: "nolo-linux-x64" };
|
|
566
|
+
}
|
|
567
|
+
if (platform === "darwin" && arch === "arm64") {
|
|
568
|
+
return { tarballName: "nolo-darwin-arm64.tar.gz", extractSubdir: "nolo-darwin-arm64" };
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
function resolveStandaloneBundleRepoBase(channel) {
|
|
573
|
+
return channel === "alpha" ? "https://us.nolo.chat" : "https://nolo.chat";
|
|
574
|
+
}
|
|
575
|
+
function buildStandaloneManualUpdateHint(channel) {
|
|
576
|
+
const repoBase = resolveStandaloneBundleRepoBase(channel);
|
|
577
|
+
return `curl -fsSL ${repoBase}/install-nolo.sh | sh`;
|
|
578
|
+
}
|
|
579
|
+
function resolveStandaloneInstallDir() {
|
|
580
|
+
const candidates = [
|
|
581
|
+
"/usr/local/bin",
|
|
582
|
+
join2(homedir2(), ".local/bin"),
|
|
583
|
+
join2(homedir2(), ".nolo/bin")
|
|
584
|
+
];
|
|
585
|
+
for (const dir of candidates) {
|
|
586
|
+
if (!existsSync2(dir)) continue;
|
|
587
|
+
try {
|
|
588
|
+
accessSync(dir, constants.W_OK);
|
|
589
|
+
return dir;
|
|
590
|
+
} catch {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return join2(homedir2(), ".nolo/bin");
|
|
595
|
+
}
|
|
596
|
+
function readPackageInfo() {
|
|
597
|
+
if (PACKAGE_JSON_PATH) {
|
|
598
|
+
try {
|
|
599
|
+
const raw = readFileSync2(PACKAGE_JSON_PATH, "utf8");
|
|
600
|
+
const parsed = JSON.parse(raw);
|
|
601
|
+
return {
|
|
602
|
+
name: parsed.name || "nolo-cli",
|
|
603
|
+
version: parsed.version || "0.0.0"
|
|
604
|
+
};
|
|
605
|
+
} catch {
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
name: process.env.NOLO_CLI_PACKAGE_NAME || "nolo-cli",
|
|
610
|
+
version: process.env.NOLO_CLI_VERSION || "0.0.0"
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
function buildCliVersionText(info) {
|
|
614
|
+
return `${info.name} ${info.version}`;
|
|
615
|
+
}
|
|
616
|
+
function buildSelfUpdateCommand(serverUrl) {
|
|
617
|
+
return buildNpmSelfUpdateCommand(getCliInstallChannel(serverUrl));
|
|
618
|
+
}
|
|
619
|
+
function resolveSelfUpdateServerUrl(env = process.env, override) {
|
|
620
|
+
if (override?.trim()) {
|
|
621
|
+
return normalizeServerOrigin(override);
|
|
622
|
+
}
|
|
623
|
+
const fromEnv = env.NOLO_SERVER || env.BASE_URL;
|
|
624
|
+
if (typeof fromEnv === "string" && fromEnv.trim()) {
|
|
625
|
+
return normalizeServerOrigin(fromEnv);
|
|
626
|
+
}
|
|
627
|
+
const profile = loadProfileConfig();
|
|
628
|
+
const profileUrl = profile?.profiles?.[profile.currentProfile]?.serverUrl;
|
|
629
|
+
if (profileUrl?.trim()) {
|
|
630
|
+
return normalizeServerOrigin(profileUrl);
|
|
631
|
+
}
|
|
632
|
+
return DEFAULT_NOLO_SERVER_URL;
|
|
633
|
+
}
|
|
634
|
+
function buildCliDoctorText(info) {
|
|
635
|
+
return [
|
|
636
|
+
"Nolo CLI doctor",
|
|
637
|
+
"---------------",
|
|
638
|
+
`version ${info.packageName} ${info.version}`,
|
|
639
|
+
`install ${info.installKind}`,
|
|
640
|
+
`channel ${info.updateChannel}`,
|
|
641
|
+
`entry ${info.entrypoint}`,
|
|
642
|
+
`server ${info.serverUrl}`,
|
|
643
|
+
`profile ${info.profileName}`,
|
|
644
|
+
`update nolo update`,
|
|
645
|
+
"",
|
|
646
|
+
"If direct `nolo` differs from repo-local `bun ./packages/cli/index.ts`,",
|
|
647
|
+
"the global install is older than this checkout.",
|
|
648
|
+
"",
|
|
649
|
+
`Manual update: ${info.installKind === "standalone-bundle" ? buildStandaloneManualUpdateHint(info.updateChannel) : buildNpmSelfUpdateCommand(info.updateChannel).join(" ")}`
|
|
650
|
+
].join("\n");
|
|
651
|
+
}
|
|
652
|
+
function isRunSelfUpdateOptions(value) {
|
|
653
|
+
return typeof value === "object" && value !== null && ("output" in value || "spawn" in value || "entrypointPath" in value || "serverUrl" in value || "env" in value || "platform" in value || "scheduleWindowsUpdate" in value);
|
|
654
|
+
}
|
|
655
|
+
async function forwardSpawnOutput(stream, output) {
|
|
656
|
+
if (!stream) {
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
for await (const chunk of stream) {
|
|
660
|
+
output.write(normalizeSpawnChunk(chunk));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function normalizeSpawnChunk(chunk) {
|
|
664
|
+
if (typeof chunk === "string" || chunk instanceof Uint8Array) {
|
|
665
|
+
return chunk;
|
|
666
|
+
}
|
|
667
|
+
if (chunk instanceof ArrayBuffer) {
|
|
668
|
+
return new Uint8Array(chunk);
|
|
669
|
+
}
|
|
670
|
+
return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
671
|
+
}
|
|
672
|
+
async function runShellScript(script, {
|
|
673
|
+
output,
|
|
674
|
+
spawn: spawn2,
|
|
675
|
+
env,
|
|
676
|
+
cwd,
|
|
677
|
+
useCustomSink
|
|
678
|
+
}) {
|
|
679
|
+
const proc = spawn2({
|
|
680
|
+
cmd: ["sh", "-c", script],
|
|
681
|
+
cwd,
|
|
682
|
+
stdin: "inherit",
|
|
683
|
+
stdout: useCustomSink ? "pipe" : "inherit",
|
|
684
|
+
stderr: useCustomSink ? "pipe" : "inherit",
|
|
685
|
+
env
|
|
686
|
+
});
|
|
687
|
+
if (!useCustomSink) {
|
|
688
|
+
return proc.exited;
|
|
689
|
+
}
|
|
690
|
+
const [exitCode] = await Promise.all([
|
|
691
|
+
proc.exited,
|
|
692
|
+
forwardSpawnOutput(proc.stdout, output),
|
|
693
|
+
forwardSpawnOutput(proc.stderr, output)
|
|
694
|
+
]);
|
|
695
|
+
return exitCode;
|
|
696
|
+
}
|
|
697
|
+
async function runStandaloneBundleUpdate(options) {
|
|
698
|
+
const output = options.output ?? process.stdout;
|
|
699
|
+
const spawn2 = options.spawn ?? resolveDefaultSpawn();
|
|
700
|
+
const env = options.env ?? process.env;
|
|
701
|
+
const serverUrl = resolveSelfUpdateServerUrl(env, options.serverUrl);
|
|
702
|
+
const channel = getCliInstallChannel(serverUrl);
|
|
703
|
+
const platform = resolveStandaloneBundlePlatform();
|
|
704
|
+
if (!platform) {
|
|
705
|
+
output.write(
|
|
706
|
+
`\u2717 Standalone-bundle self-update is not supported on this platform
|
|
707
|
+
(platform=${process.platform}, arch=${process.arch}).
|
|
708
|
+
Only linux-x64 and darwin-arm64 bundles are published.
|
|
709
|
+
Falling back is not possible from a compiled binary; reinstall via:
|
|
710
|
+
${buildStandaloneManualUpdateHint(channel)}
|
|
711
|
+
`
|
|
712
|
+
);
|
|
713
|
+
return 1;
|
|
714
|
+
}
|
|
715
|
+
const { tarballName, extractSubdir } = platform;
|
|
716
|
+
const repoBase = resolveStandaloneBundleRepoBase(channel);
|
|
717
|
+
const tarballUrl = `${repoBase}/public/downloads/${tarballName}`;
|
|
718
|
+
const fallbackUrl = `https://nolo.chat/public/downloads/${tarballName}`;
|
|
719
|
+
const extractDir = join2(homedir2(), ".nolo", extractSubdir);
|
|
720
|
+
const installDir = resolveStandaloneInstallDir();
|
|
721
|
+
const symlinkPath = join2(installDir, "nolo");
|
|
722
|
+
const script = `set -euo pipefail
|
|
723
|
+
TEMP_DIR=$(mktemp -d)
|
|
724
|
+
trap 'rm -rf "$TEMP_DIR"' EXIT
|
|
725
|
+
|
|
726
|
+
echo "Downloading nolo from ${tarballUrl}..."
|
|
727
|
+
if ! curl -fL --progress-bar "${tarballUrl}" -o "$TEMP_DIR/${tarballName}"; then
|
|
728
|
+
${tarballUrl !== fallbackUrl ? `echo "Primary source unavailable, trying ${fallbackUrl}..."
|
|
729
|
+
if ! curl -fL --progress-bar "${fallbackUrl}" -o "$TEMP_DIR/${tarballName}"; then
|
|
730
|
+
echo "Error: download failed from ${fallbackUrl}" >&2
|
|
731
|
+
exit 1
|
|
732
|
+
fi` : `echo "Error: download failed from ${tarballUrl}" >&2
|
|
733
|
+
exit 1`}
|
|
734
|
+
fi
|
|
735
|
+
|
|
736
|
+
mkdir -p "${extractDir}"
|
|
737
|
+
tar -xzf "$TEMP_DIR/${tarballName}" -C "${extractDir}"
|
|
738
|
+
chmod +x "${extractDir}/nolo"
|
|
739
|
+
|
|
740
|
+
mkdir -p "${installDir}"
|
|
741
|
+
if [ -e "${symlinkPath}" ] || [ -L "${symlinkPath}" ]; then
|
|
742
|
+
rm -f "${symlinkPath}"
|
|
743
|
+
fi
|
|
744
|
+
ln -s "${extractDir}/nolo" "${symlinkPath}"
|
|
745
|
+
trap - EXIT
|
|
746
|
+
|
|
747
|
+
echo "nolo updated to ${installDir}/nolo"
|
|
748
|
+
`;
|
|
749
|
+
output.write(`
|
|
750
|
+
Nolo Agent CLI Installer & Updater
|
|
751
|
+
`);
|
|
752
|
+
output.write(`-----------------------------------------
|
|
753
|
+
`);
|
|
754
|
+
output.write(`\u2713 Target channel: ${channel} (standalone bundle)
|
|
755
|
+
`);
|
|
756
|
+
output.write(`\u25B8 Downloading and installing bundle...
|
|
757
|
+
`);
|
|
758
|
+
const useCustomSink = options.output !== void 0;
|
|
759
|
+
const exitCode = await runShellScript(script, {
|
|
760
|
+
output,
|
|
761
|
+
spawn: spawn2,
|
|
762
|
+
env,
|
|
763
|
+
useCustomSink
|
|
764
|
+
});
|
|
765
|
+
if (exitCode === 0) {
|
|
766
|
+
output.write(` ${renderProgressBar(100)}
|
|
767
|
+
`);
|
|
768
|
+
output.write(`\u2713 Update completed successfully!
|
|
769
|
+
|
|
770
|
+
`);
|
|
771
|
+
}
|
|
772
|
+
return exitCode;
|
|
773
|
+
}
|
|
774
|
+
var NOLO_UPDATE_CHECK_KILL_SWITCH = "NOLO_CLI_NO_UPDATE_CHECK";
|
|
775
|
+
function parseCliVersion(raw) {
|
|
776
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/.exec(raw.trim());
|
|
777
|
+
if (!match) return null;
|
|
778
|
+
return {
|
|
779
|
+
main: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
780
|
+
pre: match[4] ? match[4].split(".") : []
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function compareCliVersions(a, b) {
|
|
784
|
+
const pa = parseCliVersion(a);
|
|
785
|
+
const pb = parseCliVersion(b);
|
|
786
|
+
if (!pa || !pb) return a.localeCompare(b);
|
|
787
|
+
for (let i = 0; i < 3; i++) {
|
|
788
|
+
if (pa.main[i] !== pb.main[i]) return pa.main[i] < pb.main[i] ? -1 : 1;
|
|
789
|
+
}
|
|
790
|
+
if (pa.pre.length === 0 && pb.pre.length === 0) return 0;
|
|
791
|
+
if (pa.pre.length === 0) return 1;
|
|
792
|
+
if (pb.pre.length === 0) return -1;
|
|
793
|
+
const len = Math.max(pa.pre.length, pb.pre.length);
|
|
794
|
+
for (let i = 0; i < len; i++) {
|
|
795
|
+
const x = pa.pre[i];
|
|
796
|
+
const y = pb.pre[i];
|
|
797
|
+
if (x === void 0) return -1;
|
|
798
|
+
if (y === void 0) return 1;
|
|
799
|
+
if (x === y) continue;
|
|
800
|
+
const xn = /^\d+$/.test(x) ? Number(x) : NaN;
|
|
801
|
+
const yn = /^\d+$/.test(y) ? Number(y) : NaN;
|
|
802
|
+
if (!Number.isNaN(xn) && !Number.isNaN(yn)) return xn < yn ? -1 : 1;
|
|
803
|
+
if (!Number.isNaN(xn)) return -1;
|
|
804
|
+
if (!Number.isNaN(yn)) return 1;
|
|
805
|
+
return x < y ? -1 : 1;
|
|
806
|
+
}
|
|
807
|
+
return 0;
|
|
808
|
+
}
|
|
809
|
+
async function checkForCliUpdate(currentVersion, serverUrl, options = {}) {
|
|
810
|
+
const env = options.env ?? process.env;
|
|
811
|
+
if ((env[NOLO_UPDATE_CHECK_KILL_SWITCH] ?? process.env[NOLO_UPDATE_CHECK_KILL_SWITCH]) === "1") {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
if (!currentVersion || !parseCliVersion(currentVersion)) return null;
|
|
815
|
+
const channel = getCliInstallChannel(serverUrl);
|
|
816
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
817
|
+
const timeoutMs = options.timeoutMs ?? 2e3;
|
|
818
|
+
try {
|
|
819
|
+
const res = await fetchImpl(
|
|
820
|
+
`https://registry.npmjs.org/nolo-cli/${channel}`,
|
|
821
|
+
{
|
|
822
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
823
|
+
headers: { accept: "application/json" }
|
|
824
|
+
}
|
|
825
|
+
);
|
|
826
|
+
if (!res.ok) return null;
|
|
827
|
+
const data = await res.json();
|
|
828
|
+
const latestVersion = data.version;
|
|
829
|
+
if (!latestVersion || !parseCliVersion(latestVersion) || compareCliVersions(latestVersion, currentVersion) <= 0) {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
return { latestVersion, channel };
|
|
833
|
+
} catch {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async function runSelfUpdateDetailed(outputOrOptions) {
|
|
838
|
+
const options = outputOrOptions === void 0 ? {} : isRunSelfUpdateOptions(outputOrOptions) ? outputOrOptions : { output: outputOrOptions };
|
|
839
|
+
if (isCompiledBinary()) {
|
|
840
|
+
return {
|
|
841
|
+
exitCode: await runStandaloneBundleUpdate(options),
|
|
842
|
+
disposition: "completed"
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
const output = options.output ?? process.stdout;
|
|
846
|
+
const spawn2 = options.spawn ?? resolveDefaultSpawn();
|
|
847
|
+
const env = options.env ?? process.env;
|
|
848
|
+
const serverUrl = resolveSelfUpdateServerUrl(env, options.serverUrl);
|
|
849
|
+
const channel = getCliInstallChannel(serverUrl);
|
|
850
|
+
const command = buildNpmSelfUpdateCommand(channel);
|
|
851
|
+
if ((options.platform ?? process.platform) === "win32") {
|
|
852
|
+
const packageInfo = readPackageInfo();
|
|
853
|
+
const entrypointPath = options.entrypointPath ?? fileURLToPath(import.meta.url);
|
|
854
|
+
const schedule = options.scheduleWindowsUpdate ?? scheduleWindowsSelfUpdate;
|
|
855
|
+
try {
|
|
856
|
+
const scheduled = schedule({
|
|
857
|
+
channel,
|
|
858
|
+
currentVersion: packageInfo.version,
|
|
859
|
+
entrypointPath,
|
|
860
|
+
env
|
|
861
|
+
});
|
|
862
|
+
output.write(`
|
|
863
|
+
Nolo Agent CLI Installer & Updater
|
|
864
|
+
`);
|
|
865
|
+
output.write(`-----------------------------------------
|
|
866
|
+
`);
|
|
867
|
+
output.write(`\u2713 Target channel: ${channel} (nolo-cli@${channel})
|
|
868
|
+
`);
|
|
869
|
+
output.write(`\u2713 Safe Windows update scheduled (helper pid ${scheduled.helperPid}).
|
|
870
|
+
`);
|
|
871
|
+
output.write(`\u25B8 Exit Nolo normally. Installation starts after this process releases its files.
|
|
872
|
+
`);
|
|
873
|
+
output.write(`\u25B8 Do not open another Nolo window until the update finishes.
|
|
874
|
+
`);
|
|
875
|
+
output.write(` Log: ${scheduled.logPath}
|
|
876
|
+
|
|
877
|
+
`);
|
|
878
|
+
return { exitCode: 0, disposition: "scheduled" };
|
|
879
|
+
} catch (error) {
|
|
880
|
+
output.write(
|
|
881
|
+
`\u2717 Could not schedule the Windows update: ${error instanceof Error ? error.message : String(error)}
|
|
882
|
+
`
|
|
883
|
+
);
|
|
884
|
+
output.write(
|
|
885
|
+
` Manual update after closing every Nolo window: ${command.join(" ")}
|
|
886
|
+
`
|
|
887
|
+
);
|
|
888
|
+
return { exitCode: 1, disposition: "completed" };
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
output.write(`
|
|
892
|
+
Nolo Agent CLI Installer & Updater
|
|
893
|
+
`);
|
|
894
|
+
output.write(`-----------------------------------------
|
|
895
|
+
`);
|
|
896
|
+
output.write(`\u2713 Target channel: ${channel} (nolo-cli@${channel})
|
|
897
|
+
`);
|
|
898
|
+
output.write(`\u25B8 Downloading and installing package via npm...
|
|
899
|
+
`);
|
|
900
|
+
const useCustomSink = options.output !== void 0;
|
|
901
|
+
const proc = spawn2({
|
|
902
|
+
cmd: command,
|
|
903
|
+
stdin: "inherit",
|
|
904
|
+
stdout: useCustomSink ? "pipe" : "inherit",
|
|
905
|
+
stderr: useCustomSink ? "pipe" : "inherit",
|
|
906
|
+
env
|
|
907
|
+
});
|
|
908
|
+
if (!useCustomSink) {
|
|
909
|
+
const exitCode2 = await proc.exited;
|
|
910
|
+
if (exitCode2 === 0) {
|
|
911
|
+
output.write(` ${renderProgressBar(100)}
|
|
912
|
+
`);
|
|
913
|
+
output.write(`\u2713 Update completed successfully!
|
|
914
|
+
|
|
915
|
+
`);
|
|
916
|
+
}
|
|
917
|
+
return { exitCode: exitCode2, disposition: "completed" };
|
|
918
|
+
}
|
|
919
|
+
const [exitCode] = await Promise.all([
|
|
920
|
+
proc.exited,
|
|
921
|
+
forwardSpawnOutput(proc.stdout, output),
|
|
922
|
+
forwardSpawnOutput(proc.stderr, output)
|
|
923
|
+
]);
|
|
924
|
+
if (exitCode === 0) {
|
|
925
|
+
output.write(` ${renderProgressBar(100)}
|
|
926
|
+
`);
|
|
927
|
+
output.write(`\u2713 Update completed successfully!
|
|
928
|
+
|
|
929
|
+
`);
|
|
930
|
+
}
|
|
931
|
+
return { exitCode, disposition: "completed" };
|
|
932
|
+
}
|
|
933
|
+
async function runSelfUpdate(outputOrOptions) {
|
|
934
|
+
return (await runSelfUpdateDetailed(outputOrOptions)).exitCode;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
export {
|
|
938
|
+
consumeWindowsUpdateStartupNotice,
|
|
939
|
+
getCliInstallChannel,
|
|
940
|
+
renderProgressBar,
|
|
941
|
+
buildNpmSelfUpdateCommand,
|
|
942
|
+
resolveStandaloneBundlePlatform,
|
|
943
|
+
resolveStandaloneBundleRepoBase,
|
|
944
|
+
buildStandaloneManualUpdateHint,
|
|
945
|
+
readPackageInfo,
|
|
946
|
+
buildCliVersionText,
|
|
947
|
+
buildSelfUpdateCommand,
|
|
948
|
+
resolveSelfUpdateServerUrl,
|
|
949
|
+
buildCliDoctorText,
|
|
950
|
+
runStandaloneBundleUpdate,
|
|
951
|
+
NOLO_UPDATE_CHECK_KILL_SWITCH,
|
|
952
|
+
compareCliVersions,
|
|
953
|
+
checkForCliUpdate,
|
|
954
|
+
runSelfUpdateDetailed,
|
|
955
|
+
runSelfUpdate
|
|
956
|
+
};
|