@qloo/qloo-harness 0.1.18
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/LICENSE +21 -0
- package/README.md +394 -0
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/app.js +48 -0
- package/dist/bin.js +14 -0
- package/dist/build.js +117 -0
- package/dist/cli.js +95 -0
- package/dist/doctor.js +1019 -0
- package/dist/exec.js +782 -0
- package/dist/guided-journey.js +223 -0
- package/dist/index.d.ts +1321 -0
- package/dist/index.js +156 -0
- package/dist/integration-plan.js +1097 -0
- package/dist/mcp.js +909 -0
- package/dist/observability.js +115 -0
- package/dist/paths.js +77 -0
- package/dist/plan.js +163 -0
- package/dist/profiles.js +63 -0
- package/dist/project-context.js +364 -0
- package/dist/qloo-presentation.js +358 -0
- package/dist/qloo-tools.js +2195 -0
- package/dist/resolution-provider.js +1380 -0
- package/dist/router.js +6061 -0
- package/dist/runtime/explore-policy.js +666 -0
- package/dist/runtime/pi-adapter.js +259 -0
- package/dist/runtime/pi-command-policy.js +74 -0
- package/dist/runtime/qloo-header.js +173 -0
- package/dist/runtime/resources.js +37 -0
- package/dist/setup.js +1138 -0
- package/dist/update-manager.js +906 -0
- package/dist/workflow-executor.js +976 -0
- package/package.json +72 -0
- package/resources/BUILD.md +24 -0
- package/resources/EXPLORE.md +34 -0
- package/resources/INTEGRATE.md +28 -0
- package/resources/PLAN.md +23 -0
- package/resources/SYSTEM.md +49 -0
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
import { createRequire as __qlooCreateRequire } from "node:module";
|
|
2
|
+
const require = __qlooCreateRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// apps/qloo-harness/dist/update-manager.js
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { mkdtemp, open, readFile, rm, stat, unlink } from "node:fs/promises";
|
|
8
|
+
import { gunzipSync } from "node:zlib";
|
|
9
|
+
import { dirname, join, posix, win32 } from "node:path";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { DefaultPackageManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { HARNESS_VERSION } from "./cli.js";
|
|
14
|
+
import { resolveQlooPaths } from "./paths.js";
|
|
15
|
+
var QLOO_HARNESS_PACKAGE_NAME = "@qloo/qloo-harness";
|
|
16
|
+
var QLOO_UPDATE_SPEC_ENVIRONMENT_VARIABLE = "QLOO_UPDATE_SPEC";
|
|
17
|
+
var QLOO_UPDATE_HELP = `Update Qloo
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
qloo update Update the Qloo harness
|
|
21
|
+
qloo update --self Update the Qloo harness explicitly
|
|
22
|
+
qloo update --extensions Update configured Qloo extension packages
|
|
23
|
+
qloo update --all Update extensions, then the harness
|
|
24
|
+
qloo update --self --extensions Update extensions, then the harness
|
|
25
|
+
qloo update --extension <source> Update one configured extension package
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--self Include the Qloo harness
|
|
29
|
+
--extensions Include all configured Qloo extension packages
|
|
30
|
+
--extension Update one configured Qloo extension package
|
|
31
|
+
--all Include extensions and the Qloo harness
|
|
32
|
+
--force Reinstall the harness even at the same or an older version
|
|
33
|
+
--timeout <s> Bound each package-manager install (10-900 seconds; default 120)
|
|
34
|
+
-h, --help Show this help
|
|
35
|
+
|
|
36
|
+
Update overrides:
|
|
37
|
+
QLOO_UPDATE_SPEC may select an approved absolute .tgz path or an exact Qloo
|
|
38
|
+
package tag or version, such as @qloo/qloo-harness@next.`;
|
|
39
|
+
var MODULE_PACKAGE_ROOT = dirname(fileURLToPath(new URL("../package.json", import.meta.url)));
|
|
40
|
+
var MAX_ARCHIVE_BYTES = 50 * 1024 * 1024;
|
|
41
|
+
var MAX_EXPANDED_ARCHIVE_BYTES = 100 * 1024 * 1024;
|
|
42
|
+
var MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
43
|
+
var DEFAULT_UPDATE_TIMEOUT_MS = 12e4;
|
|
44
|
+
var MIN_UPDATE_TIMEOUT_SECONDS = 10;
|
|
45
|
+
var MAX_UPDATE_TIMEOUT_SECONDS = 900;
|
|
46
|
+
var UPDATE_HEARTBEAT_MS = 5e3;
|
|
47
|
+
var MAX_CAPTURED_COMMAND_BYTES = 1024 * 1024;
|
|
48
|
+
var MAX_UNKNOWN_UPDATE_LOCK_AGE_MS = 60 * 60 * 1e3;
|
|
49
|
+
function inferPackageRootFromEntrypoint(entrypoint, platform = process.platform) {
|
|
50
|
+
if (!entrypoint)
|
|
51
|
+
return void 0;
|
|
52
|
+
const pathImplementation = platform === "win32" ? win32 : posix;
|
|
53
|
+
const normalizedEntrypoint = pathImplementation.resolve(entrypoint);
|
|
54
|
+
if (pathImplementation.basename(normalizedEntrypoint).toLowerCase() !== "bin.js") {
|
|
55
|
+
return void 0;
|
|
56
|
+
}
|
|
57
|
+
const distDirectory = pathImplementation.dirname(normalizedEntrypoint);
|
|
58
|
+
if (pathImplementation.basename(distDirectory).toLowerCase() !== "dist")
|
|
59
|
+
return void 0;
|
|
60
|
+
return pathImplementation.dirname(distDirectory);
|
|
61
|
+
}
|
|
62
|
+
var DEFAULT_PACKAGE_ROOT = inferPackageRootFromEntrypoint(process.argv[1]) ?? MODULE_PACKAGE_ROOT;
|
|
63
|
+
var UpdateUsageError = class extends Error {
|
|
64
|
+
};
|
|
65
|
+
function sanitizeTerminalText(value) {
|
|
66
|
+
return value.replace(/[\u0000-\u001f\u007f]/g, "?");
|
|
67
|
+
}
|
|
68
|
+
function parseUpdateArguments(args) {
|
|
69
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
70
|
+
return {
|
|
71
|
+
help: true,
|
|
72
|
+
self: false,
|
|
73
|
+
extensions: false,
|
|
74
|
+
force: false,
|
|
75
|
+
timeoutMs: DEFAULT_UPDATE_TIMEOUT_MS
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
let self = false;
|
|
79
|
+
let extensions = false;
|
|
80
|
+
let all = false;
|
|
81
|
+
let force = false;
|
|
82
|
+
let extensionSource;
|
|
83
|
+
let timeoutMs = DEFAULT_UPDATE_TIMEOUT_MS;
|
|
84
|
+
let timeoutSeen = false;
|
|
85
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
86
|
+
const argument = args[index];
|
|
87
|
+
if (argument === "--self") {
|
|
88
|
+
if (self)
|
|
89
|
+
throw new UpdateUsageError("--self may only be specified once");
|
|
90
|
+
self = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (argument === "--extensions") {
|
|
94
|
+
if (extensions)
|
|
95
|
+
throw new UpdateUsageError("--extensions may only be specified once");
|
|
96
|
+
extensions = true;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (argument === "--all") {
|
|
100
|
+
if (all)
|
|
101
|
+
throw new UpdateUsageError("--all may only be specified once");
|
|
102
|
+
all = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (argument === "--force") {
|
|
106
|
+
if (force)
|
|
107
|
+
throw new UpdateUsageError("--force may only be specified once");
|
|
108
|
+
force = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (argument === "--timeout" || argument?.startsWith("--timeout=")) {
|
|
112
|
+
if (timeoutSeen)
|
|
113
|
+
throw new UpdateUsageError("--timeout may only be specified once");
|
|
114
|
+
timeoutSeen = true;
|
|
115
|
+
const value = argument === "--timeout" ? args[index + 1] : argument.slice("--timeout=".length);
|
|
116
|
+
if (argument === "--timeout")
|
|
117
|
+
index += 1;
|
|
118
|
+
const seconds = Number(value);
|
|
119
|
+
if (!value || !Number.isSafeInteger(seconds) || seconds < MIN_UPDATE_TIMEOUT_SECONDS || seconds > MAX_UPDATE_TIMEOUT_SECONDS) {
|
|
120
|
+
throw new UpdateUsageError(`--timeout must be an integer from ${MIN_UPDATE_TIMEOUT_SECONDS} to ${MAX_UPDATE_TIMEOUT_SECONDS} seconds`);
|
|
121
|
+
}
|
|
122
|
+
timeoutMs = seconds * 1e3;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (argument === "--extension" || argument?.startsWith("--extension=")) {
|
|
126
|
+
if (extensionSource !== void 0) {
|
|
127
|
+
throw new UpdateUsageError("--extension may only be specified once");
|
|
128
|
+
}
|
|
129
|
+
const value = argument === "--extension" ? args[index + 1] : argument.slice("--extension=".length);
|
|
130
|
+
if (argument === "--extension")
|
|
131
|
+
index += 1;
|
|
132
|
+
if (!value || value.startsWith("-")) {
|
|
133
|
+
throw new UpdateUsageError("--extension requires a configured package source");
|
|
134
|
+
}
|
|
135
|
+
extensionSource = value;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
throw new UpdateUsageError(`unknown option ${argument ?? ""}`.trim());
|
|
139
|
+
}
|
|
140
|
+
if (all && (self || extensions || extensionSource !== void 0)) {
|
|
141
|
+
throw new UpdateUsageError("--all cannot be combined with another update target");
|
|
142
|
+
}
|
|
143
|
+
if (extensionSource !== void 0 && (self || extensions)) {
|
|
144
|
+
throw new UpdateUsageError("--extension cannot be combined with --self or --extensions");
|
|
145
|
+
}
|
|
146
|
+
if (all) {
|
|
147
|
+
self = true;
|
|
148
|
+
extensions = true;
|
|
149
|
+
} else if (!self && !extensions && extensionSource === void 0) {
|
|
150
|
+
self = true;
|
|
151
|
+
}
|
|
152
|
+
if (force && !self) {
|
|
153
|
+
throw new UpdateUsageError("--force requires a harness update target");
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
help: false,
|
|
157
|
+
self,
|
|
158
|
+
extensions: extensions || extensionSource !== void 0,
|
|
159
|
+
...extensionSource === void 0 ? {} : { extensionSource },
|
|
160
|
+
force,
|
|
161
|
+
timeoutMs
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function parseSemanticVersion(value) {
|
|
165
|
+
const match = value.match(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/);
|
|
166
|
+
if (!match)
|
|
167
|
+
throw new Error(`invalid semantic version: ${value}`);
|
|
168
|
+
const core = match.slice(1, 4).map((part) => Number(part));
|
|
169
|
+
if (core.some((part) => !Number.isSafeInteger(part))) {
|
|
170
|
+
throw new Error(`semantic version component is too large: ${value}`);
|
|
171
|
+
}
|
|
172
|
+
const prerelease = match[4] ? match[4].split(".").map((part) => {
|
|
173
|
+
if (/^\d+$/.test(part)) {
|
|
174
|
+
if (part.length > 1 && part.startsWith("0")) {
|
|
175
|
+
throw new Error(`invalid semantic version prerelease: ${value}`);
|
|
176
|
+
}
|
|
177
|
+
const numeric = Number(part);
|
|
178
|
+
if (!Number.isSafeInteger(numeric)) {
|
|
179
|
+
throw new Error(`semantic version prerelease is too large: ${value}`);
|
|
180
|
+
}
|
|
181
|
+
return numeric;
|
|
182
|
+
}
|
|
183
|
+
return part;
|
|
184
|
+
}) : [];
|
|
185
|
+
return {
|
|
186
|
+
major: core[0] ?? 0,
|
|
187
|
+
minor: core[1] ?? 0,
|
|
188
|
+
patch: core[2] ?? 0,
|
|
189
|
+
prerelease
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function compareSemanticVersions(leftValue, rightValue) {
|
|
193
|
+
const left = parseSemanticVersion(leftValue);
|
|
194
|
+
const right = parseSemanticVersion(rightValue);
|
|
195
|
+
for (const key of ["major", "minor", "patch"]) {
|
|
196
|
+
if (left[key] !== right[key])
|
|
197
|
+
return left[key] < right[key] ? -1 : 1;
|
|
198
|
+
}
|
|
199
|
+
if (left.prerelease.length === 0 || right.prerelease.length === 0) {
|
|
200
|
+
if (left.prerelease.length === right.prerelease.length)
|
|
201
|
+
return 0;
|
|
202
|
+
return left.prerelease.length === 0 ? 1 : -1;
|
|
203
|
+
}
|
|
204
|
+
const count = Math.max(left.prerelease.length, right.prerelease.length);
|
|
205
|
+
for (let index = 0; index < count; index += 1) {
|
|
206
|
+
const leftPart = left.prerelease[index];
|
|
207
|
+
const rightPart = right.prerelease[index];
|
|
208
|
+
if (leftPart === void 0 || rightPart === void 0) {
|
|
209
|
+
if (leftPart === rightPart)
|
|
210
|
+
return 0;
|
|
211
|
+
return leftPart === void 0 ? -1 : 1;
|
|
212
|
+
}
|
|
213
|
+
if (leftPart === rightPart)
|
|
214
|
+
continue;
|
|
215
|
+
if (typeof leftPart === "number" && typeof rightPart === "number") {
|
|
216
|
+
return leftPart < rightPart ? -1 : 1;
|
|
217
|
+
}
|
|
218
|
+
if (typeof leftPart === "number")
|
|
219
|
+
return -1;
|
|
220
|
+
if (typeof rightPart === "number")
|
|
221
|
+
return 1;
|
|
222
|
+
return leftPart < rightPart ? -1 : 1;
|
|
223
|
+
}
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
function isOfflineModeEnabled(env) {
|
|
227
|
+
const value = env.PI_OFFLINE?.trim().toLowerCase();
|
|
228
|
+
return value === "1" || value === "true" || value === "yes";
|
|
229
|
+
}
|
|
230
|
+
function packageRootMatches(left, right, platform) {
|
|
231
|
+
return platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
232
|
+
}
|
|
233
|
+
function inferGlobalInstallPrefix(packageRoot, platform = process.platform) {
|
|
234
|
+
const pathImplementation = platform === "win32" ? win32 : posix;
|
|
235
|
+
const normalizedRoot = pathImplementation.resolve(packageRoot);
|
|
236
|
+
const parentCount = platform === "win32" ? 3 : 4;
|
|
237
|
+
let prefix = normalizedRoot;
|
|
238
|
+
for (let index = 0; index < parentCount; index += 1)
|
|
239
|
+
prefix = pathImplementation.dirname(prefix);
|
|
240
|
+
const expectedRoot = platform === "win32" ? pathImplementation.join(prefix, "node_modules", "@qloo", "qloo-harness") : pathImplementation.join(prefix, "lib", "node_modules", "@qloo", "qloo-harness");
|
|
241
|
+
return packageRootMatches(normalizedRoot, expectedRoot, platform) ? prefix : void 0;
|
|
242
|
+
}
|
|
243
|
+
function absoluteConfiguredPath(value, platform) {
|
|
244
|
+
const configured = value?.trim();
|
|
245
|
+
if (!configured)
|
|
246
|
+
return void 0;
|
|
247
|
+
const pathImplementation = platform === "win32" ? win32 : posix;
|
|
248
|
+
return pathImplementation.isAbsolute(configured) ? pathImplementation.resolve(configured) : void 0;
|
|
249
|
+
}
|
|
250
|
+
function inferGlobalInstall(packageRoot, platform = process.platform, env = process.env) {
|
|
251
|
+
const pathImplementation = platform === "win32" ? win32 : posix;
|
|
252
|
+
const normalizedRoot = pathImplementation.resolve(packageRoot);
|
|
253
|
+
const scopeRoot = pathImplementation.dirname(normalizedRoot);
|
|
254
|
+
const nodeModulesRoot = pathImplementation.dirname(scopeRoot);
|
|
255
|
+
const isolatedInstallRoot = pathImplementation.dirname(nodeModulesRoot);
|
|
256
|
+
const versionRoot = pathImplementation.dirname(isolatedInstallRoot);
|
|
257
|
+
const globalDir = pathImplementation.dirname(versionRoot);
|
|
258
|
+
const versionLabel = pathImplementation.basename(versionRoot);
|
|
259
|
+
const pnpmLayoutVersion = /^v(\d+)$/u.exec(versionLabel);
|
|
260
|
+
const expectedRoot = pathImplementation.join(isolatedInstallRoot, "node_modules", "@qloo", "qloo-harness");
|
|
261
|
+
const pnpmLayout = pathImplementation.basename(scopeRoot) !== "@qloo" || pathImplementation.basename(nodeModulesRoot) !== "node_modules" || !packageRootMatches(normalizedRoot, expectedRoot, platform) || !pnpmLayoutVersion || Number(pnpmLayoutVersion[1]) < 11 ? void 0 : pnpmLayoutVersion;
|
|
262
|
+
if (pnpmLayout) {
|
|
263
|
+
const configuredBinDir = absoluteConfiguredPath(env.PNPM_CONFIG_GLOBAL_BIN_DIR, platform);
|
|
264
|
+
const pnpmHome = absoluteConfiguredPath(env.PNPM_HOME, platform);
|
|
265
|
+
const conventionalHome = pathImplementation.basename(globalDir) === "global" ? pathImplementation.dirname(globalDir) : void 0;
|
|
266
|
+
const globalBinDir = configuredBinDir ?? (pnpmHome ? pathImplementation.join(pnpmHome, "bin") : void 0) ?? (conventionalHome ? pathImplementation.join(conventionalHome, "bin") : void 0);
|
|
267
|
+
if (!globalBinDir)
|
|
268
|
+
return void 0;
|
|
269
|
+
return {
|
|
270
|
+
packageManager: "pnpm",
|
|
271
|
+
globalDir,
|
|
272
|
+
globalBinDir,
|
|
273
|
+
lockDirectory: globalDir
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const npmPrefix = inferGlobalInstallPrefix(packageRoot, platform);
|
|
277
|
+
return npmPrefix ? {
|
|
278
|
+
packageManager: "npm",
|
|
279
|
+
prefix: npmPrefix,
|
|
280
|
+
globalBinDir: platform === "win32" ? npmPrefix : pathImplementation.join(npmPrefix, "bin"),
|
|
281
|
+
lockDirectory: npmPrefix
|
|
282
|
+
} : void 0;
|
|
283
|
+
}
|
|
284
|
+
function parseTarString(data, offset, length) {
|
|
285
|
+
const end = data.indexOf(0, offset);
|
|
286
|
+
const boundedEnd = end === -1 || end > offset + length ? offset + length : end;
|
|
287
|
+
return data.subarray(offset, boundedEnd).toString("utf8").trim();
|
|
288
|
+
}
|
|
289
|
+
function parseTarSize(data, offset) {
|
|
290
|
+
const raw = parseTarString(data, offset, 12).replace(/\s+$/g, "");
|
|
291
|
+
if (!/^[0-7]+$/.test(raw))
|
|
292
|
+
throw new Error("artifact contains an invalid tar entry size");
|
|
293
|
+
const size = Number.parseInt(raw, 8);
|
|
294
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
295
|
+
throw new Error("artifact contains an unsupported tar entry size");
|
|
296
|
+
}
|
|
297
|
+
return size;
|
|
298
|
+
}
|
|
299
|
+
async function readArtifactManifest(archivePath) {
|
|
300
|
+
const archiveStats = await stat(archivePath);
|
|
301
|
+
if (!archiveStats.isFile())
|
|
302
|
+
throw new Error("QLOO_UPDATE_SPEC must point to a .tgz file");
|
|
303
|
+
if (archiveStats.size > MAX_ARCHIVE_BYTES) {
|
|
304
|
+
throw new Error(`update artifact exceeds the ${MAX_ARCHIVE_BYTES}-byte safety limit`);
|
|
305
|
+
}
|
|
306
|
+
const compressed = await readFile(archivePath);
|
|
307
|
+
const archive = gunzipSync(compressed, { maxOutputLength: MAX_EXPANDED_ARCHIVE_BYTES });
|
|
308
|
+
for (let offset = 0; offset + 512 <= archive.length; ) {
|
|
309
|
+
const header = archive.subarray(offset, offset + 512);
|
|
310
|
+
if (header.every((byte) => byte === 0))
|
|
311
|
+
break;
|
|
312
|
+
const name = parseTarString(header, 0, 100);
|
|
313
|
+
const prefix = parseTarString(header, 345, 155);
|
|
314
|
+
const entryPath = prefix ? `${prefix}/${name}` : name;
|
|
315
|
+
const entrySize = parseTarSize(header, 124);
|
|
316
|
+
const contentStart = offset + 512;
|
|
317
|
+
const contentEnd = contentStart + entrySize;
|
|
318
|
+
if (contentEnd > archive.length)
|
|
319
|
+
throw new Error("update artifact is truncated");
|
|
320
|
+
if (entryPath === "package/package.json") {
|
|
321
|
+
if (entrySize > MAX_MANIFEST_BYTES)
|
|
322
|
+
throw new Error("update artifact manifest is too large");
|
|
323
|
+
const parsed = JSON.parse(archive.subarray(contentStart, contentEnd).toString("utf8"));
|
|
324
|
+
if (!parsed || typeof parsed !== "object")
|
|
325
|
+
throw new Error("update artifact manifest is invalid");
|
|
326
|
+
const manifest = parsed;
|
|
327
|
+
if (manifest.name !== QLOO_HARNESS_PACKAGE_NAME || typeof manifest.version !== "string") {
|
|
328
|
+
throw new Error(`update artifact must contain ${QLOO_HARNESS_PACKAGE_NAME}`);
|
|
329
|
+
}
|
|
330
|
+
parseSemanticVersion(manifest.version);
|
|
331
|
+
return { name: manifest.name, version: manifest.version };
|
|
332
|
+
}
|
|
333
|
+
offset = contentStart + Math.ceil(entrySize / 512) * 512;
|
|
334
|
+
}
|
|
335
|
+
throw new Error("update artifact does not contain package/package.json");
|
|
336
|
+
}
|
|
337
|
+
async function runExternalCommand(request) {
|
|
338
|
+
return new Promise((resolveResult) => {
|
|
339
|
+
const startedAt = Date.now();
|
|
340
|
+
const child = spawn(request.command, [...request.args], {
|
|
341
|
+
cwd: request.cwd,
|
|
342
|
+
env: { ...request.env },
|
|
343
|
+
shell: false,
|
|
344
|
+
windowsHide: true,
|
|
345
|
+
detached: process.platform !== "win32",
|
|
346
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
347
|
+
});
|
|
348
|
+
let stdout = "";
|
|
349
|
+
let stderr = "";
|
|
350
|
+
let settled = false;
|
|
351
|
+
let timedOut = false;
|
|
352
|
+
let timeout;
|
|
353
|
+
let heartbeat;
|
|
354
|
+
let forceKill;
|
|
355
|
+
let finalFallback;
|
|
356
|
+
const appendBounded = (current, text) => {
|
|
357
|
+
const combined = current + text;
|
|
358
|
+
return combined.length <= MAX_CAPTURED_COMMAND_BYTES ? combined : combined.slice(-MAX_CAPTURED_COMMAND_BYTES);
|
|
359
|
+
};
|
|
360
|
+
const clearTimers = () => {
|
|
361
|
+
if (timeout)
|
|
362
|
+
clearTimeout(timeout);
|
|
363
|
+
if (heartbeat)
|
|
364
|
+
clearInterval(heartbeat);
|
|
365
|
+
if (forceKill)
|
|
366
|
+
clearTimeout(forceKill);
|
|
367
|
+
if (finalFallback)
|
|
368
|
+
clearTimeout(finalFallback);
|
|
369
|
+
};
|
|
370
|
+
const finish = (result) => {
|
|
371
|
+
if (settled)
|
|
372
|
+
return;
|
|
373
|
+
settled = true;
|
|
374
|
+
clearTimers();
|
|
375
|
+
resolveResult({ ...result, durationMs: Date.now() - startedAt });
|
|
376
|
+
};
|
|
377
|
+
const terminate = (signal) => {
|
|
378
|
+
try {
|
|
379
|
+
if (process.platform !== "win32" && child.pid !== void 0) {
|
|
380
|
+
process.kill(-child.pid, signal);
|
|
381
|
+
} else {
|
|
382
|
+
child.kill(signal);
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
child.kill(signal);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
if (request.heartbeatMs && request.heartbeatMs > 0) {
|
|
389
|
+
heartbeat = setInterval(() => {
|
|
390
|
+
const description = request.description ?? request.command;
|
|
391
|
+
request.writeOut(`Still ${sanitizeTerminalText(description)} (${Math.round((Date.now() - startedAt) / 1e3)}s elapsed)...
|
|
392
|
+
`);
|
|
393
|
+
}, request.heartbeatMs);
|
|
394
|
+
heartbeat.unref?.();
|
|
395
|
+
}
|
|
396
|
+
if (request.timeoutMs && request.timeoutMs > 0) {
|
|
397
|
+
timeout = setTimeout(() => {
|
|
398
|
+
timedOut = true;
|
|
399
|
+
const description = request.description ?? request.command;
|
|
400
|
+
request.writeError(`${sanitizeTerminalText(description)} exceeded ${Math.max(1, Math.ceil(request.timeoutMs / 1e3))}s; terminating it.
|
|
401
|
+
`);
|
|
402
|
+
terminate("SIGTERM");
|
|
403
|
+
forceKill = setTimeout(() => terminate("SIGKILL"), 2e3);
|
|
404
|
+
forceKill.unref?.();
|
|
405
|
+
finalFallback = setTimeout(() => finish({
|
|
406
|
+
exitCode: null,
|
|
407
|
+
signal: "SIGKILL",
|
|
408
|
+
stdout,
|
|
409
|
+
stderr,
|
|
410
|
+
timedOut: true
|
|
411
|
+
}), 5e3);
|
|
412
|
+
finalFallback.unref?.();
|
|
413
|
+
}, request.timeoutMs);
|
|
414
|
+
timeout.unref?.();
|
|
415
|
+
}
|
|
416
|
+
child.stdout.on("data", (chunk) => {
|
|
417
|
+
const text = chunk.toString();
|
|
418
|
+
stdout = appendBounded(stdout, text);
|
|
419
|
+
if (request.streamOutput)
|
|
420
|
+
request.writeOut(text);
|
|
421
|
+
});
|
|
422
|
+
child.stderr.on("data", (chunk) => {
|
|
423
|
+
const text = chunk.toString();
|
|
424
|
+
stderr = appendBounded(stderr, text);
|
|
425
|
+
if (request.streamOutput)
|
|
426
|
+
request.writeError(text);
|
|
427
|
+
});
|
|
428
|
+
child.once("error", (error) => {
|
|
429
|
+
finish({ exitCode: null, signal: null, stdout, stderr, error, ...timedOut ? { timedOut } : {} });
|
|
430
|
+
});
|
|
431
|
+
child.once("close", (exitCode, signal) => {
|
|
432
|
+
finish({ exitCode, signal, stdout, stderr, ...timedOut ? { timedOut } : {} });
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
function parseRegistryVersion(stdout, packageManager) {
|
|
437
|
+
const parsed = JSON.parse(stdout.trim());
|
|
438
|
+
if (typeof parsed !== "string") {
|
|
439
|
+
throw new Error(`${packageManager} returned an unexpected version response`);
|
|
440
|
+
}
|
|
441
|
+
parseSemanticVersion(parsed);
|
|
442
|
+
return parsed;
|
|
443
|
+
}
|
|
444
|
+
async function resolveUpdateTarget(options, install) {
|
|
445
|
+
const metadataTimeoutMs = Math.min(options.timeoutMs, 3e4);
|
|
446
|
+
const packageManager = install.packageManager;
|
|
447
|
+
const packageManagerCommand2 = packageManager === "pnpm" ? options.pnpmCommand : options.npmCommand;
|
|
448
|
+
const commandCwd = packageManager === "pnpm" ? tmpdir() : options.cwd;
|
|
449
|
+
const configuredSpec = options.env[QLOO_UPDATE_SPEC_ENVIRONMENT_VARIABLE];
|
|
450
|
+
if (configuredSpec !== void 0) {
|
|
451
|
+
const spec = configuredSpec.trim();
|
|
452
|
+
if (!spec)
|
|
453
|
+
throw new Error(`${QLOO_UPDATE_SPEC_ENVIRONMENT_VARIABLE} cannot be empty`);
|
|
454
|
+
const pathImplementation = options.platform === "win32" ? win32 : posix;
|
|
455
|
+
if (pathImplementation.isAbsolute(spec)) {
|
|
456
|
+
if (!spec.toLowerCase().endsWith(".tgz")) {
|
|
457
|
+
throw new Error(`${QLOO_UPDATE_SPEC_ENVIRONMENT_VARIABLE} local paths must end in .tgz`);
|
|
458
|
+
}
|
|
459
|
+
const manifest = await readArtifactManifest(spec);
|
|
460
|
+
return { installSpec: spec, version: manifest.version, localArtifact: true };
|
|
461
|
+
}
|
|
462
|
+
const escapedPackageName = QLOO_HARNESS_PACKAGE_NAME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
463
|
+
const allowedRegistrySpec = new RegExp(`^${escapedPackageName}@[A-Za-z0-9][A-Za-z0-9._+-]*$`);
|
|
464
|
+
if (!allowedRegistrySpec.test(spec)) {
|
|
465
|
+
throw new Error(`${QLOO_UPDATE_SPEC_ENVIRONMENT_VARIABLE} must be an absolute .tgz path or ${QLOO_HARNESS_PACKAGE_NAME}@<tag-or-version>`);
|
|
466
|
+
}
|
|
467
|
+
if (isOfflineModeEnabled(options.env)) {
|
|
468
|
+
throw new Error("PI_OFFLINE is enabled; a registry update cannot run offline");
|
|
469
|
+
}
|
|
470
|
+
const result2 = await options.runCommand({
|
|
471
|
+
command: packageManagerCommand2,
|
|
472
|
+
args: ["view", spec, "version", "--json"],
|
|
473
|
+
cwd: commandCwd,
|
|
474
|
+
env: options.env,
|
|
475
|
+
streamOutput: false,
|
|
476
|
+
writeOut: options.writeOut,
|
|
477
|
+
writeError: options.writeError,
|
|
478
|
+
timeoutMs: metadataTimeoutMs,
|
|
479
|
+
description: "checking the Qloo update channel"
|
|
480
|
+
});
|
|
481
|
+
if (result2.exitCode !== 0) {
|
|
482
|
+
throw new Error(`unable to resolve ${spec} (${packageManager} exited with status ${result2.exitCode ?? "unknown"})`);
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
installSpec: spec,
|
|
486
|
+
version: parseRegistryVersion(result2.stdout, packageManager),
|
|
487
|
+
localArtifact: false
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
if (isOfflineModeEnabled(options.env)) {
|
|
491
|
+
throw new Error("PI_OFFLINE is enabled; the Qloo release channel cannot be checked offline");
|
|
492
|
+
}
|
|
493
|
+
const defaultSpec = `${QLOO_HARNESS_PACKAGE_NAME}@latest`;
|
|
494
|
+
const result = await options.runCommand({
|
|
495
|
+
command: packageManagerCommand2,
|
|
496
|
+
args: ["view", defaultSpec, "version", "--json"],
|
|
497
|
+
cwd: commandCwd,
|
|
498
|
+
env: options.env,
|
|
499
|
+
streamOutput: false,
|
|
500
|
+
writeOut: options.writeOut,
|
|
501
|
+
writeError: options.writeError,
|
|
502
|
+
timeoutMs: metadataTimeoutMs,
|
|
503
|
+
description: "checking the Qloo update channel"
|
|
504
|
+
});
|
|
505
|
+
if (result.exitCode !== 0) {
|
|
506
|
+
throw new Error(`the Qloo npm registry channel is not available (${packageManager} exited with status ${result.exitCode ?? "unknown"}). For an approved local artifact, set ${QLOO_UPDATE_SPEC_ENVIRONMENT_VARIABLE} to its absolute .tgz path`);
|
|
507
|
+
}
|
|
508
|
+
return {
|
|
509
|
+
installSpec: defaultSpec,
|
|
510
|
+
version: parseRegistryVersion(result.stdout, packageManager),
|
|
511
|
+
localArtifact: false
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function globalPackageRoot(prefix, platform) {
|
|
515
|
+
const pathImplementation = platform === "win32" ? win32 : posix;
|
|
516
|
+
return platform === "win32" ? pathImplementation.join(prefix, "node_modules", "@qloo", "qloo-harness") : pathImplementation.join(prefix, "lib", "node_modules", "@qloo", "qloo-harness");
|
|
517
|
+
}
|
|
518
|
+
function npmInstallArguments(prefix, installSpec) {
|
|
519
|
+
return [
|
|
520
|
+
"install",
|
|
521
|
+
"--global",
|
|
522
|
+
"--prefix",
|
|
523
|
+
prefix,
|
|
524
|
+
"--ignore-scripts",
|
|
525
|
+
"--no-audit",
|
|
526
|
+
"--no-fund",
|
|
527
|
+
"--color=false",
|
|
528
|
+
"--package-lock=false",
|
|
529
|
+
installSpec
|
|
530
|
+
];
|
|
531
|
+
}
|
|
532
|
+
function pnpmInstallArguments(installSpec) {
|
|
533
|
+
return [
|
|
534
|
+
"add",
|
|
535
|
+
"--global",
|
|
536
|
+
"--ignore-scripts",
|
|
537
|
+
"--reporter=append-only",
|
|
538
|
+
installSpec
|
|
539
|
+
];
|
|
540
|
+
}
|
|
541
|
+
function packageManagerCommand(install, options) {
|
|
542
|
+
return install.packageManager === "pnpm" ? options.pnpmCommand : options.npmCommand;
|
|
543
|
+
}
|
|
544
|
+
function installArguments(install, installSpec) {
|
|
545
|
+
return install.packageManager === "pnpm" ? pnpmInstallArguments(installSpec) : npmInstallArguments(install.prefix, installSpec);
|
|
546
|
+
}
|
|
547
|
+
function qlooBinaryPath(install, platform) {
|
|
548
|
+
const pathImplementation = platform === "win32" ? win32 : posix;
|
|
549
|
+
return pathImplementation.join(install.globalBinDir, platform === "win32" ? "qloo.cmd" : "qloo");
|
|
550
|
+
}
|
|
551
|
+
function installEnvironment(install, env, platform) {
|
|
552
|
+
const configured = { ...env, NO_COLOR: "1" };
|
|
553
|
+
if (install.packageManager !== "pnpm")
|
|
554
|
+
return configured;
|
|
555
|
+
const pathImplementation = platform === "win32" ? win32 : posix;
|
|
556
|
+
const pathKey = platform === "win32" ? Object.keys(configured).find((key) => key.toLowerCase() === "path") ?? "Path" : "PATH";
|
|
557
|
+
const existingPath = configured[pathKey]?.trim();
|
|
558
|
+
configured.PNPM_HOME = pathImplementation.dirname(install.globalBinDir);
|
|
559
|
+
configured.PNPM_CONFIG_GLOBAL_DIR = install.globalDir;
|
|
560
|
+
configured.PNPM_CONFIG_GLOBAL_BIN_DIR = install.globalBinDir;
|
|
561
|
+
configured[pathKey] = existingPath ? `${install.globalBinDir}${platform === "win32" ? ";" : ":"}${existingPath}` : install.globalBinDir;
|
|
562
|
+
return configured;
|
|
563
|
+
}
|
|
564
|
+
function stagingInstall(active, stagingRoot, platform) {
|
|
565
|
+
const pathImplementation = platform === "win32" ? win32 : posix;
|
|
566
|
+
if (active.packageManager === "npm") {
|
|
567
|
+
return {
|
|
568
|
+
packageManager: "npm",
|
|
569
|
+
prefix: stagingRoot,
|
|
570
|
+
globalBinDir: platform === "win32" ? stagingRoot : pathImplementation.join(stagingRoot, "bin"),
|
|
571
|
+
lockDirectory: stagingRoot
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
packageManager: "pnpm",
|
|
576
|
+
globalDir: pathImplementation.join(stagingRoot, "global"),
|
|
577
|
+
globalBinDir: pathImplementation.join(stagingRoot, "bin"),
|
|
578
|
+
lockDirectory: pathImplementation.join(stagingRoot, "global")
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
async function installedVersion(packageRoot) {
|
|
582
|
+
const installedManifest = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
|
|
583
|
+
return installedManifest && typeof installedManifest === "object" ? installedManifest.version : void 0;
|
|
584
|
+
}
|
|
585
|
+
function installFailureMessage(packageManager, phase, result) {
|
|
586
|
+
if (result.timedOut) {
|
|
587
|
+
return `Qloo update ${phase} timed out after its configured limit; the update process was terminated`;
|
|
588
|
+
}
|
|
589
|
+
const detail = result.error ? `: ${result.error.message}` : "";
|
|
590
|
+
return `${packageManager} could not complete Qloo update ${phase} (status ${result.exitCode ?? "unknown"})${detail}`;
|
|
591
|
+
}
|
|
592
|
+
function isErrnoException(error, code) {
|
|
593
|
+
return error instanceof Error && error.code === code;
|
|
594
|
+
}
|
|
595
|
+
function processIsRunning(pid) {
|
|
596
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
597
|
+
return false;
|
|
598
|
+
try {
|
|
599
|
+
process.kill(pid, 0);
|
|
600
|
+
return true;
|
|
601
|
+
} catch (error) {
|
|
602
|
+
return isErrnoException(error, "EPERM");
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function existingUpdateLockIsActive(lockPath) {
|
|
606
|
+
try {
|
|
607
|
+
const [contents, metadata] = await Promise.all([
|
|
608
|
+
readFile(lockPath, "utf8"),
|
|
609
|
+
stat(lockPath)
|
|
610
|
+
]);
|
|
611
|
+
try {
|
|
612
|
+
const record = JSON.parse(contents);
|
|
613
|
+
if (typeof record.pid === "number")
|
|
614
|
+
return processIsRunning(record.pid);
|
|
615
|
+
} catch {
|
|
616
|
+
}
|
|
617
|
+
return Date.now() - metadata.mtimeMs <= MAX_UNKNOWN_UPDATE_LOCK_AGE_MS;
|
|
618
|
+
} catch (error) {
|
|
619
|
+
if (isErrnoException(error, "ENOENT"))
|
|
620
|
+
return false;
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
async function acquireQlooUpdateLock(prefix) {
|
|
625
|
+
const lockPath = join(prefix, ".qloo-update.lock");
|
|
626
|
+
const record = {
|
|
627
|
+
pid: process.pid,
|
|
628
|
+
token: randomUUID(),
|
|
629
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
630
|
+
};
|
|
631
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
632
|
+
try {
|
|
633
|
+
const handle = await open(lockPath, "wx", 384);
|
|
634
|
+
try {
|
|
635
|
+
await handle.writeFile(`${JSON.stringify(record)}
|
|
636
|
+
`, "utf8");
|
|
637
|
+
} catch (error) {
|
|
638
|
+
await handle.close().catch(() => void 0);
|
|
639
|
+
await unlink(lockPath).catch(() => void 0);
|
|
640
|
+
throw error;
|
|
641
|
+
}
|
|
642
|
+
await handle.close();
|
|
643
|
+
return async () => {
|
|
644
|
+
try {
|
|
645
|
+
const current = JSON.parse(await readFile(lockPath, "utf8"));
|
|
646
|
+
if (current.token === record.token)
|
|
647
|
+
await unlink(lockPath);
|
|
648
|
+
} catch (error) {
|
|
649
|
+
if (!isErrnoException(error, "ENOENT"))
|
|
650
|
+
throw error;
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
} catch (error) {
|
|
654
|
+
if (!isErrnoException(error, "EEXIST"))
|
|
655
|
+
throw error;
|
|
656
|
+
if (await existingUpdateLockIsActive(lockPath)) {
|
|
657
|
+
throw new Error(`another Qloo update is already running for ${prefix}; wait for it to finish or inspect ${lockPath}`);
|
|
658
|
+
}
|
|
659
|
+
await unlink(lockPath).catch((unlinkError) => {
|
|
660
|
+
if (!isErrnoException(unlinkError, "ENOENT"))
|
|
661
|
+
throw unlinkError;
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
throw new Error(`unable to acquire the Qloo update lock at ${lockPath}`);
|
|
666
|
+
}
|
|
667
|
+
async function updateQlooSelf(options) {
|
|
668
|
+
parseSemanticVersion(options.currentVersion);
|
|
669
|
+
const activeInstall = inferGlobalInstall(options.packageRoot, options.platform, options.env);
|
|
670
|
+
if (!activeInstall) {
|
|
671
|
+
throw new Error("self-update requires a supported global npm or pnpm installation of Qloo; this copy appears to be running from a source checkout or another installer");
|
|
672
|
+
}
|
|
673
|
+
const target = await resolveUpdateTarget(options, activeInstall);
|
|
674
|
+
const comparison = compareSemanticVersions(target.version, options.currentVersion);
|
|
675
|
+
if (comparison === 0 && !options.force) {
|
|
676
|
+
options.writeOut(`Qloo is already up to date (${options.currentVersion}).
|
|
677
|
+
`);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
if (comparison < 0 && !options.force) {
|
|
681
|
+
throw new Error(`refusing to downgrade Qloo from ${options.currentVersion} to ${target.version} without --force`);
|
|
682
|
+
}
|
|
683
|
+
const releaseUpdateLock = await acquireQlooUpdateLock(activeInstall.lockDirectory);
|
|
684
|
+
try {
|
|
685
|
+
const verb = comparison === 0 ? "Reinstalling" : comparison < 0 ? "Downgrading" : "Updating";
|
|
686
|
+
options.writeOut(`${verb} Qloo ${options.currentVersion} -> ${target.version}...
|
|
687
|
+
`);
|
|
688
|
+
const stagingRoot = await mkdtemp(join(tmpdir(), "qloo-update-stage-"));
|
|
689
|
+
try {
|
|
690
|
+
const candidateInstall = stagingInstall(activeInstall, stagingRoot, options.platform);
|
|
691
|
+
const managerCommand = packageManagerCommand(activeInstall, options);
|
|
692
|
+
const managerCwd = activeInstall.packageManager === "pnpm" ? stagingRoot : options.cwd;
|
|
693
|
+
options.writeOut("Validating the candidate in an isolated staging prefix...\n");
|
|
694
|
+
const stagedInstall = await options.runCommand({
|
|
695
|
+
command: managerCommand,
|
|
696
|
+
args: installArguments(candidateInstall, target.installSpec),
|
|
697
|
+
cwd: managerCwd,
|
|
698
|
+
env: installEnvironment(candidateInstall, options.env, options.platform),
|
|
699
|
+
streamOutput: false,
|
|
700
|
+
writeOut: options.writeOut,
|
|
701
|
+
writeError: options.writeError,
|
|
702
|
+
timeoutMs: options.timeoutMs,
|
|
703
|
+
heartbeatMs: UPDATE_HEARTBEAT_MS,
|
|
704
|
+
description: "validating the staged Qloo update"
|
|
705
|
+
});
|
|
706
|
+
if (stagedInstall.exitCode !== 0) {
|
|
707
|
+
throw new Error(installFailureMessage(activeInstall.packageManager, "staging", stagedInstall));
|
|
708
|
+
}
|
|
709
|
+
let stagedSmokeCommand;
|
|
710
|
+
let stagedSmokeArguments;
|
|
711
|
+
if (candidateInstall.packageManager === "npm") {
|
|
712
|
+
const stagedPackageRoot = globalPackageRoot(candidateInstall.prefix, options.platform);
|
|
713
|
+
const stagedVersion = await installedVersion(stagedPackageRoot);
|
|
714
|
+
if (stagedVersion !== target.version) {
|
|
715
|
+
throw new Error(`staged Qloo version is ${String(stagedVersion)} instead of ${target.version}`);
|
|
716
|
+
}
|
|
717
|
+
stagedSmokeCommand = process.execPath;
|
|
718
|
+
stagedSmokeArguments = [join(stagedPackageRoot, "dist", "bin.js"), "--version"];
|
|
719
|
+
} else {
|
|
720
|
+
stagedSmokeCommand = qlooBinaryPath(candidateInstall, options.platform);
|
|
721
|
+
stagedSmokeArguments = ["--version"];
|
|
722
|
+
}
|
|
723
|
+
const stagedSmoke = await options.runCommand({
|
|
724
|
+
command: stagedSmokeCommand,
|
|
725
|
+
args: stagedSmokeArguments,
|
|
726
|
+
cwd: managerCwd,
|
|
727
|
+
env: installEnvironment(candidateInstall, options.env, options.platform),
|
|
728
|
+
streamOutput: false,
|
|
729
|
+
writeOut: options.writeOut,
|
|
730
|
+
writeError: options.writeError,
|
|
731
|
+
timeoutMs: Math.min(options.timeoutMs, 15e3),
|
|
732
|
+
description: "checking the staged Qloo executable"
|
|
733
|
+
});
|
|
734
|
+
if (stagedSmoke.exitCode !== 0 || stagedSmoke.stdout.trim() !== target.version) {
|
|
735
|
+
throw new Error("the staged Qloo executable did not pass its version check");
|
|
736
|
+
}
|
|
737
|
+
options.writeOut("Candidate passed staging; activating it in the current global prefix...\n");
|
|
738
|
+
const activation = await options.runCommand({
|
|
739
|
+
command: managerCommand,
|
|
740
|
+
args: installArguments(activeInstall, target.installSpec),
|
|
741
|
+
cwd: managerCwd,
|
|
742
|
+
env: installEnvironment(activeInstall, options.env, options.platform),
|
|
743
|
+
streamOutput: true,
|
|
744
|
+
writeOut: options.writeOut,
|
|
745
|
+
writeError: options.writeError,
|
|
746
|
+
timeoutMs: options.timeoutMs,
|
|
747
|
+
heartbeatMs: UPDATE_HEARTBEAT_MS,
|
|
748
|
+
description: "activating the Qloo update"
|
|
749
|
+
});
|
|
750
|
+
if (activation.exitCode !== 0) {
|
|
751
|
+
throw new Error(`${installFailureMessage(activeInstall.packageManager, "activation", activation)}. The staged candidate passed, but ${activeInstall.packageManager} may have modified the active installation; rerun the same update or reinstall the previous artifact`);
|
|
752
|
+
}
|
|
753
|
+
let activeVersion;
|
|
754
|
+
if (activeInstall.packageManager === "npm") {
|
|
755
|
+
activeVersion = await installedVersion(options.packageRoot);
|
|
756
|
+
} else {
|
|
757
|
+
const activeSmoke = await options.runCommand({
|
|
758
|
+
command: qlooBinaryPath(activeInstall, options.platform),
|
|
759
|
+
args: ["--version"],
|
|
760
|
+
cwd: managerCwd,
|
|
761
|
+
env: installEnvironment(activeInstall, options.env, options.platform),
|
|
762
|
+
streamOutput: false,
|
|
763
|
+
writeOut: options.writeOut,
|
|
764
|
+
writeError: options.writeError,
|
|
765
|
+
timeoutMs: Math.min(options.timeoutMs, 15e3),
|
|
766
|
+
description: "checking the active Qloo executable"
|
|
767
|
+
});
|
|
768
|
+
activeVersion = activeSmoke.exitCode === 0 ? activeSmoke.stdout.trim() : void 0;
|
|
769
|
+
}
|
|
770
|
+
if (activeVersion !== target.version) {
|
|
771
|
+
throw new Error(`${activeInstall.packageManager} completed, but the installed Qloo version is ${String(activeVersion)} instead of ${target.version}`);
|
|
772
|
+
}
|
|
773
|
+
} finally {
|
|
774
|
+
await rm(stagingRoot, { recursive: true, force: true }).catch(() => void 0);
|
|
775
|
+
}
|
|
776
|
+
const completedVerb = comparison === 0 ? "Reinstalled" : comparison < 0 ? "Downgraded" : "Updated";
|
|
777
|
+
options.writeOut(`${completedVerb} Qloo to ${target.version}. Restart any running Qloo sessions.
|
|
778
|
+
`);
|
|
779
|
+
if (target.localArtifact) {
|
|
780
|
+
options.writeOut("Installed from the approved local artifact.\n");
|
|
781
|
+
}
|
|
782
|
+
} finally {
|
|
783
|
+
await releaseUpdateLock();
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function createDefaultExtensionPackageManager(agentDirectory) {
|
|
787
|
+
const storedSettings = SettingsManager.create(agentDirectory, agentDirectory, { projectTrusted: false });
|
|
788
|
+
const settingsErrors = storedSettings.drainErrors().filter(({ scope }) => scope === "global");
|
|
789
|
+
if (settingsErrors.length > 0) {
|
|
790
|
+
throw new Error(`unable to read Qloo extension settings: ${settingsErrors.map(({ error }) => error.message).join("; ")}`);
|
|
791
|
+
}
|
|
792
|
+
const globalSettings = SettingsManager.inMemory(storedSettings.getGlobalSettings(), {
|
|
793
|
+
projectTrusted: false
|
|
794
|
+
});
|
|
795
|
+
return new DefaultPackageManager({
|
|
796
|
+
cwd: agentDirectory,
|
|
797
|
+
agentDir: agentDirectory,
|
|
798
|
+
settingsManager: globalSettings
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
async function updateQlooExtensions(source, options) {
|
|
802
|
+
const paths = resolveQlooPaths({ env: options.env });
|
|
803
|
+
const packageManager = options.createPackageManager(paths.agentDir);
|
|
804
|
+
const configured = packageManager.listConfiguredPackages();
|
|
805
|
+
if (configured.length === 0 && source === void 0) {
|
|
806
|
+
options.writeOut("No Qloo extension packages are configured.\n");
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (isOfflineModeEnabled(options.env)) {
|
|
810
|
+
throw new Error("PI_OFFLINE is enabled; Qloo extension packages cannot be updated offline");
|
|
811
|
+
}
|
|
812
|
+
packageManager.setProgressCallback((event) => {
|
|
813
|
+
if (event.type === "start") {
|
|
814
|
+
options.writeOut(`${sanitizeTerminalText(event.message ?? `Updating ${event.source}...`)}
|
|
815
|
+
`);
|
|
816
|
+
} else if (event.type === "complete") {
|
|
817
|
+
options.writeOut(`Updated ${sanitizeTerminalText(event.source)}.
|
|
818
|
+
`);
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
try {
|
|
822
|
+
await packageManager.update(source);
|
|
823
|
+
} finally {
|
|
824
|
+
packageManager.setProgressCallback(void 0);
|
|
825
|
+
}
|
|
826
|
+
if (source) {
|
|
827
|
+
options.writeOut(`Checked Qloo extension package ${sanitizeTerminalText(source)}.
|
|
828
|
+
`);
|
|
829
|
+
} else {
|
|
830
|
+
const noun = configured.length === 1 ? "package" : "packages";
|
|
831
|
+
options.writeOut(`Checked ${configured.length} configured Qloo extension ${noun}; pinned and local sources remain unchanged.
|
|
832
|
+
`);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function defaultWriteOut(text) {
|
|
836
|
+
process.stdout.write(text);
|
|
837
|
+
}
|
|
838
|
+
function defaultWriteError(text) {
|
|
839
|
+
process.stderr.write(text);
|
|
840
|
+
}
|
|
841
|
+
async function runQlooUpdate(args, options = {}) {
|
|
842
|
+
const writeOut = options.writeOut ?? defaultWriteOut;
|
|
843
|
+
const writeError = options.writeError ?? defaultWriteError;
|
|
844
|
+
let parsed;
|
|
845
|
+
try {
|
|
846
|
+
parsed = parseUpdateArguments(args);
|
|
847
|
+
} catch (error) {
|
|
848
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
849
|
+
writeError(`qloo update: ${sanitizeTerminalText(message)}
|
|
850
|
+
Run qloo update --help for usage.
|
|
851
|
+
`);
|
|
852
|
+
return 2;
|
|
853
|
+
}
|
|
854
|
+
if (parsed.help) {
|
|
855
|
+
writeOut(`${QLOO_UPDATE_HELP}
|
|
856
|
+
`);
|
|
857
|
+
return 0;
|
|
858
|
+
}
|
|
859
|
+
const env = options.env ?? process.env;
|
|
860
|
+
const cwd = options.cwd ?? process.cwd();
|
|
861
|
+
const platform = options.platform ?? process.platform;
|
|
862
|
+
const runCommand = options.runCommand ?? runExternalCommand;
|
|
863
|
+
const createExtensionPackageManager = options.createExtensionPackageManager ?? createDefaultExtensionPackageManager;
|
|
864
|
+
const extensionUpdate = options.extensionUpdate ?? ((source) => updateQlooExtensions(source, {
|
|
865
|
+
env,
|
|
866
|
+
writeOut,
|
|
867
|
+
createPackageManager: createExtensionPackageManager
|
|
868
|
+
}));
|
|
869
|
+
const selfUpdate = options.selfUpdate ?? ((force, timeoutMs) => updateQlooSelf({
|
|
870
|
+
currentVersion: options.currentVersion ?? HARNESS_VERSION,
|
|
871
|
+
packageRoot: options.packageRoot ?? DEFAULT_PACKAGE_ROOT,
|
|
872
|
+
cwd,
|
|
873
|
+
env,
|
|
874
|
+
platform,
|
|
875
|
+
npmCommand: options.npmCommand ?? (platform === "win32" ? "npm.cmd" : "npm"),
|
|
876
|
+
pnpmCommand: options.pnpmCommand ?? (platform === "win32" ? "pnpm.cmd" : "pnpm"),
|
|
877
|
+
force,
|
|
878
|
+
timeoutMs,
|
|
879
|
+
writeOut,
|
|
880
|
+
writeError,
|
|
881
|
+
runCommand
|
|
882
|
+
}));
|
|
883
|
+
try {
|
|
884
|
+
if (parsed.extensions)
|
|
885
|
+
await extensionUpdate(parsed.extensionSource);
|
|
886
|
+
if (parsed.self)
|
|
887
|
+
await selfUpdate(parsed.force, parsed.timeoutMs);
|
|
888
|
+
return 0;
|
|
889
|
+
} catch (error) {
|
|
890
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
891
|
+
writeError(`qloo update: ${sanitizeTerminalText(message)}
|
|
892
|
+
`);
|
|
893
|
+
return 1;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
export {
|
|
897
|
+
QLOO_HARNESS_PACKAGE_NAME,
|
|
898
|
+
QLOO_UPDATE_HELP,
|
|
899
|
+
QLOO_UPDATE_SPEC_ENVIRONMENT_VARIABLE,
|
|
900
|
+
acquireQlooUpdateLock,
|
|
901
|
+
inferGlobalInstall,
|
|
902
|
+
inferGlobalInstallPrefix,
|
|
903
|
+
inferPackageRootFromEntrypoint,
|
|
904
|
+
runExternalCommand,
|
|
905
|
+
runQlooUpdate
|
|
906
|
+
};
|