@nissyai/desktop 0.2.36 → 0.2.38
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/bin/nissy.mjs +195 -1
- package/package.json +4 -4
- package/src/cli.mjs +169 -0
- package/src/resolve.d.mts +14 -0
- package/src/store.d.mts +38 -0
- package/src/store.mjs +977 -0
package/bin/nissy.mjs
CHANGED
|
@@ -33,10 +33,36 @@ import {
|
|
|
33
33
|
unsafeArchiveEntries,
|
|
34
34
|
UnsupportedPlatformError,
|
|
35
35
|
} from "../src/resolve.mjs";
|
|
36
|
+
import { confirmHealthyStart, prepareStore } from "../src/store.mjs";
|
|
37
|
+
import { launcherCommand, runLauncherCommand } from "../src/cli.mjs";
|
|
36
38
|
import { DISTRIBUTION } from "../src/distribution.mjs";
|
|
37
39
|
|
|
38
40
|
const require = createRequire(import.meta.url);
|
|
39
41
|
|
|
42
|
+
/** npm's copy of the payload version, or null when it cannot be read — an
|
|
43
|
+
* uninstalled or half-installed payload, which is precisely the state
|
|
44
|
+
* `--version` exists to describe rather than fall over on. */
|
|
45
|
+
function readPayloadVersion() {
|
|
46
|
+
try {
|
|
47
|
+
const spec = resolvePayloadSpec(process.platform, process.arch);
|
|
48
|
+
return JSON.parse(readFileSync(require.resolve(`${spec.pkg}/package.json`), "utf8")).version;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** This launcher package's own version. Published from `0.0.0-template`, which
|
|
55
|
+
* the pipeline substitutes, so a `-template` here means a working copy. */
|
|
56
|
+
const LAUNCHER_VERSION = (() => {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(
|
|
59
|
+
readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"),
|
|
60
|
+
).version;
|
|
61
|
+
} catch {
|
|
62
|
+
return "unknown";
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
|
|
40
66
|
function fail(message) {
|
|
41
67
|
console.error(`[${DISTRIBUTION.npmCommand}] ${message}`);
|
|
42
68
|
process.exit(1);
|
|
@@ -95,6 +121,38 @@ function isExtractedBundleInside(tempDir, bundlePath) {
|
|
|
95
121
|
}
|
|
96
122
|
}
|
|
97
123
|
|
|
124
|
+
// `--version` / `--rollback` answer and exit FIRST — before the payload is
|
|
125
|
+
// resolved, before the store is prepared, before anything is spawned.
|
|
126
|
+
//
|
|
127
|
+
// Before the payload, because these are support commands and the moment you
|
|
128
|
+
// need them is the moment the install is broken. A `--version` that refuses
|
|
129
|
+
// with "platform payload is not installed" is useless exactly when someone is
|
|
130
|
+
// trying to find out what state their machine is in, and a `--rollback` that
|
|
131
|
+
// needs a healthy install cannot rescue an unhealthy one.
|
|
132
|
+
//
|
|
133
|
+
// Before the store, because preparing it would let `--version` apply a pending
|
|
134
|
+
// update as a side effect of asking a question, and would let `--rollback` undo
|
|
135
|
+
// a swap the same command had just performed.
|
|
136
|
+
const launcherOwnCommand = launcherCommand(process.argv.slice(2));
|
|
137
|
+
if (launcherOwnCommand !== null) {
|
|
138
|
+
process.exit(
|
|
139
|
+
runLauncherCommand(launcherOwnCommand, {
|
|
140
|
+
platform: process.platform,
|
|
141
|
+
home: homedir(),
|
|
142
|
+
env: process.env,
|
|
143
|
+
cacheName: DISTRIBUTION.cacheName,
|
|
144
|
+
// Null when npm's copy cannot be read. The store is the authority for what
|
|
145
|
+
// RUNS; the payload version is a second opinion these commands report
|
|
146
|
+
// alongside it, not something they depend on.
|
|
147
|
+
payloadVersion: readPayloadVersion(),
|
|
148
|
+
launcherVersion: LAUNCHER_VERSION,
|
|
149
|
+
log: (message) => {
|
|
150
|
+
console.log(message);
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
98
156
|
let spec;
|
|
99
157
|
try {
|
|
100
158
|
spec = resolvePayloadSpec(process.platform, process.arch);
|
|
@@ -127,6 +185,17 @@ try {
|
|
|
127
185
|
// lifecycle scripts on cached installs still self-heal.
|
|
128
186
|
const payloadRoot = dirname(payloadPkgJson);
|
|
129
187
|
const archivePath = join(payloadRoot, "app.tar.gz");
|
|
188
|
+
// The version npm currently has on disk. On Windows this is what the store
|
|
189
|
+
// seeds from; it goes STALE the moment the app updates itself, which is why the
|
|
190
|
+
// store only ever seeds from it when it is genuinely newer than `current`.
|
|
191
|
+
const payloadVersionForStore = (() => {
|
|
192
|
+
try {
|
|
193
|
+
return JSON.parse(readFileSync(payloadPkgJson, "utf8")).version;
|
|
194
|
+
} catch {
|
|
195
|
+
return "";
|
|
196
|
+
}
|
|
197
|
+
})();
|
|
198
|
+
|
|
130
199
|
let exePath;
|
|
131
200
|
// The installed macOS `.app` bundle, when this is the macOS archive payload —
|
|
132
201
|
// the anchor we hand to `open` so LaunchServices dedups. null on Windows
|
|
@@ -209,6 +278,61 @@ if (!existsSync(exePath)) {
|
|
|
209
278
|
fail(`payload executable missing at: ${exePath}`);
|
|
210
279
|
}
|
|
211
280
|
|
|
281
|
+
// ── the versioned store (Windows npm channel) ─────────────────────────────
|
|
282
|
+
//
|
|
283
|
+
// Run from `<store>/current/...` rather than from the npm payload in place, so
|
|
284
|
+
// an update can be staged beside the running copy and applied by moving a
|
|
285
|
+
// pointer. `npm i -g` cannot do that: it renames the installed package aside
|
|
286
|
+
// before unpacking and rolls that rename back only on a best-effort basis, so a
|
|
287
|
+
// failed update over a locked, running exe can leave nothing installed at all.
|
|
288
|
+
//
|
|
289
|
+
// EVERY failure here falls back to `exePath` — the payload in place, exactly as
|
|
290
|
+
// this launcher behaved before the store existed. A bug in the store must cost
|
|
291
|
+
// a user their update, never their ability to start the app.
|
|
292
|
+
/** The npm payload's own executable, captured before the store may replace
|
|
293
|
+
* `exePath`, so a spawn failure can retreat to it instead of taking the whole
|
|
294
|
+
* launcher down. */
|
|
295
|
+
const payloadExePath = exePath;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* How long to watch a newly applied version before calling it started.
|
|
299
|
+
*
|
|
300
|
+
* Short on purpose. The launcher is a CLI: every millisecond here is a
|
|
301
|
+
* millisecond the user's terminal is held, and this only buys confidence
|
|
302
|
+
* against a process that dies straight away. The attempt counter in the store
|
|
303
|
+
* is what covers the slower failures.
|
|
304
|
+
*/
|
|
305
|
+
const HEALTH_WAIT_MS = 3000;
|
|
306
|
+
|
|
307
|
+
/** Is the process we started still there? Signal 0 checks without delivering. */
|
|
308
|
+
const stillRunning = (pid) => {
|
|
309
|
+
try {
|
|
310
|
+
process.kill(pid, 0);
|
|
311
|
+
return true;
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
let storeExePath = null;
|
|
317
|
+
const storeVerbose = process.env.NISSY_STORE_DEBUG === "1";
|
|
318
|
+
const storeLog = (message) => {
|
|
319
|
+
if (storeVerbose) console.error(`[${DISTRIBUTION.npmCommand}] ${message}`);
|
|
320
|
+
};
|
|
321
|
+
const staged = prepareStore({
|
|
322
|
+
platform: process.platform,
|
|
323
|
+
home: homedir(),
|
|
324
|
+
env: process.env,
|
|
325
|
+
cacheName: DISTRIBUTION.cacheName,
|
|
326
|
+
payloadVersion: payloadVersionForStore,
|
|
327
|
+
payloadRoot,
|
|
328
|
+
exeSegments: spec.exe,
|
|
329
|
+
log: storeLog,
|
|
330
|
+
});
|
|
331
|
+
if (staged !== null) {
|
|
332
|
+
exePath = staged.exePath;
|
|
333
|
+
storeExePath = staged.exePath;
|
|
334
|
+
}
|
|
335
|
+
|
|
212
336
|
// By default we launch the app detached and return immediately — `npx`/`nissy`
|
|
213
337
|
// frees the terminal instead of streaming the app's stdio. The app writes its
|
|
214
338
|
// own persistent log (electron-log → ~/Library/Logs/Nissy/main.log on macOS),
|
|
@@ -264,6 +388,22 @@ if (foreground) {
|
|
|
264
388
|
child.on("error", (err) => {
|
|
265
389
|
fail(`failed to spawn payload: ${err.message}`);
|
|
266
390
|
});
|
|
391
|
+
// Same handshake as the detached path, but this process is sticking around
|
|
392
|
+
// anyway, so it can be a timer instead of a blocking wait. Without it,
|
|
393
|
+
// debugging with --foreground would spend an attempt on every run and
|
|
394
|
+
// eventually roll a perfectly good version back.
|
|
395
|
+
if (staged !== null && staged.awaitingConfirmation === true) {
|
|
396
|
+
setTimeout(() => {
|
|
397
|
+
confirmHealthyStart({
|
|
398
|
+
platform: process.platform,
|
|
399
|
+
home: homedir(),
|
|
400
|
+
env: process.env,
|
|
401
|
+
cacheName: DISTRIBUTION.cacheName,
|
|
402
|
+
confirm: child.exitCode === null && child.signalCode === null,
|
|
403
|
+
log: storeLog,
|
|
404
|
+
});
|
|
405
|
+
}, HEALTH_WAIT_MS).unref();
|
|
406
|
+
}
|
|
267
407
|
child.on("exit", (code, signal) => {
|
|
268
408
|
if (signal !== null) {
|
|
269
409
|
// Re-raise the signal so the parent's exit reflects how the child died.
|
|
@@ -276,7 +416,34 @@ if (foreground) {
|
|
|
276
416
|
// `open` exits as soon as it hands off to LaunchServices; the direct-exec
|
|
277
417
|
// path needs the detach so this launcher can exit while the app keeps
|
|
278
418
|
// running. Either way, don't tie the app's lifetime to this process.
|
|
279
|
-
|
|
419
|
+
let child;
|
|
420
|
+
try {
|
|
421
|
+
child = spawn(command, args, { detached: true, stdio: "ignore", env: childEnv });
|
|
422
|
+
} catch (err) {
|
|
423
|
+
// `spawn` throws synchronously for a malformed or unreadable executable
|
|
424
|
+
// (EFTYPE, ENOEXEC), so the "error" handler below never sees it and the
|
|
425
|
+
// launcher dies with a Node stack trace. Observed with a corrupt staged
|
|
426
|
+
// version. If we were running from the store, retreat to the npm payload
|
|
427
|
+
// rather than leaving the user with nothing; `pending` stays set, so the
|
|
428
|
+
// next launch rolls the bad version back.
|
|
429
|
+
if (storeExePath !== null && exePath !== payloadExePath) {
|
|
430
|
+
storeLog(`spawn failed from the store (${err.code || err.message}); using the npm payload`);
|
|
431
|
+
try {
|
|
432
|
+
// The retreat can throw too — the payload itself may be the thing that
|
|
433
|
+
// is broken. Then there is genuinely nothing left to run, and saying so
|
|
434
|
+
// beats a raw Node stack trace.
|
|
435
|
+
child = spawn(payloadExePath, args, { detached: true, stdio: "ignore", env: childEnv });
|
|
436
|
+
} catch (fallbackErr) {
|
|
437
|
+
fail(
|
|
438
|
+
`neither the staged version nor the npm payload could be started ` +
|
|
439
|
+
`(${err.code || err.message} / ${fallbackErr.code || fallbackErr.message}). ` +
|
|
440
|
+
`Reinstall with: npm i -g ${DISTRIBUTION.npmLauncherPackage}`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
fail(`failed to spawn payload: ${err.message}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
280
447
|
child.on("error", (err) => {
|
|
281
448
|
fail(
|
|
282
449
|
viaLaunchServices
|
|
@@ -285,4 +452,31 @@ if (foreground) {
|
|
|
285
452
|
);
|
|
286
453
|
});
|
|
287
454
|
child.unref();
|
|
455
|
+
|
|
456
|
+
// The app is away. Two different jobs now, and conflating them is what made
|
|
457
|
+
// the first version of this wrong.
|
|
458
|
+
//
|
|
459
|
+
// A version we have already proven just needs its dead siblings dropped, and
|
|
460
|
+
// the launcher should get out of the way immediately.
|
|
461
|
+
//
|
|
462
|
+
// A version on trial needs evidence that it STARTED, and `spawn` returning a
|
|
463
|
+
// pid is not that — Windows creates the process before the loader has resolved
|
|
464
|
+
// a single import, so a build missing a DLL "spawns" perfectly and is gone a
|
|
465
|
+
// moment later. So wait, then ask whether it is still there. Cheap, and it
|
|
466
|
+
// catches the failure that actually happens. It is paid once per update, never
|
|
467
|
+
// on an ordinary launch.
|
|
468
|
+
//
|
|
469
|
+
// This is still the weak version of the handshake — see confirmHealthyStart.
|
|
470
|
+
const onTrial = staged !== null && staged.awaitingConfirmation === true;
|
|
471
|
+
if (onTrial) {
|
|
472
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, HEALTH_WAIT_MS);
|
|
473
|
+
}
|
|
474
|
+
confirmHealthyStart({
|
|
475
|
+
platform: process.platform,
|
|
476
|
+
home: homedir(),
|
|
477
|
+
env: process.env,
|
|
478
|
+
cacheName: DISTRIBUTION.cacheName,
|
|
479
|
+
confirm: onTrial && stillRunning(child.pid),
|
|
480
|
+
log: storeLog,
|
|
481
|
+
});
|
|
288
482
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nissyai/desktop",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.38",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Launcher for the Nissy desktop app. Resolves the host's app payload and spawns it. Run via `npx @nissyai/desktop`; a global install (`npm i -g @nissyai/desktop`) exposes the `nissy` command.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"src/"
|
|
21
21
|
],
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@nissyai/app-win32-x64": "0.2.
|
|
24
|
-
"@nissyai/app-darwin-arm64": "0.2.
|
|
25
|
-
"@nissyai/app-darwin-x64": "0.2.
|
|
23
|
+
"@nissyai/app-win32-x64": "0.2.38",
|
|
24
|
+
"@nissyai/app-darwin-arm64": "0.2.38",
|
|
25
|
+
"@nissyai/app-darwin-x64": "0.2.38"
|
|
26
26
|
}
|
|
27
27
|
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// The launcher's own commands: `nissy --version` and `nissy --rollback`.
|
|
2
|
+
//
|
|
3
|
+
// Both exist for support rather than for users. The AUTOMATIC rollback — a
|
|
4
|
+
// version that never confirms a start, after three attempts — is what protects
|
|
5
|
+
// people, and it is in `store.mjs`. These two are for the conversation that
|
|
6
|
+
// starts "it updated and now something is wrong", where the useful questions are
|
|
7
|
+
// "which version is actually running?" and "put me back", and today the only way
|
|
8
|
+
// to answer either is to read `store.log`.
|
|
9
|
+
//
|
|
10
|
+
// Kept out of `bin/nissy.mjs` because that file is a straight line — resolve the
|
|
11
|
+
// payload, prepare the store, spawn — and a command that answers and exits does
|
|
12
|
+
// not belong in the middle of it. Pure apart from the store it is handed, so the
|
|
13
|
+
// tests do not need a real install.
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
compareVersions,
|
|
17
|
+
isCompleteVersion,
|
|
18
|
+
listVersions,
|
|
19
|
+
pointCurrentAt,
|
|
20
|
+
readCurrentVersion,
|
|
21
|
+
readPreviousVersion,
|
|
22
|
+
storeLayout,
|
|
23
|
+
versionDir,
|
|
24
|
+
clearPending,
|
|
25
|
+
} from "./store.mjs";
|
|
26
|
+
|
|
27
|
+
export const VERSION_FLAG = "--version";
|
|
28
|
+
export const ROLLBACK_FLAG = "--rollback";
|
|
29
|
+
|
|
30
|
+
/** Which command, if any, the argv asks for. */
|
|
31
|
+
export function launcherCommand(argv) {
|
|
32
|
+
if (argv.includes(ROLLBACK_FLAG)) return "rollback";
|
|
33
|
+
if (argv.includes(VERSION_FLAG)) return "version";
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* What `--version` should print.
|
|
39
|
+
*
|
|
40
|
+
* Three versions, because they are genuinely three different things and the
|
|
41
|
+
* confusion between them is why this exists:
|
|
42
|
+
*
|
|
43
|
+
* - the STORE's current version, which is what actually runs
|
|
44
|
+
* - the npm PAYLOAD's version, which is what `npm i -g` last installed and
|
|
45
|
+
* which goes stale the moment the app updates itself
|
|
46
|
+
* - the LAUNCHER's own version
|
|
47
|
+
*
|
|
48
|
+
* A user reading "0.2.37" off `npm ls -g` while running 0.2.41 out of the store
|
|
49
|
+
* is the exact support call this is meant to shorten.
|
|
50
|
+
*/
|
|
51
|
+
export function describeVersions({ storeVersion, payloadVersion, launcherVersion, previousVersion }) {
|
|
52
|
+
const lines = [];
|
|
53
|
+
if (storeVersion !== null) {
|
|
54
|
+
lines.push(`running ${storeVersion}`);
|
|
55
|
+
} else if (payloadVersion !== null) {
|
|
56
|
+
lines.push(`running ${payloadVersion} (npm payload, no store yet)`);
|
|
57
|
+
} else {
|
|
58
|
+
// Neither readable. That IS the broken install this command exists to
|
|
59
|
+
// describe, so it reports what it found rather than refusing to answer.
|
|
60
|
+
lines.push("running unknown - no store, and npm's payload could not be read");
|
|
61
|
+
}
|
|
62
|
+
if (storeVersion !== null && previousVersion !== null && previousVersion !== storeVersion) {
|
|
63
|
+
lines.push(`previous ${previousVersion}`);
|
|
64
|
+
}
|
|
65
|
+
if (storeVersion !== null && payloadVersion !== null && payloadVersion !== storeVersion) {
|
|
66
|
+
// Not a warning. The store being AHEAD of npm is the normal steady state
|
|
67
|
+
// after a self-update, and saying so stops it looking like a fault.
|
|
68
|
+
lines.push(`npm has ${payloadVersion}`);
|
|
69
|
+
}
|
|
70
|
+
if (storeVersion !== null && payloadVersion === null) {
|
|
71
|
+
lines.push("npm has nothing readable - reinstall to repair the payload");
|
|
72
|
+
}
|
|
73
|
+
lines.push(`launcher ${launcherVersion}`);
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Decide what `--rollback` can do, without doing it.
|
|
79
|
+
*
|
|
80
|
+
* Separated from the action so every refusal is a value with a reason, and so
|
|
81
|
+
* the reasons are testable. A rollback that silently does nothing is worse than
|
|
82
|
+
* one that explains itself: the user reruns it, concludes the tool is broken,
|
|
83
|
+
* and reinstalls by hand — which is the thing this whole line of work exists to
|
|
84
|
+
* stop people having to do.
|
|
85
|
+
*/
|
|
86
|
+
export function planRollback({ currentVersion, previousVersion, versionExists }) {
|
|
87
|
+
if (currentVersion === null) {
|
|
88
|
+
return { ok: false, reason: "there is no versioned store yet, so there is nothing to roll back from" };
|
|
89
|
+
}
|
|
90
|
+
if (previousVersion === null) {
|
|
91
|
+
return { ok: false, reason: `${currentVersion} is the only version ever installed here` };
|
|
92
|
+
}
|
|
93
|
+
if (previousVersion === currentVersion) {
|
|
94
|
+
return { ok: false, reason: `${currentVersion} is already the previous version` };
|
|
95
|
+
}
|
|
96
|
+
if (compareVersions(previousVersion, currentVersion) > 0) {
|
|
97
|
+
// `previous` is NEWER. That happens the moment you roll back at all:
|
|
98
|
+
// pointing `current` at the old version demotes the new one to `previous`,
|
|
99
|
+
// so a second `--rollback` would put you straight back on the build you just
|
|
100
|
+
// escaped — while printing "rolled back", which is the opposite of true.
|
|
101
|
+
//
|
|
102
|
+
// A rollback only ever goes backwards. Going forwards is an update, and it
|
|
103
|
+
// has its own path.
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
reason:
|
|
107
|
+
`${currentVersion} is already older than ${previousVersion}. ` +
|
|
108
|
+
`Rollback only goes backwards; use the app's update to move forward.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (!versionExists(previousVersion)) {
|
|
112
|
+
// Pruned, or deleted by hand. Naming it matters: "reinstall to get it back"
|
|
113
|
+
// is actionable, "rollback failed" is not.
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
reason:
|
|
117
|
+
`${previousVersion} is no longer on disk, so it cannot be restored. ` +
|
|
118
|
+
`Reinstall a specific version instead.`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { ok: true, from: currentVersion, to: previousVersion };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Run the command. Returns the process exit code.
|
|
126
|
+
*
|
|
127
|
+
* `log` rather than `console.log` directly so the tests can read what a user
|
|
128
|
+
* would see, which is the whole output of these commands.
|
|
129
|
+
*/
|
|
130
|
+
export function runLauncherCommand(
|
|
131
|
+
command,
|
|
132
|
+
{ platform, home, env, cacheName, payloadVersion, launcherVersion, log },
|
|
133
|
+
) {
|
|
134
|
+
const layout = storeLayout({ platform, home, env, ...(cacheName ? { cacheName } : {}) });
|
|
135
|
+
|
|
136
|
+
if (command === "version") {
|
|
137
|
+
log(
|
|
138
|
+
describeVersions({
|
|
139
|
+
storeVersion: readCurrentVersion(layout),
|
|
140
|
+
previousVersion: readPreviousVersion(layout),
|
|
141
|
+
payloadVersion,
|
|
142
|
+
launcherVersion,
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const plan = planRollback({
|
|
149
|
+
currentVersion: readCurrentVersion(layout),
|
|
150
|
+
previousVersion: readPreviousVersion(layout),
|
|
151
|
+
versionExists: (version) =>
|
|
152
|
+
listVersions(layout).includes(version) && isCompleteVersion(versionDir(layout, version)),
|
|
153
|
+
});
|
|
154
|
+
if (!plan.ok) {
|
|
155
|
+
log(`cannot roll back: ${plan.reason}`);
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
pointCurrentAt(layout, plan.to);
|
|
160
|
+
// Whatever was pending was asking for the version we have just moved away
|
|
161
|
+
// from. Leaving it would let the next launch apply it again and undo this.
|
|
162
|
+
clearPending(layout);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
log(`rollback failed: ${(err && err.message) || String(err)}`);
|
|
165
|
+
return 1;
|
|
166
|
+
}
|
|
167
|
+
log(`rolled back ${plan.from} -> ${plan.to}. Start Nissy again to use it.`);
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Types for the parts of the resolver the DESKTOP consumes.
|
|
2
|
+
//
|
|
3
|
+
// The launcher is plain ESM with JSDoc and has no build step. This declaration
|
|
4
|
+
// exists so `apps/desktop`'s parity test can import the payload table and be
|
|
5
|
+
// type-checked rather than silently typed `any` — which would let the test pass
|
|
6
|
+
// while comparing `undefined` to `undefined`.
|
|
7
|
+
|
|
8
|
+
/** Host key (`${platform}-${arch}`) → payload package + the executable within
|
|
9
|
+
* it, as path segments. Mirrored by the app's `payloadExeSegments`; enforced by
|
|
10
|
+
* `payload-exe-parity.test.ts`. */
|
|
11
|
+
export declare const PAYLOADS: Record<
|
|
12
|
+
string,
|
|
13
|
+
{ pkg: string; exe: readonly string[] }
|
|
14
|
+
>;
|
package/src/store.d.mts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Types for the parts of the store the DESKTOP consumes.
|
|
2
|
+
//
|
|
3
|
+
// The launcher is plain ESM with JSDoc and has no build step; this declaration
|
|
4
|
+
// exists so `apps/desktop`'s parity test can import `compareVersions` and be
|
|
5
|
+
// type-checked rather than silently typed `any`. It deliberately declares only
|
|
6
|
+
// what crosses that boundary, not the whole module — nothing else outside the
|
|
7
|
+
// launcher should be importing from here.
|
|
8
|
+
|
|
9
|
+
/** -1 / 0 / 1. Must agree with the app's `compareVersions`; enforced by
|
|
10
|
+
* `apps/desktop/src/main/features/auto-update/version-parity.test.ts`. */
|
|
11
|
+
export declare function compareVersions(a: string, b: string): number;
|
|
12
|
+
|
|
13
|
+
/** Absolute paths of everything in the store. Mirrored by the app's
|
|
14
|
+
* `npmStoreLayout`; enforced by `npm-store.parity.test.ts`. */
|
|
15
|
+
export declare function storeLayout(input: {
|
|
16
|
+
platform: string;
|
|
17
|
+
home: string;
|
|
18
|
+
env: Record<string, string | undefined>;
|
|
19
|
+
cacheName?: string;
|
|
20
|
+
}): {
|
|
21
|
+
root: string;
|
|
22
|
+
versionsDir: string;
|
|
23
|
+
currentLink: string;
|
|
24
|
+
previousLink: string;
|
|
25
|
+
pendingFile: string;
|
|
26
|
+
healthFile: string;
|
|
27
|
+
logFile: string;
|
|
28
|
+
lockDir: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Semver-ish, and free of anything that means something to a path. */
|
|
32
|
+
export declare function isSafeVersion(version: string): boolean;
|
|
33
|
+
|
|
34
|
+
/** Written last into a version directory; the only thing that makes it eligible. */
|
|
35
|
+
export declare const SEED_COMPLETE_MARKER: string;
|
|
36
|
+
|
|
37
|
+
/** Launches an applied version gets to prove it starts. */
|
|
38
|
+
export declare const DEFAULT_TRIES: number;
|