jouzu 0.0.1 → 0.1.0
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/README.md +50 -0
- package/dist/args.d.ts +45 -0
- package/dist/args.d.ts.map +1 -0
- package/dist/args.js +221 -0
- package/dist/args.js.map +1 -0
- package/dist/cli.js +250 -29
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +36 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +224 -0
- package/dist/doctor.js.map +1 -0
- package/dist/keybindings.d.ts +68 -0
- package/dist/keybindings.d.ts.map +1 -0
- package/dist/keybindings.js +460 -0
- package/dist/keybindings.js.map +1 -0
- package/dist/metadata.d.ts +21 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +22 -0
- package/dist/metadata.js.map +1 -0
- package/dist/paths.d.ts +17 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +65 -0
- package/dist/paths.js.map +1 -0
- package/dist/pi.lock.json +15 -0
- package/dist/presentation.d.ts +45 -0
- package/dist/presentation.d.ts.map +1 -0
- package/dist/presentation.js +202 -0
- package/dist/presentation.js.map +1 -0
- package/dist/private-fs.d.ts +12 -0
- package/dist/private-fs.d.ts.map +1 -0
- package/dist/private-fs.js +64 -0
- package/dist/private-fs.js.map +1 -0
- package/dist/profile-choice.d.ts +14 -0
- package/dist/profile-choice.d.ts.map +1 -0
- package/dist/profile-choice.js +83 -0
- package/dist/profile-choice.js.map +1 -0
- package/dist/profile-manager.d.ts +50 -0
- package/dist/profile-manager.d.ts.map +1 -0
- package/dist/profile-manager.js +364 -0
- package/dist/profile-manager.js.map +1 -0
- package/dist/profiles/core/assets/jouzu-core-skill.md +12 -0
- package/dist/profiles/core/assets/jouzu-review.md +1 -0
- package/dist/profiles/core/manifest.json +17 -0
- package/dist/profiles/ja/assets/APPEND_SYSTEM.md +8 -0
- package/dist/profiles/ja/manifest.json +13 -0
- package/dist/profiles.d.ts +18 -0
- package/dist/profiles.d.ts.map +1 -0
- package/dist/profiles.js +153 -0
- package/dist/profiles.js.map +1 -0
- package/dist/resume.d.ts +7 -0
- package/dist/resume.d.ts.map +1 -0
- package/dist/resume.js +23 -0
- package/dist/resume.js.map +1 -0
- package/dist/runtime.d.ts +18 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +59 -0
- package/dist/runtime.js.map +1 -0
- package/dist/state-lock.d.ts +39 -0
- package/dist/state-lock.d.ts.map +1 -0
- package/dist/state-lock.js +143 -0
- package/dist/state-lock.js.map +1 -0
- package/dist/updater.d.ts +113 -0
- package/dist/updater.d.ts.map +1 -0
- package/dist/updater.js +698 -0
- package/dist/updater.js.map +1 -0
- package/package.json +7 -4
package/dist/updater.js
ADDED
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { closeSync, existsSync, fsyncSync, lstatSync, openSync, readFileSync, realpathSync, renameSync, rmdirSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import { dirname, join, posix, resolve, win32 } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { ensurePrivateDirectory, validatePrivateDirectory } from "./private-fs.js";
|
|
7
|
+
import { acquireStateLock, STATE_LOCK_STALE_MS } from "./state-lock.js";
|
|
8
|
+
const PI_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
9
|
+
const UPDATE_STATE_FIELDS = new Set([
|
|
10
|
+
"schemaVersion",
|
|
11
|
+
"policy",
|
|
12
|
+
"channel",
|
|
13
|
+
"lastCheckedAt",
|
|
14
|
+
"nextCheckAt",
|
|
15
|
+
"lastResult",
|
|
16
|
+
"installedVersion",
|
|
17
|
+
"latestVersion",
|
|
18
|
+
"latestIntegrity",
|
|
19
|
+
"previousVersion",
|
|
20
|
+
"lastUpdatedAt",
|
|
21
|
+
"lastErrorCode",
|
|
22
|
+
]);
|
|
23
|
+
const UPDATE_POLICIES = ["auto-restart", "notify", "off"];
|
|
24
|
+
const UPDATE_RESULTS = ["never", "current", "available", "updated", "failed"];
|
|
25
|
+
const DEFAULT_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
26
|
+
const FAILED_CHECK_INTERVAL_MS = 60 * 60 * 1000;
|
|
27
|
+
const SRI_SHA512_RE = /^sha512-[A-Za-z0-9+/]+={0,2}$/;
|
|
28
|
+
const SEMVER_RE = /^(?:v)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
29
|
+
export class UpdateError extends Error {
|
|
30
|
+
code;
|
|
31
|
+
exitCode;
|
|
32
|
+
fatal;
|
|
33
|
+
constructor(message, code, options = {}) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "UpdateError";
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.exitCode = options.exitCode ?? 4;
|
|
38
|
+
this.fatal = options.fatal ?? false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function exactFields(value, fields) {
|
|
42
|
+
return Object.keys(value).every((key) => fields.has(key)) && Object.keys(value).length === fields.size;
|
|
43
|
+
}
|
|
44
|
+
function validTimestamp(value) {
|
|
45
|
+
return value === null || (typeof value === "string" && Number.isFinite(Date.parse(value)));
|
|
46
|
+
}
|
|
47
|
+
function parseSemver(value) {
|
|
48
|
+
const match = SEMVER_RE.exec(value);
|
|
49
|
+
if (!match)
|
|
50
|
+
throw new UpdateError(`invalid semantic version: ${value}`, "invalid-version");
|
|
51
|
+
const [major, minor, patch] = match.slice(1, 4).map(Number);
|
|
52
|
+
if (![major, minor, patch].every(Number.isSafeInteger)) {
|
|
53
|
+
throw new UpdateError(`semantic version components exceed the safe integer range: ${value}`, "invalid-version");
|
|
54
|
+
}
|
|
55
|
+
const prerelease = match[4]
|
|
56
|
+
? match[4].split(".").map((part) => {
|
|
57
|
+
if (/^\d+$/.test(part)) {
|
|
58
|
+
if (!/^(?:0|[1-9]\d*)$/.test(part)) {
|
|
59
|
+
throw new UpdateError(`semantic version has a zero-padded prerelease number: ${value}`, "invalid-version");
|
|
60
|
+
}
|
|
61
|
+
const numeric = Number(part);
|
|
62
|
+
if (!Number.isSafeInteger(numeric)) {
|
|
63
|
+
throw new UpdateError(`semantic version prerelease exceeds the safe integer range: ${value}`, "invalid-version");
|
|
64
|
+
}
|
|
65
|
+
return numeric;
|
|
66
|
+
}
|
|
67
|
+
return part;
|
|
68
|
+
})
|
|
69
|
+
: [];
|
|
70
|
+
return { major, minor, patch, prerelease };
|
|
71
|
+
}
|
|
72
|
+
export function compareSemver(left, right) {
|
|
73
|
+
const a = parseSemver(left);
|
|
74
|
+
const b = parseSemver(right);
|
|
75
|
+
for (const field of ["major", "minor", "patch"]) {
|
|
76
|
+
if (a[field] !== b[field])
|
|
77
|
+
return a[field] < b[field] ? -1 : 1;
|
|
78
|
+
}
|
|
79
|
+
if (a.prerelease.length === 0 && b.prerelease.length === 0)
|
|
80
|
+
return 0;
|
|
81
|
+
if (a.prerelease.length === 0)
|
|
82
|
+
return 1;
|
|
83
|
+
if (b.prerelease.length === 0)
|
|
84
|
+
return -1;
|
|
85
|
+
for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index += 1) {
|
|
86
|
+
const leftPart = a.prerelease[index];
|
|
87
|
+
const rightPart = b.prerelease[index];
|
|
88
|
+
if (leftPart === undefined)
|
|
89
|
+
return -1;
|
|
90
|
+
if (rightPart === undefined)
|
|
91
|
+
return 1;
|
|
92
|
+
if (leftPart === rightPart)
|
|
93
|
+
continue;
|
|
94
|
+
if (typeof leftPart === "number" && typeof rightPart === "string")
|
|
95
|
+
return -1;
|
|
96
|
+
if (typeof leftPart === "string" && typeof rightPart === "number")
|
|
97
|
+
return 1;
|
|
98
|
+
return leftPart < rightPart ? -1 : 1;
|
|
99
|
+
}
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
function defaultState(currentVersion) {
|
|
103
|
+
parseSemver(currentVersion);
|
|
104
|
+
return {
|
|
105
|
+
schemaVersion: 1,
|
|
106
|
+
policy: "auto-restart",
|
|
107
|
+
channel: "latest",
|
|
108
|
+
lastCheckedAt: null,
|
|
109
|
+
nextCheckAt: null,
|
|
110
|
+
lastResult: "never",
|
|
111
|
+
installedVersion: currentVersion,
|
|
112
|
+
latestVersion: null,
|
|
113
|
+
latestIntegrity: null,
|
|
114
|
+
previousVersion: null,
|
|
115
|
+
lastUpdatedAt: null,
|
|
116
|
+
lastErrorCode: null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export function updateStatePath(paths) {
|
|
120
|
+
return join(paths.stateDir, "self-update.json");
|
|
121
|
+
}
|
|
122
|
+
function updateLockPath(paths) {
|
|
123
|
+
return join(paths.stateDir, "self-update.lock");
|
|
124
|
+
}
|
|
125
|
+
export function readUpdateState(path, currentVersion) {
|
|
126
|
+
try {
|
|
127
|
+
validatePrivateDirectory(dirname(path));
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
throw new UpdateError(error instanceof Error ? error.message : String(error), "invalid-state");
|
|
131
|
+
}
|
|
132
|
+
if (!existsSync(path))
|
|
133
|
+
return defaultState(currentVersion);
|
|
134
|
+
const metadata = lstatSync(path);
|
|
135
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
136
|
+
throw new UpdateError("self-update state must be a regular file", "invalid-state");
|
|
137
|
+
}
|
|
138
|
+
let parsed;
|
|
139
|
+
try {
|
|
140
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
throw new UpdateError(`self-update state is invalid JSON: ${error instanceof Error ? error.message : error}`, "invalid-state");
|
|
144
|
+
}
|
|
145
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
146
|
+
throw new UpdateError("self-update state must be an object", "invalid-state");
|
|
147
|
+
}
|
|
148
|
+
const value = parsed;
|
|
149
|
+
if (!exactFields(value, UPDATE_STATE_FIELDS)) {
|
|
150
|
+
throw new UpdateError("self-update state has missing or unknown fields", "invalid-state");
|
|
151
|
+
}
|
|
152
|
+
if (value.schemaVersion !== 1 ||
|
|
153
|
+
!UPDATE_POLICIES.includes(value.policy) ||
|
|
154
|
+
value.channel !== "latest" ||
|
|
155
|
+
!validTimestamp(value.lastCheckedAt) ||
|
|
156
|
+
!validTimestamp(value.nextCheckAt) ||
|
|
157
|
+
!UPDATE_RESULTS.includes(value.lastResult) ||
|
|
158
|
+
typeof value.installedVersion !== "string" ||
|
|
159
|
+
(value.latestVersion !== null && typeof value.latestVersion !== "string") ||
|
|
160
|
+
(value.latestIntegrity !== null &&
|
|
161
|
+
(typeof value.latestIntegrity !== "string" || !SRI_SHA512_RE.test(value.latestIntegrity))) ||
|
|
162
|
+
(value.previousVersion !== null && typeof value.previousVersion !== "string") ||
|
|
163
|
+
!validTimestamp(value.lastUpdatedAt) ||
|
|
164
|
+
(value.lastErrorCode !== null && typeof value.lastErrorCode !== "string")) {
|
|
165
|
+
throw new UpdateError("self-update state fields are invalid", "invalid-state");
|
|
166
|
+
}
|
|
167
|
+
if ((value.latestVersion === null) !== (value.latestIntegrity === null)) {
|
|
168
|
+
throw new UpdateError("self-update state must pair latest version and integrity", "invalid-state");
|
|
169
|
+
}
|
|
170
|
+
if (value.lastResult === "available" && value.latestVersion === null) {
|
|
171
|
+
throw new UpdateError("available self-update state is missing release metadata", "invalid-state");
|
|
172
|
+
}
|
|
173
|
+
parseSemver(value.installedVersion);
|
|
174
|
+
if (typeof value.latestVersion === "string")
|
|
175
|
+
parseSemver(value.latestVersion);
|
|
176
|
+
if (typeof value.previousVersion === "string")
|
|
177
|
+
parseSemver(value.previousVersion);
|
|
178
|
+
return value;
|
|
179
|
+
}
|
|
180
|
+
function atomicWrite(path, content) {
|
|
181
|
+
ensurePrivateDirectory(dirname(path));
|
|
182
|
+
const temporary = join(dirname(path), `.${randomUUID()}.tmp`);
|
|
183
|
+
let descriptor;
|
|
184
|
+
try {
|
|
185
|
+
descriptor = openSync(temporary, "wx", 0o600);
|
|
186
|
+
writeFileSync(descriptor, content);
|
|
187
|
+
fsyncSync(descriptor);
|
|
188
|
+
closeSync(descriptor);
|
|
189
|
+
descriptor = undefined;
|
|
190
|
+
renameSync(temporary, path);
|
|
191
|
+
}
|
|
192
|
+
finally {
|
|
193
|
+
if (descriptor !== undefined)
|
|
194
|
+
closeSync(descriptor);
|
|
195
|
+
rmSync(temporary, { force: true });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function writeUpdateState(path, state) {
|
|
199
|
+
atomicWrite(path, `${JSON.stringify(state, null, 2)}\n`);
|
|
200
|
+
}
|
|
201
|
+
function envFlagIsTrue(value) {
|
|
202
|
+
return value !== undefined && ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
203
|
+
}
|
|
204
|
+
function effectivePolicy(state, env) {
|
|
205
|
+
if (envFlagIsTrue(env.JOUZU_NO_UPDATE))
|
|
206
|
+
return "off";
|
|
207
|
+
const override = env.JOUZU_UPDATE_POLICY;
|
|
208
|
+
if (override !== undefined) {
|
|
209
|
+
return UPDATE_POLICIES.includes(override) ? override : "off";
|
|
210
|
+
}
|
|
211
|
+
return state.policy;
|
|
212
|
+
}
|
|
213
|
+
function normalizePathForPlatform(path, platform) {
|
|
214
|
+
const pathApi = platform === "win32" ? win32 : posix;
|
|
215
|
+
const normalized = pathApi.resolve(path).replaceAll("\\", "/").replace(/\/$/, "");
|
|
216
|
+
return platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
217
|
+
}
|
|
218
|
+
export function classifyInstallChannel(options) {
|
|
219
|
+
const platform = options.platform ?? process.platform;
|
|
220
|
+
const packageRoot = normalizePathForPlatform(options.packageRoot, platform);
|
|
221
|
+
if (packageRoot.endsWith("/packages/cli"))
|
|
222
|
+
return "source";
|
|
223
|
+
if (packageRoot.includes("/_npx/"))
|
|
224
|
+
return "ephemeral-npx";
|
|
225
|
+
if (options.globalNpmRoot) {
|
|
226
|
+
const globalPackage = normalizePathForPlatform((platform === "win32" ? win32 : posix).join(options.globalNpmRoot, "jouzu"), platform);
|
|
227
|
+
if (packageRoot === globalPackage)
|
|
228
|
+
return "global-npm";
|
|
229
|
+
}
|
|
230
|
+
if (packageRoot.includes("/node_modules/jouzu"))
|
|
231
|
+
return "local-npm";
|
|
232
|
+
return "other";
|
|
233
|
+
}
|
|
234
|
+
function canonicalExistingPath(path) {
|
|
235
|
+
try {
|
|
236
|
+
return realpathSync(path);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return path;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function defaultPackageRoot() {
|
|
243
|
+
return dirname(fileURLToPath(new URL("../package.json", import.meta.url)));
|
|
244
|
+
}
|
|
245
|
+
function defaultNpmRunner(env, platform) {
|
|
246
|
+
return (args, timeoutMs) => {
|
|
247
|
+
const command = platform === "win32" ? (env.ComSpec ?? "cmd.exe") : "npm";
|
|
248
|
+
const prefix = platform === "win32" ? ["/d", "/s", "/c", "npm"] : [];
|
|
249
|
+
const result = spawnSync(command, [...prefix, ...args], {
|
|
250
|
+
encoding: "utf8",
|
|
251
|
+
timeout: timeoutMs,
|
|
252
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
253
|
+
env: { ...env, npm_config_update_notifier: "false" },
|
|
254
|
+
});
|
|
255
|
+
return {
|
|
256
|
+
status: result.status,
|
|
257
|
+
stdout: result.stdout ?? "",
|
|
258
|
+
stderr: result.stderr ?? "",
|
|
259
|
+
...(result.error ? { error: result.error } : {}),
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function requireCommandSuccess(result, operation) {
|
|
264
|
+
if (result.error) {
|
|
265
|
+
const code = "code" in result.error && typeof result.error.code === "string" ? result.error.code : "spawn";
|
|
266
|
+
throw new UpdateError(`${operation} failed (${code})`, `npm-${operation}-${code}`);
|
|
267
|
+
}
|
|
268
|
+
if (result.status !== 0) {
|
|
269
|
+
throw new UpdateError(`${operation} failed (npm exit ${result.status ?? "unknown"})`, `npm-${operation}-exit`);
|
|
270
|
+
}
|
|
271
|
+
return result.stdout;
|
|
272
|
+
}
|
|
273
|
+
function parseRegistryRelease(stdout) {
|
|
274
|
+
let parsed;
|
|
275
|
+
try {
|
|
276
|
+
parsed = JSON.parse(stdout);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
throw new UpdateError("npm returned invalid update metadata", "invalid-registry-metadata");
|
|
280
|
+
}
|
|
281
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
282
|
+
throw new UpdateError("npm returned invalid update metadata", "invalid-registry-metadata");
|
|
283
|
+
}
|
|
284
|
+
const value = parsed;
|
|
285
|
+
const version = value.version;
|
|
286
|
+
const integrity = value["dist.integrity"];
|
|
287
|
+
if (typeof version !== "string" || typeof integrity !== "string" || !SRI_SHA512_RE.test(integrity)) {
|
|
288
|
+
throw new UpdateError("npm update metadata is missing version or SHA-512 integrity", "invalid-registry-metadata");
|
|
289
|
+
}
|
|
290
|
+
parseSemver(version);
|
|
291
|
+
return { version, integrity };
|
|
292
|
+
}
|
|
293
|
+
function parsePackedArtifact(stdout, directory, expectedVersion) {
|
|
294
|
+
let parsed;
|
|
295
|
+
try {
|
|
296
|
+
parsed = JSON.parse(stdout);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
throw new UpdateError("npm pack returned invalid metadata", "invalid-pack-metadata");
|
|
300
|
+
}
|
|
301
|
+
if (!Array.isArray(parsed) || parsed.length !== 1 || !parsed[0] || typeof parsed[0] !== "object") {
|
|
302
|
+
throw new UpdateError("npm pack returned invalid metadata", "invalid-pack-metadata");
|
|
303
|
+
}
|
|
304
|
+
const value = parsed[0];
|
|
305
|
+
if (value.name !== "jouzu" ||
|
|
306
|
+
typeof value.version !== "string" ||
|
|
307
|
+
(expectedVersion !== undefined && value.version !== expectedVersion) ||
|
|
308
|
+
typeof value.integrity !== "string" ||
|
|
309
|
+
!SRI_SHA512_RE.test(value.integrity) ||
|
|
310
|
+
typeof value.filename !== "string" ||
|
|
311
|
+
value.filename !== posix.basename(value.filename) ||
|
|
312
|
+
value.filename !== win32.basename(value.filename)) {
|
|
313
|
+
throw new UpdateError("npm pack metadata does not describe the expected Jouzu artifact", "invalid-pack-metadata");
|
|
314
|
+
}
|
|
315
|
+
return { path: join(directory, value.filename), integrity: value.integrity };
|
|
316
|
+
}
|
|
317
|
+
function verifyFileIntegrity(path, integrity) {
|
|
318
|
+
const metadata = lstatSync(path);
|
|
319
|
+
if (!metadata.isFile() || metadata.isSymbolicLink()) {
|
|
320
|
+
throw new UpdateError("downloaded update is not a regular file", "invalid-update-artifact");
|
|
321
|
+
}
|
|
322
|
+
const observed = `sha512-${createHash("sha512").update(readFileSync(path)).digest("base64")}`;
|
|
323
|
+
if (observed !== integrity)
|
|
324
|
+
throw new UpdateError("downloaded update failed SHA-512 verification", "integrity-mismatch");
|
|
325
|
+
}
|
|
326
|
+
function acquireUpdateLock(path, now) {
|
|
327
|
+
return acquireStateLock({
|
|
328
|
+
path,
|
|
329
|
+
now,
|
|
330
|
+
staleMs: STATE_LOCK_STALE_MS,
|
|
331
|
+
describe: "self-update",
|
|
332
|
+
onBusy: (inspection) => new UpdateError(inspection.status === "invalid" ? "self-update lock is not a regular file" : "another Jouzu update is running", "update-busy"),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function defaultVerifyInstalled(packageRoot, executable, expectedVersion, env) {
|
|
336
|
+
const packageJson = JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8"));
|
|
337
|
+
const piLock = JSON.parse(readFileSync(join(packageRoot, "dist", "pi.lock.json"), "utf8"));
|
|
338
|
+
const piDependency = packageJson.dependencies?.[PI_PACKAGE];
|
|
339
|
+
const piLockVersion = piLock.packages?.[PI_PACKAGE]?.version;
|
|
340
|
+
if (packageJson.name !== "jouzu" ||
|
|
341
|
+
packageJson.version !== expectedVersion ||
|
|
342
|
+
typeof piDependency !== "string" ||
|
|
343
|
+
piDependency !== piLockVersion) {
|
|
344
|
+
throw new UpdateError("installed update metadata failed verification", "installed-metadata-mismatch");
|
|
345
|
+
}
|
|
346
|
+
const cliBytes = readFileSync(executable, "utf8");
|
|
347
|
+
if (!cliBytes.startsWith("#!/usr/bin/env node\n")) {
|
|
348
|
+
throw new UpdateError("installed update CLI failed verification", "installed-cli-invalid");
|
|
349
|
+
}
|
|
350
|
+
const result = spawnSync(process.execPath, [executable, "--version"], {
|
|
351
|
+
encoding: "utf8",
|
|
352
|
+
timeout: 30_000,
|
|
353
|
+
env: { ...env, JOUZU_NO_UPDATE: "1" },
|
|
354
|
+
});
|
|
355
|
+
if (result.status !== 0 || !result.stdout.startsWith(`jouzu ${expectedVersion}\n`)) {
|
|
356
|
+
throw new UpdateError("installed update runtime smoke failed", "installed-runtime-invalid");
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function checkIntervalMs(env) {
|
|
360
|
+
const raw = env.JOUZU_UPDATE_INTERVAL_HOURS;
|
|
361
|
+
if (!raw)
|
|
362
|
+
return DEFAULT_CHECK_INTERVAL_MS;
|
|
363
|
+
const hours = Number(raw);
|
|
364
|
+
return Number.isFinite(hours) && hours > 0 ? hours * 60 * 60 * 1000 : DEFAULT_CHECK_INTERVAL_MS;
|
|
365
|
+
}
|
|
366
|
+
function checkIsDue(state, now) {
|
|
367
|
+
return state.nextCheckAt === null || Date.parse(state.nextCheckAt) <= now.getTime();
|
|
368
|
+
}
|
|
369
|
+
export class JouzuUpdater {
|
|
370
|
+
paths;
|
|
371
|
+
currentVersion;
|
|
372
|
+
executable;
|
|
373
|
+
packageRoot;
|
|
374
|
+
env;
|
|
375
|
+
platform;
|
|
376
|
+
now;
|
|
377
|
+
runNpm;
|
|
378
|
+
verifyInstalled;
|
|
379
|
+
report;
|
|
380
|
+
cachedInstallChannel;
|
|
381
|
+
constructor(options) {
|
|
382
|
+
parseSemver(options.currentVersion);
|
|
383
|
+
this.paths = options.paths;
|
|
384
|
+
this.currentVersion = options.currentVersion;
|
|
385
|
+
this.executable = resolve(options.executable);
|
|
386
|
+
this.packageRoot = canonicalExistingPath(options.packageRoot ?? defaultPackageRoot());
|
|
387
|
+
this.env = options.env ?? process.env;
|
|
388
|
+
this.platform = options.platform ?? process.platform;
|
|
389
|
+
this.now = options.now ?? (() => new Date());
|
|
390
|
+
this.runNpm = options.runNpm ?? defaultNpmRunner(this.env, this.platform);
|
|
391
|
+
this.verifyInstalled =
|
|
392
|
+
options.verifyInstalled ??
|
|
393
|
+
((expectedVersion) => defaultVerifyInstalled(this.packageRoot, this.executable, expectedVersion, this.env));
|
|
394
|
+
this.report = options.report ?? (() => { });
|
|
395
|
+
}
|
|
396
|
+
readState() {
|
|
397
|
+
return readUpdateState(updateStatePath(this.paths), this.currentVersion);
|
|
398
|
+
}
|
|
399
|
+
writeState(state) {
|
|
400
|
+
writeUpdateState(updateStatePath(this.paths), state);
|
|
401
|
+
}
|
|
402
|
+
recordFailure(code, now, checked = false) {
|
|
403
|
+
try {
|
|
404
|
+
const state = this.readState();
|
|
405
|
+
this.writeState({
|
|
406
|
+
...state,
|
|
407
|
+
lastCheckedAt: checked ? now.toISOString() : state.lastCheckedAt,
|
|
408
|
+
lastResult: "failed",
|
|
409
|
+
nextCheckAt: new Date(now.getTime() + FAILED_CHECK_INTERVAL_MS).toISOString(),
|
|
410
|
+
installedVersion: this.currentVersion,
|
|
411
|
+
lastErrorCode: code,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
catch { }
|
|
415
|
+
}
|
|
416
|
+
globalNpmRoot() {
|
|
417
|
+
try {
|
|
418
|
+
const root = requireCommandSuccess(this.runNpm(["root", "--global", "--loglevel=error"], 15_000), "root").trim();
|
|
419
|
+
return canonicalExistingPath(root);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
installChannel() {
|
|
426
|
+
this.cachedInstallChannel ??= classifyInstallChannel({
|
|
427
|
+
packageRoot: this.packageRoot,
|
|
428
|
+
globalNpmRoot: this.globalNpmRoot(),
|
|
429
|
+
platform: this.platform,
|
|
430
|
+
});
|
|
431
|
+
return this.cachedInstallChannel;
|
|
432
|
+
}
|
|
433
|
+
status() {
|
|
434
|
+
const state = this.readState();
|
|
435
|
+
const policy = effectivePolicy(state, this.env);
|
|
436
|
+
const installChannel = this.installChannel();
|
|
437
|
+
return {
|
|
438
|
+
policy,
|
|
439
|
+
installChannel,
|
|
440
|
+
startupEligible: installChannel === "global-npm" && policy !== "off",
|
|
441
|
+
state: { ...state, installedVersion: this.currentVersion },
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
setPolicy(policy) {
|
|
445
|
+
if (!UPDATE_POLICIES.includes(policy))
|
|
446
|
+
throw new UpdateError(`unknown update policy: ${policy}`, "invalid-policy", { exitCode: 2 });
|
|
447
|
+
const state = this.readState();
|
|
448
|
+
const updated = { ...state, policy };
|
|
449
|
+
this.writeState(updated);
|
|
450
|
+
return updated;
|
|
451
|
+
}
|
|
452
|
+
check() {
|
|
453
|
+
const now = this.now();
|
|
454
|
+
try {
|
|
455
|
+
const stdout = requireCommandSuccess(this.runNpm([
|
|
456
|
+
"view",
|
|
457
|
+
"jouzu@latest",
|
|
458
|
+
"version",
|
|
459
|
+
"dist.integrity",
|
|
460
|
+
"--json",
|
|
461
|
+
"--fetch-retries=0",
|
|
462
|
+
"--fetch-timeout=8000",
|
|
463
|
+
"--loglevel=error",
|
|
464
|
+
], 12_000), "view");
|
|
465
|
+
const release = parseRegistryRelease(stdout);
|
|
466
|
+
const status = compareSemver(release.version, this.currentVersion) > 0 ? "available" : "current";
|
|
467
|
+
const state = this.readState();
|
|
468
|
+
this.writeState({
|
|
469
|
+
...state,
|
|
470
|
+
lastCheckedAt: now.toISOString(),
|
|
471
|
+
nextCheckAt: new Date(now.getTime() + checkIntervalMs(this.env)).toISOString(),
|
|
472
|
+
lastResult: status,
|
|
473
|
+
installedVersion: this.currentVersion,
|
|
474
|
+
latestVersion: release.version,
|
|
475
|
+
latestIntegrity: release.integrity,
|
|
476
|
+
lastErrorCode: null,
|
|
477
|
+
});
|
|
478
|
+
return { status, installedVersion: this.currentVersion, ...release };
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
const updateError = error instanceof UpdateError ? error : new UpdateError("update check failed", "check-failed");
|
|
482
|
+
this.recordFailure(updateError.code, now, true);
|
|
483
|
+
throw updateError;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
createDownloadDirectory() {
|
|
487
|
+
const directory = join(this.paths.cacheDir, `self-update-${randomUUID()}`);
|
|
488
|
+
ensurePrivateDirectory(this.paths.cacheDir, directory);
|
|
489
|
+
return directory;
|
|
490
|
+
}
|
|
491
|
+
pack(specifier, directory, expectedVersion) {
|
|
492
|
+
const stdout = requireCommandSuccess(this.runNpm([
|
|
493
|
+
"pack",
|
|
494
|
+
specifier,
|
|
495
|
+
"--ignore-scripts",
|
|
496
|
+
"--json",
|
|
497
|
+
"--pack-destination",
|
|
498
|
+
directory,
|
|
499
|
+
"--fetch-retries=1",
|
|
500
|
+
"--fetch-timeout=15000",
|
|
501
|
+
"--loglevel=error",
|
|
502
|
+
], 60_000), "pack");
|
|
503
|
+
return parsePackedArtifact(stdout, directory, expectedVersion);
|
|
504
|
+
}
|
|
505
|
+
installTarball(path) {
|
|
506
|
+
requireCommandSuccess(this.runNpm(["install", "--global", path, "--ignore-scripts", "--no-audit", "--no-fund", "--loglevel=error"], 300_000), "install");
|
|
507
|
+
}
|
|
508
|
+
apply(checkResult) {
|
|
509
|
+
if (this.installChannel() !== "global-npm") {
|
|
510
|
+
throw new UpdateError("automatic installation is available only for a real global npm install; update this source, local, or npx invocation through its owning channel", "unsupported-install-channel");
|
|
511
|
+
}
|
|
512
|
+
const check = checkResult ?? this.check();
|
|
513
|
+
if (check.status === "current")
|
|
514
|
+
return { changed: false, version: this.currentVersion };
|
|
515
|
+
const now = this.now();
|
|
516
|
+
const releaseLock = acquireUpdateLock(updateLockPath(this.paths), now);
|
|
517
|
+
let directory;
|
|
518
|
+
let backup;
|
|
519
|
+
let candidate;
|
|
520
|
+
let installedAndVerified = false;
|
|
521
|
+
try {
|
|
522
|
+
directory = this.createDownloadDirectory();
|
|
523
|
+
this.report(`Downloading Jouzu ${check.version}…`);
|
|
524
|
+
backup = this.pack(this.packageRoot, directory, this.currentVersion);
|
|
525
|
+
verifyFileIntegrity(backup.path, backup.integrity);
|
|
526
|
+
candidate = this.pack(`jouzu@${check.version}`, directory, check.version);
|
|
527
|
+
if (candidate.integrity !== check.integrity) {
|
|
528
|
+
throw new UpdateError("npm metadata changed during update download", "registry-integrity-drift");
|
|
529
|
+
}
|
|
530
|
+
verifyFileIntegrity(candidate.path, check.integrity);
|
|
531
|
+
this.report(`Installing Jouzu ${check.version}…`);
|
|
532
|
+
try {
|
|
533
|
+
this.installTarball(candidate.path);
|
|
534
|
+
this.verifyInstalled(check.version);
|
|
535
|
+
installedAndVerified = true;
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
let currentRemainsVerified = false;
|
|
539
|
+
try {
|
|
540
|
+
this.verifyInstalled(this.currentVersion);
|
|
541
|
+
currentRemainsVerified = true;
|
|
542
|
+
}
|
|
543
|
+
catch { }
|
|
544
|
+
if (currentRemainsVerified) {
|
|
545
|
+
throw new UpdateError(`Jouzu ${check.version} was not installed; ${this.currentVersion} remains verified`, "update-not-installed");
|
|
546
|
+
}
|
|
547
|
+
this.report(`Jouzu ${check.version} failed verification; restoring ${this.currentVersion}…`);
|
|
548
|
+
try {
|
|
549
|
+
verifyFileIntegrity(backup.path, backup.integrity);
|
|
550
|
+
this.installTarball(backup.path);
|
|
551
|
+
this.verifyInstalled(this.currentVersion);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
throw new UpdateError("Jouzu update and automatic rollback both failed; reinstall the previous Jouzu version with npm", "rollback-failed", { fatal: true });
|
|
555
|
+
}
|
|
556
|
+
throw new UpdateError(`Jouzu ${check.version} failed verification and ${this.currentVersion} was restored`, "update-rolled-back");
|
|
557
|
+
}
|
|
558
|
+
try {
|
|
559
|
+
const state = this.readState();
|
|
560
|
+
this.writeState({
|
|
561
|
+
...state,
|
|
562
|
+
lastResult: "updated",
|
|
563
|
+
installedVersion: check.version,
|
|
564
|
+
latestVersion: check.version,
|
|
565
|
+
latestIntegrity: check.integrity,
|
|
566
|
+
previousVersion: this.currentVersion,
|
|
567
|
+
lastUpdatedAt: now.toISOString(),
|
|
568
|
+
lastErrorCode: null,
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
this.report("Jouzu was updated and verified, but its update status could not be recorded.");
|
|
573
|
+
}
|
|
574
|
+
return { changed: true, version: check.version };
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
if (installedAndVerified)
|
|
578
|
+
return { changed: true, version: check.version };
|
|
579
|
+
const updateError = error instanceof UpdateError ? error : new UpdateError("Jouzu update failed", "update-failed");
|
|
580
|
+
this.recordFailure(updateError.code, now);
|
|
581
|
+
throw updateError;
|
|
582
|
+
}
|
|
583
|
+
finally {
|
|
584
|
+
for (const artifact of [candidate, backup]) {
|
|
585
|
+
if (!artifact)
|
|
586
|
+
continue;
|
|
587
|
+
try {
|
|
588
|
+
unlinkSync(artifact.path);
|
|
589
|
+
}
|
|
590
|
+
catch { }
|
|
591
|
+
}
|
|
592
|
+
if (directory) {
|
|
593
|
+
try {
|
|
594
|
+
rmdirSync(directory);
|
|
595
|
+
}
|
|
596
|
+
catch { }
|
|
597
|
+
}
|
|
598
|
+
releaseLock();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
startup() {
|
|
602
|
+
if (this.env.JOUZU_INTERNAL_UPDATE_RESTARTED === "1")
|
|
603
|
+
return { action: "continue" };
|
|
604
|
+
let state;
|
|
605
|
+
try {
|
|
606
|
+
state = this.readState();
|
|
607
|
+
}
|
|
608
|
+
catch (error) {
|
|
609
|
+
return {
|
|
610
|
+
action: "continue",
|
|
611
|
+
message: `Automatic update skipped: ${error instanceof Error ? error.message : error}`,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const policy = effectivePolicy(state, this.env);
|
|
615
|
+
if (policy === "off")
|
|
616
|
+
return { action: "continue" };
|
|
617
|
+
if (this.installChannel() !== "global-npm")
|
|
618
|
+
return { action: "continue" };
|
|
619
|
+
let check;
|
|
620
|
+
if (!checkIsDue(state, this.now()) &&
|
|
621
|
+
state.lastResult === "available" &&
|
|
622
|
+
state.latestVersion !== null &&
|
|
623
|
+
state.latestIntegrity !== null &&
|
|
624
|
+
compareSemver(state.latestVersion, this.currentVersion) > 0) {
|
|
625
|
+
check = {
|
|
626
|
+
status: "available",
|
|
627
|
+
installedVersion: this.currentVersion,
|
|
628
|
+
version: state.latestVersion,
|
|
629
|
+
integrity: state.latestIntegrity,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
else if (!checkIsDue(state, this.now())) {
|
|
633
|
+
return { action: "continue" };
|
|
634
|
+
}
|
|
635
|
+
if (!check) {
|
|
636
|
+
try {
|
|
637
|
+
check = this.check();
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
return {
|
|
641
|
+
action: "continue",
|
|
642
|
+
message: `Jouzu update check failed; continuing with ${this.currentVersion} (${error instanceof UpdateError ? error.code : "unknown"})`,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (check.status === "current")
|
|
647
|
+
return { action: "continue" };
|
|
648
|
+
if (policy === "notify") {
|
|
649
|
+
return {
|
|
650
|
+
action: "continue",
|
|
651
|
+
message: `Jouzu ${check.version} is available; run "jouzu self-update apply"`,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
const applied = this.apply(check);
|
|
656
|
+
return applied.changed ? { action: "restart", version: applied.version } : { action: "continue" };
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
if (error instanceof UpdateError && error.fatal)
|
|
660
|
+
throw error;
|
|
661
|
+
return {
|
|
662
|
+
action: "continue",
|
|
663
|
+
message: `Jouzu update failed; continuing with ${this.currentVersion} (${error instanceof UpdateError ? error.code : "unknown"})`,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
export function relaunchUpdatedJouzu(options) {
|
|
669
|
+
const result = spawnSync(process.execPath, [options.executable, ...options.args], {
|
|
670
|
+
stdio: "inherit",
|
|
671
|
+
env: { ...(options.env ?? process.env), JOUZU_INTERNAL_UPDATE_RESTARTED: "1" },
|
|
672
|
+
});
|
|
673
|
+
if (result.error)
|
|
674
|
+
throw new UpdateError(`updated Jouzu could not restart: ${result.error.message}`, "restart-failed", {
|
|
675
|
+
fatal: true,
|
|
676
|
+
});
|
|
677
|
+
if (result.signal)
|
|
678
|
+
return 128;
|
|
679
|
+
return result.status ?? 1;
|
|
680
|
+
}
|
|
681
|
+
export function formatUpdateStatus(status) {
|
|
682
|
+
const state = status.state;
|
|
683
|
+
return [
|
|
684
|
+
"Jouzu self-update status",
|
|
685
|
+
`Policy: ${status.policy}`,
|
|
686
|
+
`Install channel: ${status.installChannel}`,
|
|
687
|
+
`Automatic startup update: ${status.startupEligible ? "eligible" : "not eligible"}`,
|
|
688
|
+
`Installed version: ${state.installedVersion}`,
|
|
689
|
+
`Latest observed version: ${state.latestVersion ?? "not checked"}`,
|
|
690
|
+
`Last result: ${state.lastResult}`,
|
|
691
|
+
`Last checked: ${state.lastCheckedAt ?? "never"}`,
|
|
692
|
+
`Next check: ${state.nextCheckAt ?? "due"}`,
|
|
693
|
+
`Last updated: ${state.lastUpdatedAt ?? "never"}`,
|
|
694
|
+
`Previous version: ${state.previousVersion ?? "none"}`,
|
|
695
|
+
`Last error: ${state.lastErrorCode ?? "none"}`,
|
|
696
|
+
].join("\n");
|
|
697
|
+
}
|
|
698
|
+
//# sourceMappingURL=updater.js.map
|