@nissyai/desktop 0.2.56 → 0.2.59
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 +144 -142
- package/package.json +4 -4
- package/src/cli.d.mts +13 -0
- package/src/cli.mjs +63 -5
- package/src/demo-preflight.d.mts +35 -0
- package/src/demo-preflight.mjs +129 -0
- package/src/mac-store.d.mts +8 -0
- package/src/mac-store.mjs +664 -0
- package/src/resolve.d.mts +4 -0
- package/src/resolve.mjs +0 -17
package/bin/nissy.mjs
CHANGED
|
@@ -6,36 +6,38 @@
|
|
|
6
6
|
// installed it, and spawns the self-contained app executable inside it,
|
|
7
7
|
// inheriting stdio and propagating the exit code / signal.
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
10
|
import {
|
|
11
11
|
existsSync,
|
|
12
|
-
lstatSync,
|
|
13
12
|
mkdirSync,
|
|
14
13
|
readdirSync,
|
|
15
14
|
readFileSync,
|
|
16
|
-
realpathSync,
|
|
17
|
-
renameSync,
|
|
18
15
|
rmSync,
|
|
19
16
|
writeFileSync,
|
|
20
17
|
} from "node:fs";
|
|
21
18
|
import { createRequire } from "node:module";
|
|
22
19
|
import { homedir } from "node:os";
|
|
23
|
-
import { dirname,
|
|
20
|
+
import { dirname, join } from "node:path";
|
|
24
21
|
import { fileURLToPath } from "node:url";
|
|
25
22
|
|
|
26
23
|
import {
|
|
27
24
|
cacheRootSegments,
|
|
28
25
|
LAUNCHER_TOOLCHAIN_FILE,
|
|
29
|
-
needsMacReinstall,
|
|
30
26
|
npxChannelEnv,
|
|
31
27
|
resolveLaunchCommand,
|
|
32
28
|
resolvePayloadSpec,
|
|
33
|
-
unsafeArchiveEntries,
|
|
34
29
|
UnsupportedPlatformError,
|
|
35
30
|
} from "../src/resolve.mjs";
|
|
36
31
|
import { confirmHealthyStart, prepareStore } from "../src/store.mjs";
|
|
37
|
-
import {
|
|
32
|
+
import { MAC_STORE_OUTCOME, prepareMacStore, pruneMacStore } from "../src/mac-store.mjs";
|
|
33
|
+
import {
|
|
34
|
+
launcherCommand,
|
|
35
|
+
launcherOptions,
|
|
36
|
+
runLauncherCommand,
|
|
37
|
+
stripLauncherFlags,
|
|
38
|
+
} from "../src/cli.mjs";
|
|
38
39
|
import { DISTRIBUTION } from "../src/distribution.mjs";
|
|
40
|
+
import { asarBeside, demoPreflight, demoTemplatePath, userDataDir } from "../src/demo-preflight.mjs";
|
|
39
41
|
|
|
40
42
|
const require = createRequire(import.meta.url);
|
|
41
43
|
|
|
@@ -68,58 +70,31 @@ function fail(message) {
|
|
|
68
70
|
process.exit(1);
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.split(/\r?\n/u)
|
|
78
|
-
.map((entry) => entry.trim())
|
|
79
|
-
.filter((entry) => entry.length > 0);
|
|
80
|
-
const unsafe = unsafeArchiveEntries(entries);
|
|
81
|
-
if (unsafe.length > 0) {
|
|
82
|
-
throw new Error(
|
|
83
|
-
`payload archive contains unsafe entries: ${unsafe.slice(0, 5).join(", ")}`,
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function isPathInside(root, candidate) {
|
|
89
|
-
const escaped = relative(root, candidate);
|
|
90
|
-
return escaped !== "" && !escaped.startsWith("..") && !isAbsolute(escaped);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function isExtractedBundleInside(tempDir, bundlePath) {
|
|
73
|
+
/** Where the macOS bundle lives and how to read it, for a supported Mac host;
|
|
74
|
+
* null anywhere else. Computed without the payload, because the installed
|
|
75
|
+
* bundle is what runs and it must be describable when npm's copy is gone. */
|
|
76
|
+
function macContext() {
|
|
77
|
+
if (process.platform !== "darwin") return null;
|
|
78
|
+
let macSpec;
|
|
94
79
|
try {
|
|
95
|
-
|
|
96
|
-
const bundleStat = lstatSync(bundlePath);
|
|
97
|
-
if (bundleStat.isSymbolicLink() || !bundleStat.isDirectory()) return false;
|
|
98
|
-
const bundleRoot = realpathSync(bundlePath);
|
|
99
|
-
if (!isPathInside(tempRoot, bundleRoot)) return false;
|
|
100
|
-
|
|
101
|
-
// The top-level bundle can be inside the temp directory while an inner
|
|
102
|
-
// symlink still points outside it. Walk without following directories and
|
|
103
|
-
// reject every such link before the bundle is moved into Applications.
|
|
104
|
-
const pending = [bundlePath];
|
|
105
|
-
while (pending.length > 0) {
|
|
106
|
-
const current = pending.pop();
|
|
107
|
-
if (current === undefined) continue;
|
|
108
|
-
const stat = lstatSync(current);
|
|
109
|
-
if (stat.isSymbolicLink()) {
|
|
110
|
-
if (!isPathInside(tempRoot, realpathSync(current))) return false;
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
if (!stat.isDirectory()) continue;
|
|
114
|
-
for (const entry of readdirSync(current)) {
|
|
115
|
-
pending.push(join(current, entry));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return true;
|
|
80
|
+
macSpec = resolvePayloadSpec(process.platform, process.arch);
|
|
119
81
|
} catch {
|
|
120
|
-
return
|
|
82
|
+
return null;
|
|
121
83
|
}
|
|
84
|
+
const bundleName = macSpec.exe[1]; // e.g. "Nissy.app" (exe[0] is "app")
|
|
85
|
+
const appsDir = join(homedir(), "Applications");
|
|
86
|
+
return {
|
|
87
|
+
bundleName,
|
|
88
|
+
exeSegments: macSpec.exe,
|
|
89
|
+
installedApp: join(appsDir, bundleName),
|
|
90
|
+
exePath: join(appsDir, ...macSpec.exe.slice(1)),
|
|
91
|
+
markerPath: join(
|
|
92
|
+
...cacheRootSegments({ platform: process.platform, home: homedir(), env: process.env }),
|
|
93
|
+
"applications-darwin.version",
|
|
94
|
+
),
|
|
95
|
+
};
|
|
122
96
|
}
|
|
97
|
+
const mac = macContext();
|
|
123
98
|
|
|
124
99
|
// `--version` / `--rollback` answer and exit FIRST — before the payload is
|
|
125
100
|
// resolved, before the store is prepared, before anything is spawned.
|
|
@@ -146,6 +121,7 @@ if (launcherOwnCommand !== null) {
|
|
|
146
121
|
// alongside it, not something they depend on.
|
|
147
122
|
payloadVersion: readPayloadVersion(),
|
|
148
123
|
launcherVersion: LAUNCHER_VERSION,
|
|
124
|
+
mac,
|
|
149
125
|
log: (message) => {
|
|
150
126
|
console.log(message);
|
|
151
127
|
},
|
|
@@ -153,6 +129,8 @@ if (launcherOwnCommand !== null) {
|
|
|
153
129
|
);
|
|
154
130
|
}
|
|
155
131
|
|
|
132
|
+
const { afterPid, applyOnly } = launcherOptions(process.argv.slice(2));
|
|
133
|
+
|
|
156
134
|
let spec;
|
|
157
135
|
try {
|
|
158
136
|
spec = resolvePayloadSpec(process.platform, process.arch);
|
|
@@ -163,32 +141,32 @@ try {
|
|
|
163
141
|
throw err;
|
|
164
142
|
}
|
|
165
143
|
|
|
166
|
-
|
|
144
|
+
const missingPayloadMessage =
|
|
145
|
+
`platform payload "${spec.pkg}" is not installed for ${spec.key}. ` +
|
|
146
|
+
`Reinstall so the matching optional dependency is fetched.`;
|
|
147
|
+
let payloadPkgJson = null;
|
|
167
148
|
try {
|
|
168
149
|
payloadPkgJson = require.resolve(`${spec.pkg}/package.json`);
|
|
169
150
|
} catch {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
151
|
+
// On macOS an installed bundle still runs without npm's copy — npm drops an
|
|
152
|
+
// optional dependency whose download failed and exits 0, which is exactly how
|
|
153
|
+
// a Mac ended up with a launcher and no payload. Everywhere else, and on a Mac
|
|
154
|
+
// with nothing installed, there is nothing to run.
|
|
155
|
+
if (mac === null || !existsSync(mac.exePath)) fail(missingPayloadMessage);
|
|
174
156
|
}
|
|
175
157
|
|
|
176
158
|
// Resolve the executable to launch. Two payload layouts:
|
|
177
|
-
// • app/ — Forge output copied verbatim (Windows).
|
|
178
|
-
// • app.tar.gz — symlink-preserving archive (macOS
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
// Refreshed in place when the payload version changes. Done
|
|
184
|
-
// here (not a postinstall hook) so package managers that skip
|
|
185
|
-
// lifecycle scripts on cached installs still self-heal.
|
|
186
|
-
const payloadRoot = dirname(payloadPkgJson);
|
|
187
|
-
const archivePath = join(payloadRoot, "app.tar.gz");
|
|
159
|
+
// • app/ — Forge output copied verbatim (Windows). Seeds the store.
|
|
160
|
+
// • app.tar.gz — symlink-preserving archive (macOS). Installed into
|
|
161
|
+
// ~/Applications by the macOS store below, and only when
|
|
162
|
+
// npm's copy is genuinely newer than what is installed.
|
|
163
|
+
const payloadRoot = payloadPkgJson === null ? null : dirname(payloadPkgJson);
|
|
164
|
+
const archivePath = payloadRoot === null ? null : join(payloadRoot, "app.tar.gz");
|
|
188
165
|
// The version npm currently has on disk. On Windows this is what the store
|
|
189
166
|
// seeds from; it goes STALE the moment the app updates itself, which is why the
|
|
190
167
|
// store only ever seeds from it when it is genuinely newer than `current`.
|
|
191
168
|
const payloadVersionForStore = (() => {
|
|
169
|
+
if (payloadPkgJson === null) return "";
|
|
192
170
|
try {
|
|
193
171
|
return JSON.parse(readFileSync(payloadPkgJson, "utf8")).version;
|
|
194
172
|
} catch {
|
|
@@ -201,79 +179,70 @@ let exePath;
|
|
|
201
179
|
// the anchor we hand to `open` so LaunchServices dedups. null on Windows
|
|
202
180
|
// (run-in-place), where we exec the binary directly.
|
|
203
181
|
let macBundlePath = null;
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
259
|
-
// The previous install is still there, so we CAN carry on — and carrying
|
|
260
|
-
// on is right: a user with a working app should not be stopped from
|
|
261
|
-
// opening it because the new version would not unpack.
|
|
262
|
-
//
|
|
263
|
-
// But say so. Silence here is what made the validation bug above cost
|
|
264
|
-
// real users their updates rather than a bug report: the app relaunched
|
|
265
|
-
// the old version, found the same update waiting, and looped — with
|
|
266
|
-
// nothing written anywhere to say an install had been attempted at all.
|
|
267
|
-
console.error(
|
|
268
|
-
`[nissy] could not install ${version}: ${err.message}\n` +
|
|
269
|
-
`[nissy] continuing on the installed version at ${installedApp}`,
|
|
270
|
-
);
|
|
271
|
-
}
|
|
182
|
+
if (mac !== null) {
|
|
183
|
+
// macOS: `current` is the bundle in ~/Applications — a standard,
|
|
184
|
+
// Spotlight-indexed location, so Nissy shows in Spotlight / Launchpad / Finder
|
|
185
|
+
// and carries its Dock icon. The npm payload ships it as `app.tar.gz`
|
|
186
|
+
// (symlink-preserving; npm pack would strip the framework symlinks), and the
|
|
187
|
+
// store decides whether to install it, apply an update the app staged, or roll
|
|
188
|
+
// back — and never does any of that while Nissy is running. See
|
|
189
|
+
// src/mac-store.mjs. Done here, not in a postinstall hook, so package managers
|
|
190
|
+
// that skip lifecycle scripts on cached installs still self-heal.
|
|
191
|
+
macBundlePath = mac.installedApp;
|
|
192
|
+
exePath = mac.exePath;
|
|
193
|
+
const prepared = prepareMacStore({
|
|
194
|
+
home: homedir(),
|
|
195
|
+
env: process.env,
|
|
196
|
+
cacheName: DISTRIBUTION.cacheName,
|
|
197
|
+
installedApp: mac.installedApp,
|
|
198
|
+
exePath: mac.exePath,
|
|
199
|
+
bundleName: mac.bundleName,
|
|
200
|
+
exeSegments: mac.exeSegments,
|
|
201
|
+
markerPath: mac.markerPath,
|
|
202
|
+
payloadVersion: payloadVersionForStore,
|
|
203
|
+
archivePath,
|
|
204
|
+
afterPid,
|
|
205
|
+
willLaunch: !applyOnly,
|
|
206
|
+
log: (message) => {
|
|
207
|
+
if (process.env.NISSY_STORE_DEBUG === "1") console.error(`[${DISTRIBUTION.npmCommand}] ${message}`);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
if (prepared.error !== undefined) {
|
|
211
|
+
// Said out loud, not only logged: a store that silently never updates looks
|
|
212
|
+
// exactly like one that is working. The installed bundle still opens.
|
|
213
|
+
console.error(`[${DISTRIBUTION.npmCommand}] could not update: ${prepared.error.message}`);
|
|
214
|
+
}
|
|
215
|
+
if (prepared.outcome === MAC_STORE_OUTCOME.stagedForLater) {
|
|
216
|
+
console.error(
|
|
217
|
+
`[${DISTRIBUTION.npmCommand}] Nissy is running, so ${prepared.version} is ready and will be ` +
|
|
218
|
+
`installed when you quit it.`,
|
|
219
|
+
);
|
|
220
|
+
} else if (prepared.outcome === MAC_STORE_OUTCOME.deferred && prepared.version !== null && !applyOnly) {
|
|
221
|
+
console.error(
|
|
222
|
+
`[${DISTRIBUTION.npmCommand}] Nissy is running, so ${prepared.version} was not installed yet. ` +
|
|
223
|
+
`Quit Nissy and run ${DISTRIBUTION.npmCommand} again to finish.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (!existsSync(exePath)) {
|
|
227
|
+
fail(
|
|
228
|
+
payloadPkgJson === null
|
|
229
|
+
? missingPayloadMessage
|
|
230
|
+
: `failed to install ${mac.installedApp}; see ${join(
|
|
231
|
+
...cacheRootSegments({ platform: process.platform, home: homedir(), env: process.env }),
|
|
232
|
+
"store",
|
|
233
|
+
"store.log",
|
|
234
|
+
)}`,
|
|
235
|
+
);
|
|
272
236
|
}
|
|
237
|
+
pruneMacStore({ home: homedir(), env: process.env, cacheName: DISTRIBUTION.cacheName });
|
|
238
|
+
if (applyOnly) process.exit(0);
|
|
273
239
|
|
|
274
240
|
// Best-effort: reclaim space from the OLD version-keyed cache layout earlier
|
|
275
241
|
// launcher versions used (~140 MB each). Never block launch.
|
|
276
242
|
try {
|
|
243
|
+
const cacheRoot = join(
|
|
244
|
+
...cacheRootSegments({ platform: process.platform, home: homedir(), env: process.env }),
|
|
245
|
+
);
|
|
277
246
|
for (const entry of readdirSync(cacheRoot)) {
|
|
278
247
|
if (entry.startsWith(`${DISTRIBUTION.cacheName}-app-darwin-`)) {
|
|
279
248
|
rmSync(join(cacheRoot, entry), { recursive: true, force: true });
|
|
@@ -282,6 +251,8 @@ if (existsSync(join(payloadRoot, "app"))) {
|
|
|
282
251
|
} catch {
|
|
283
252
|
// ignore — cleanup is non-essential
|
|
284
253
|
}
|
|
254
|
+
} else if (existsSync(join(payloadRoot, "app"))) {
|
|
255
|
+
exePath = join(payloadRoot, ...spec.exe);
|
|
285
256
|
} else {
|
|
286
257
|
fail(`payload "${spec.pkg}" contains neither app/ nor app.tar.gz`);
|
|
287
258
|
}
|
|
@@ -350,8 +321,9 @@ if (staged !== null) {
|
|
|
350
321
|
// own persistent log (electron-log → ~/Library/Logs/Nissy/main.log on macOS),
|
|
351
322
|
// so dropping stdio here loses nothing. Pass --foreground to stay attached and
|
|
352
323
|
// mirror the app's stdio + exit code, which is what you want when debugging.
|
|
353
|
-
const
|
|
354
|
-
const
|
|
324
|
+
const userArgs = stripLauncherFlags(process.argv.slice(2));
|
|
325
|
+
const forwardedArgs = userArgs.filter((a) => a !== "--foreground");
|
|
326
|
+
const foreground = forwardedArgs.length !== userArgs.length;
|
|
355
327
|
|
|
356
328
|
// On macOS, launch through `/usr/bin/open <bundle>` so LaunchServices enforces
|
|
357
329
|
// single-instance: a `nissy`/`npx` run while Nissy is already up just activates
|
|
@@ -395,6 +367,36 @@ try {
|
|
|
395
367
|
// Ignore: self-update falls back to the env/persisted value or the manual command.
|
|
396
368
|
}
|
|
397
369
|
|
|
370
|
+
// `--demo` against a missing or stale template: say so here, in the terminal
|
|
371
|
+
// the user typed the command into. The app would refuse it too, but from a
|
|
372
|
+
// detached process whose output this launcher has already let go of, so the
|
|
373
|
+
// only trace would be a dialog. Advisory by design: every path the check cannot
|
|
374
|
+
// decide lets the launch proceed and leaves the verdict to the app, and a fault
|
|
375
|
+
// in the check itself must never be what stops Nissy starting.
|
|
376
|
+
try {
|
|
377
|
+
const userData = userDataDir({
|
|
378
|
+
platform: process.platform,
|
|
379
|
+
home: homedir(),
|
|
380
|
+
env: process.env,
|
|
381
|
+
userDataName: DISTRIBUTION.userDataName,
|
|
382
|
+
});
|
|
383
|
+
if (userData !== null) {
|
|
384
|
+
const launchedVersion = staged !== null ? staged.version : payloadVersionForStore;
|
|
385
|
+
const verdict = demoPreflight({
|
|
386
|
+
args: forwardedArgs,
|
|
387
|
+
templatePath: demoTemplatePath({ userData, flavor: DISTRIBUTION.flavor, env: process.env }),
|
|
388
|
+
asarPath: asarBeside(exePath, existsSync),
|
|
389
|
+
appLabel: launchedVersion === "" ? DISTRIBUTION.appName : `${DISTRIBUTION.appName} ${launchedVersion}`,
|
|
390
|
+
command: DISTRIBUTION.npmCommand,
|
|
391
|
+
exists: existsSync,
|
|
392
|
+
readText: (path, encoding) => readFileSync(path, encoding),
|
|
393
|
+
});
|
|
394
|
+
if (!verdict.ok) fail(verdict.message);
|
|
395
|
+
}
|
|
396
|
+
} catch (err) {
|
|
397
|
+
storeLog(`demo pre-flight skipped: ${err.message}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
398
400
|
if (foreground) {
|
|
399
401
|
const child = spawn(command, args, { stdio: "inherit", env: childEnv });
|
|
400
402
|
child.on("error", (err) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nissyai/desktop",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.59",
|
|
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.59",
|
|
24
|
+
"@nissyai/app-darwin-arm64": "0.2.59",
|
|
25
|
+
"@nissyai/app-darwin-x64": "0.2.59"
|
|
26
26
|
}
|
|
27
27
|
}
|
package/src/cli.d.mts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Types for the launcher-only flags the DESKTOP hands the launcher.
|
|
2
|
+
//
|
|
3
|
+
// The app builds these arguments and the launcher parses them; they share no
|
|
4
|
+
// code, so `launcher-handoff.test.ts` imports both sides through this
|
|
5
|
+
// declaration and fails if the names drift.
|
|
6
|
+
|
|
7
|
+
export declare const AFTER_PID_FLAG: "--nissy-after-pid";
|
|
8
|
+
export declare const APPLY_ONLY_FLAG: "--nissy-apply-only";
|
|
9
|
+
export declare function launcherOptions(argv: readonly string[]): {
|
|
10
|
+
afterPid: number | null;
|
|
11
|
+
applyOnly: boolean;
|
|
12
|
+
};
|
|
13
|
+
export declare function stripLauncherFlags(argv: readonly string[]): string[];
|
package/src/cli.mjs
CHANGED
|
@@ -23,10 +23,47 @@ import {
|
|
|
23
23
|
versionDir,
|
|
24
24
|
clearPending,
|
|
25
25
|
} from "./store.mjs";
|
|
26
|
+
import {
|
|
27
|
+
bundleProcesses,
|
|
28
|
+
installedMacVersion,
|
|
29
|
+
isStagedMacVersion,
|
|
30
|
+
swapInBundle,
|
|
31
|
+
} from "./mac-store.mjs";
|
|
26
32
|
|
|
27
33
|
export const VERSION_FLAG = "--version";
|
|
28
34
|
export const ROLLBACK_FLAG = "--rollback";
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Flags that are for the launcher alone and are never forwarded to the app.
|
|
38
|
+
*
|
|
39
|
+
* Both are set by the APP, not by people. `--nissy-after-pid=<pid>` is how the
|
|
40
|
+
* app hands over when it restarts or quits with an update staged: the launcher
|
|
41
|
+
* waits for that process (and everything running from its bundle) to be gone
|
|
42
|
+
* before it swaps anything, which is the Squirrel.Mac / Sparkle contract.
|
|
43
|
+
* `--nissy-apply-only` asks for the swap and nothing else — the quit path, where
|
|
44
|
+
* the user asked to quit, not to see the app again.
|
|
45
|
+
*/
|
|
46
|
+
export const AFTER_PID_FLAG = "--nissy-after-pid";
|
|
47
|
+
export const APPLY_ONLY_FLAG = "--nissy-apply-only";
|
|
48
|
+
|
|
49
|
+
/** The launcher-only options in an argv. A malformed pid is ignored rather than
|
|
50
|
+
* trusted: waiting on the wrong process is worse than not waiting. */
|
|
51
|
+
export function launcherOptions(argv) {
|
|
52
|
+
let afterPid = null;
|
|
53
|
+
for (const arg of argv) {
|
|
54
|
+
if (arg.startsWith(`${AFTER_PID_FLAG}=`)) {
|
|
55
|
+
const pid = Number(arg.slice(AFTER_PID_FLAG.length + 1));
|
|
56
|
+
if (Number.isInteger(pid) && pid > 0) afterPid = pid;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { afterPid, applyOnly: argv.includes(APPLY_ONLY_FLAG) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The argv with the launcher-only flags removed, for forwarding to the app. */
|
|
63
|
+
export function stripLauncherFlags(argv) {
|
|
64
|
+
return argv.filter((arg) => arg !== APPLY_ONLY_FLAG && !arg.startsWith(`${AFTER_PID_FLAG}=`));
|
|
65
|
+
}
|
|
66
|
+
|
|
30
67
|
/** Which command, if any, the argv asks for. */
|
|
31
68
|
export function launcherCommand(argv) {
|
|
32
69
|
if (argv.includes(ROLLBACK_FLAG)) return "rollback";
|
|
@@ -129,14 +166,17 @@ export function planRollback({ currentVersion, previousVersion, versionExists })
|
|
|
129
166
|
*/
|
|
130
167
|
export function runLauncherCommand(
|
|
131
168
|
command,
|
|
132
|
-
{ platform, home, env, cacheName, payloadVersion, launcherVersion, log },
|
|
169
|
+
{ platform, home, env, cacheName, payloadVersion, launcherVersion, log, mac = null },
|
|
133
170
|
) {
|
|
134
171
|
const layout = storeLayout({ platform, home, env, ...(cacheName ? { cacheName } : {}) });
|
|
172
|
+
// On macOS `current` is the installed bundle, not a junction.
|
|
173
|
+
const readCurrent = () =>
|
|
174
|
+
mac === null ? readCurrentVersion(layout) : installedMacVersion(mac);
|
|
135
175
|
|
|
136
176
|
if (command === "version") {
|
|
137
177
|
log(
|
|
138
178
|
describeVersions({
|
|
139
|
-
storeVersion:
|
|
179
|
+
storeVersion: readCurrent(),
|
|
140
180
|
previousVersion: readPreviousVersion(layout),
|
|
141
181
|
payloadVersion,
|
|
142
182
|
launcherVersion,
|
|
@@ -146,17 +186,35 @@ export function runLauncherCommand(
|
|
|
146
186
|
}
|
|
147
187
|
|
|
148
188
|
const plan = planRollback({
|
|
149
|
-
currentVersion:
|
|
189
|
+
currentVersion: readCurrent(),
|
|
150
190
|
previousVersion: readPreviousVersion(layout),
|
|
151
191
|
versionExists: (version) =>
|
|
152
|
-
|
|
192
|
+
mac === null
|
|
193
|
+
? listVersions(layout).includes(version) && isCompleteVersion(versionDir(layout, version))
|
|
194
|
+
: isStagedMacVersion(layout, version, mac.exeSegments),
|
|
153
195
|
});
|
|
154
196
|
if (!plan.ok) {
|
|
155
197
|
log(`cannot roll back: ${plan.reason}`);
|
|
156
198
|
return 1;
|
|
157
199
|
}
|
|
158
200
|
try {
|
|
159
|
-
|
|
201
|
+
if (mac === null) {
|
|
202
|
+
pointCurrentAt(layout, plan.to);
|
|
203
|
+
} else {
|
|
204
|
+
// The same rule as every other macOS swap: never under a running app.
|
|
205
|
+
const running = bundleProcesses(mac.installedApp, mac.list);
|
|
206
|
+
if (running === null || running.length > 0) {
|
|
207
|
+
log("cannot roll back while Nissy is running. Quit it, then run this again.");
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
swapInBundle({
|
|
211
|
+
layout,
|
|
212
|
+
version: plan.to,
|
|
213
|
+
bundleName: mac.bundleName,
|
|
214
|
+
installedApp: mac.installedApp,
|
|
215
|
+
markerPath: mac.markerPath,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
160
218
|
// Whatever was pending was asking for the version we have just moved away
|
|
161
219
|
// from. Leaving it would let the next launch apply it again and undo this.
|
|
162
220
|
clearPending(layout);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Types for the launcher's Demo Mode pre-flight.
|
|
2
|
+
//
|
|
3
|
+
// The launcher mirrors two decisions the desktop owns: where the demo template
|
|
4
|
+
// lives, and how the installed app's migrations are named. They share no code,
|
|
5
|
+
// so `demo-preflight-parity.test.ts` in the desktop imports both sides through
|
|
6
|
+
// this declaration and fails if they drift.
|
|
7
|
+
|
|
8
|
+
export declare const DEMO_FLAG: "--demo";
|
|
9
|
+
|
|
10
|
+
export declare function userDataDir(input: {
|
|
11
|
+
platform: NodeJS.Platform;
|
|
12
|
+
home: string;
|
|
13
|
+
env: NodeJS.ProcessEnv;
|
|
14
|
+
userDataName: string;
|
|
15
|
+
}): string | null;
|
|
16
|
+
|
|
17
|
+
export declare function demoTemplatePath(input: {
|
|
18
|
+
userData: string;
|
|
19
|
+
flavor: string;
|
|
20
|
+
env: NodeJS.ProcessEnv;
|
|
21
|
+
}): string;
|
|
22
|
+
|
|
23
|
+
export declare function asarBeside(exePath: string, exists: (path: string) => boolean): string | null;
|
|
24
|
+
|
|
25
|
+
export declare function embeddedMigrations(asarText: string): string[];
|
|
26
|
+
|
|
27
|
+
export declare function demoPreflight(input: {
|
|
28
|
+
args: readonly string[];
|
|
29
|
+
templatePath: string;
|
|
30
|
+
asarPath: string | null;
|
|
31
|
+
appLabel: string;
|
|
32
|
+
command: string;
|
|
33
|
+
exists: (path: string) => boolean;
|
|
34
|
+
readText: (path: string, encoding: BufferEncoding) => string;
|
|
35
|
+
}): { ok: true } | { ok: false; message: string };
|