scenri 0.8.2 → 0.9.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/CHANGELOG.md +107 -0
- package/README.md +2 -0
- package/dist/chunk-4NQKRVO2.js +55 -0
- package/dist/chunk-HLMR33DT.js +246 -0
- package/dist/chunk-OJAG3FRX.js +1940 -0
- package/dist/chunk-PAIAXRAC.js +20 -0
- package/dist/chunk-QDODTMSN.js +62 -0
- package/dist/cli-BVPVS4V5.js +376 -0
- package/dist/index.js +23 -2
- package/dist/install-V6DAA7TT.js +4 -0
- package/dist/{launcher-DIZ4MQRF.js → launcher-IVFIYHPN.js} +6 -2
- package/dist/offer-IVTNHIDJ.js +47 -0
- package/dist/paths-36Y73GGY.js +3 -0
- package/dist/refresh-7HXPUHLQ.js +91 -0
- package/dist/serve.js +434 -2186
- package/dist/src-CSR34EBZ.js +3 -0
- package/launcher/Scenri.icns +0 -0
- package/launcher/launch.mjs +144 -0
- package/launcher/scenri.ico +0 -0
- package/launcher/starting.html +78 -0
- package/package.json +2 -1
- package/studio-dist/assets/index-DSSGRUMi.js +104 -0
- package/studio-dist/assets/{index-DwfWp4jH.css → index-DfAHZRYM.css} +1 -1
- package/studio-dist/index.html +2 -2
- package/studio-dist/assets/index-9Y9YnJq6.js +0 -103
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { realpathSync } from 'fs';
|
|
2
|
+
import { sep, join } from 'path';
|
|
3
|
+
|
|
4
|
+
// src/installKind.ts
|
|
5
|
+
function detectInstallKind(entryPath, home) {
|
|
6
|
+
if (!entryPath.includes(`${sep}dist${sep}`) && !entryPath.endsWith(`${sep}dist`)) return "dev";
|
|
7
|
+
let homeReal = home;
|
|
8
|
+
try {
|
|
9
|
+
homeReal = realpathSync(home);
|
|
10
|
+
} catch {
|
|
11
|
+
}
|
|
12
|
+
if (entryPath.startsWith(join(homeReal, "app", "versions") + sep)) return "managed";
|
|
13
|
+
if (entryPath.includes(`${sep}_npx${sep}`)) return "npx";
|
|
14
|
+
if (entryPath.includes(`${sep}node_modules${sep}`)) return "global";
|
|
15
|
+
return "unknown";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { detectInstallKind };
|
|
19
|
+
//# sourceMappingURL=chunk-PAIAXRAC.js.map
|
|
20
|
+
//# sourceMappingURL=chunk-PAIAXRAC.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { isValidVersionDir, stagingDir, versionsDir, pruneStaged } from './chunk-4MAFHYAD.js';
|
|
2
|
+
import { execFile } from 'child_process';
|
|
3
|
+
import { existsSync, rmSync, mkdirSync, cpSync, renameSync, readFileSync } from 'fs';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
|
|
6
|
+
async function adoptRunningInstall(deps) {
|
|
7
|
+
if (deps.installKind === "dev") return { adopted: false, reason: "dev" };
|
|
8
|
+
if (deps.installKind === "managed") return { adopted: false, reason: "managed" };
|
|
9
|
+
if (isValidVersionDir(deps.home, deps.pkg, deps.version)) return { adopted: false, reason: "present" };
|
|
10
|
+
const pkgRoot = dirname(dirname(deps.ownEntry));
|
|
11
|
+
if (!existsSync(deps.ownEntry) || manifestVersion(pkgRoot) !== deps.version) {
|
|
12
|
+
return { adopted: false, reason: "no-source" };
|
|
13
|
+
}
|
|
14
|
+
const nested = existsSync(join(pkgRoot, "node_modules", "fastify"));
|
|
15
|
+
const source = nested ? pkgRoot : dirname(pkgRoot);
|
|
16
|
+
const work = join(stagingDir(deps.home), `adopt-${deps.version}`);
|
|
17
|
+
rmSync(work, { recursive: true, force: true });
|
|
18
|
+
const dest = nested ? join(work, "node_modules", deps.pkg) : join(work, "node_modules");
|
|
19
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
20
|
+
try {
|
|
21
|
+
cpSync(source, dest, { recursive: true, verbatimSymlinks: true });
|
|
22
|
+
const copied = join(work, "node_modules", deps.pkg, "dist", "index.js");
|
|
23
|
+
if (!await (deps.verifyImpl ?? verifyEntry)(copied)) return { adopted: false, reason: "no-source" };
|
|
24
|
+
const target = join(versionsDir(deps.home), deps.version);
|
|
25
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
26
|
+
rmSync(target, { recursive: true, force: true });
|
|
27
|
+
renameSync(work, target);
|
|
28
|
+
} finally {
|
|
29
|
+
rmSync(work, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
pruneStaged(deps.home, deps.pkg, /* @__PURE__ */ new Set([deps.version]));
|
|
32
|
+
return { adopted: true };
|
|
33
|
+
}
|
|
34
|
+
function verifyEntry(entry) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
execFile(
|
|
37
|
+
process.execPath,
|
|
38
|
+
[entry, "verify"],
|
|
39
|
+
{ encoding: "utf8", timeout: 12e4, windowsHide: true, maxBuffer: 4 * 1024 * 1024 },
|
|
40
|
+
(err, stdout) => {
|
|
41
|
+
if (err) return resolve(false);
|
|
42
|
+
const line = String(stdout).trim().split("\n").reverse().find((l) => l.startsWith("{"));
|
|
43
|
+
try {
|
|
44
|
+
resolve(Boolean(line && JSON.parse(line).ok === true));
|
|
45
|
+
} catch {
|
|
46
|
+
resolve(false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function manifestVersion(pkgRoot) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8")).version ?? null;
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { adoptRunningInstall };
|
|
61
|
+
//# sourceMappingURL=chunk-QDODTMSN.js.map
|
|
62
|
+
//# sourceMappingURL=chunk-QDODTMSN.js.map
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { adoptRunningInstall } from './chunk-QDODTMSN.js';
|
|
2
|
+
import { detectInstallKind } from './chunk-PAIAXRAC.js';
|
|
3
|
+
import { readMeta } from './chunk-Y3ZPBPLP.js';
|
|
4
|
+
import { defaultHome, newestStaged, listStaged, compareSemver, entryOf } from './chunk-4MAFHYAD.js';
|
|
5
|
+
import { removeDesktop, installDesktop, POWERSHELL_ARGS, POWERSHELL } from './chunk-HLMR33DT.js';
|
|
6
|
+
import { launcherDir, logsDir } from './chunk-4NQKRVO2.js';
|
|
7
|
+
import { execFile, spawn } from 'child_process';
|
|
8
|
+
import { existsSync, closeSync, mkdirSync, writeFileSync, readFileSync, openSync, rmSync, statSync, renameSync, appendFileSync } from 'fs';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { join, dirname, delimiter } from 'path';
|
|
11
|
+
import { pathToFileURL } from 'url';
|
|
12
|
+
|
|
13
|
+
async function openInBrowser(url, platform, env) {
|
|
14
|
+
if (platform === "win32") {
|
|
15
|
+
await new Promise((resolve) => {
|
|
16
|
+
const child = spawn(POWERSHELL, [...POWERSHELL_ARGS, "Start-Process $env:SCENRI_URL"], {
|
|
17
|
+
stdio: "ignore",
|
|
18
|
+
windowsHide: true,
|
|
19
|
+
detached: true,
|
|
20
|
+
env: { ...env, SCENRI_URL: url }
|
|
21
|
+
});
|
|
22
|
+
child.once("error", () => resolve());
|
|
23
|
+
child.once("spawn", () => resolve());
|
|
24
|
+
child.unref();
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const { default: open } = await import('open');
|
|
29
|
+
await open(url);
|
|
30
|
+
}
|
|
31
|
+
var MESSAGE_BOX = "Add-Type -AssemblyName System.Windows.Forms | Out-Null; [System.Windows.Forms.MessageBox]::Show($env:SCENRI_MESSAGE, 'Scenri') | Out-Null";
|
|
32
|
+
function showDialog(platform, message, env) {
|
|
33
|
+
if (env.SCENRI_NO_DIALOG === "1") return Promise.resolve();
|
|
34
|
+
let cmd;
|
|
35
|
+
let args;
|
|
36
|
+
let childEnv;
|
|
37
|
+
if (platform === "darwin") {
|
|
38
|
+
cmd = "/usr/bin/osascript";
|
|
39
|
+
args = [
|
|
40
|
+
"-e",
|
|
41
|
+
"on run argv",
|
|
42
|
+
"-e",
|
|
43
|
+
'display dialog (item 1 of argv) with title "Scenri" buttons {"OK"} default button 1 with icon stop',
|
|
44
|
+
"-e",
|
|
45
|
+
"end run",
|
|
46
|
+
"--",
|
|
47
|
+
message
|
|
48
|
+
];
|
|
49
|
+
} else if (platform === "win32") {
|
|
50
|
+
cmd = POWERSHELL;
|
|
51
|
+
args = [...POWERSHELL_ARGS, MESSAGE_BOX];
|
|
52
|
+
childEnv = { ...env, SCENRI_MESSAGE: message };
|
|
53
|
+
} else {
|
|
54
|
+
console.error(`
|
|
55
|
+
${message}
|
|
56
|
+
`);
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
}
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
try {
|
|
61
|
+
const child = spawn(cmd, args, { stdio: "ignore", windowsHide: true, env: childEnv });
|
|
62
|
+
child.once("error", () => resolve());
|
|
63
|
+
child.once("exit", () => resolve());
|
|
64
|
+
} catch {
|
|
65
|
+
resolve();
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
var DEFAULT_MAX = 1024 * 1024;
|
|
70
|
+
function rotate(path, maxBytes) {
|
|
71
|
+
try {
|
|
72
|
+
if (statSync(path).size > maxBytes) renameSync(path, `${path}.1`);
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function appendLog(path, line, opts = {}) {
|
|
77
|
+
try {
|
|
78
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
79
|
+
rotate(path, opts.maxBytes ?? DEFAULT_MAX);
|
|
80
|
+
appendFileSync(path, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
81
|
+
`);
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function openLogFd(path, opts = {}) {
|
|
86
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
87
|
+
rotate(path, opts.maxBytes ?? 5 * DEFAULT_MAX);
|
|
88
|
+
return openSync(path, "a");
|
|
89
|
+
}
|
|
90
|
+
var LOCK_FRESH_MS = 6e4;
|
|
91
|
+
var QUICK_DEATH_MS = 1e4;
|
|
92
|
+
var NATIVE_MARKERS = ["NODE_MODULE_VERSION", "Could not locate the bindings file", "ERR_DLOPEN_FAILED"];
|
|
93
|
+
async function showBrowser(deps, url) {
|
|
94
|
+
try {
|
|
95
|
+
await deps.openBrowser(url);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
deps.log(`open: the browser could not be opened (${err instanceof Error ? err.message : String(err)})`);
|
|
98
|
+
const message = `Scenri is running at ${url} but the browser could not be opened. Open that address in your browser.`;
|
|
99
|
+
deps.log(`open: dialog: ${message}`);
|
|
100
|
+
await deps.showDialog(message);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function openScenri(deps) {
|
|
104
|
+
const port = Number(deps.env.SCENRI_PORT || 4747);
|
|
105
|
+
const url = `http://127.0.0.1:${port}/`;
|
|
106
|
+
const noOpen = deps.env.SCENRI_NO_OPEN === "1";
|
|
107
|
+
deps.log(`open: invoked by ${deps.version} for port ${port}`);
|
|
108
|
+
if (!takeLock(deps)) {
|
|
109
|
+
deps.log("open: another launch is in progress, stepping aside");
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const running = await deps.probe(`${url}api/version`, 2e3);
|
|
114
|
+
if (running?.name === deps.pkg) {
|
|
115
|
+
deps.log(`open: Scenri ${running.version ?? ""} already running, opening the browser`);
|
|
116
|
+
if (!noOpen) await showBrowser(deps, url);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
const fd = openLogFd(deps.serverLogPath);
|
|
120
|
+
const child = deps.spawnImpl(deps.execPath, [deps.ownEntry], {
|
|
121
|
+
detached: true,
|
|
122
|
+
stdio: ["ignore", fd, fd],
|
|
123
|
+
windowsHide: true,
|
|
124
|
+
env: {
|
|
125
|
+
...deps.env,
|
|
126
|
+
SCENRI_NO_OPEN: "1",
|
|
127
|
+
SCENRI_HEADLESS: "1",
|
|
128
|
+
// npm sits beside node for nvm, fnm, Volta, Homebrew and the installer,
|
|
129
|
+
// and a Finder or Explorer PATH has none of them: without this the
|
|
130
|
+
// server boots but one-click updates report no npm.
|
|
131
|
+
PATH: [dirname(deps.execPath), deps.env.PATH].filter(Boolean).join(delimiter)
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
child.unref();
|
|
135
|
+
closeSync(fd);
|
|
136
|
+
const started = deps.now();
|
|
137
|
+
let exit = null;
|
|
138
|
+
child.on("exit", (code) => {
|
|
139
|
+
exit = { code };
|
|
140
|
+
});
|
|
141
|
+
deps.log(`open: started the supervisor, pid ${child.pid ?? "unknown"}, log ${deps.serverLogPath}`);
|
|
142
|
+
let shown = false;
|
|
143
|
+
if (!noOpen && deps.startingTemplate && existsSync(deps.startingTemplate)) {
|
|
144
|
+
try {
|
|
145
|
+
mkdirSync(dirname(deps.startingPage), { recursive: true });
|
|
146
|
+
writeFileSync(deps.startingPage, renderStartingPage(readFileSync(deps.startingTemplate, "utf8"), url));
|
|
147
|
+
await deps.openBrowser(pathToFileURL(deps.startingPage).href);
|
|
148
|
+
shown = true;
|
|
149
|
+
deps.log("open: showing the starting page");
|
|
150
|
+
} catch (err) {
|
|
151
|
+
deps.log(`open: could not show the starting page (${err instanceof Error ? err.message : String(err)})`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const deadline = started + (deps.readyTimeoutMs ?? 9e4);
|
|
155
|
+
for (; ; ) {
|
|
156
|
+
const info = await deps.probe(`${url}api/version`, 1e3);
|
|
157
|
+
if (info?.name === deps.pkg) {
|
|
158
|
+
deps.log(`open: ready in ${deps.now() - started}ms`);
|
|
159
|
+
if (!noOpen && !shown) await showBrowser(deps, url);
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
if (exit) break;
|
|
163
|
+
if (deps.now() >= deadline) {
|
|
164
|
+
const message2 = "Scenri is taking too long to start. Open Terminal and run npx scenri to see what it is doing.";
|
|
165
|
+
deps.log(`open: dialog: ${message2}`);
|
|
166
|
+
await deps.showDialog(message2);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
await deps.sleep(deps.pollMs ?? 250);
|
|
170
|
+
}
|
|
171
|
+
const lived = deps.now() - started;
|
|
172
|
+
deps.log(`open: the server exited with ${exit.code} after ${lived}ms`);
|
|
173
|
+
const previous = deps.previousEntries[0];
|
|
174
|
+
if (lived < QUICK_DEATH_MS && previous && !deps.env.SCENRI_DESKTOP_FALLBACK) {
|
|
175
|
+
deps.log(`open: falling back to ${previous}`);
|
|
176
|
+
releaseLock(deps.lockPath);
|
|
177
|
+
const fallback = deps.spawnImpl(deps.execPath, [previous, "open"], {
|
|
178
|
+
stdio: "inherit",
|
|
179
|
+
env: {
|
|
180
|
+
...deps.env,
|
|
181
|
+
SCENRI_DESKTOP_FALLBACK: deps.version,
|
|
182
|
+
// the page is already up and polling; a second tab would be noise
|
|
183
|
+
...shown ? { SCENRI_NO_OPEN: "1" } : {}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
return await new Promise((resolve) => fallback.on("exit", (code) => resolve(code ?? 1)));
|
|
187
|
+
}
|
|
188
|
+
const message = explain(deps.readLogTail(), port);
|
|
189
|
+
deps.log(`open: dialog: ${message}`);
|
|
190
|
+
await deps.showDialog(message);
|
|
191
|
+
return 1;
|
|
192
|
+
} finally {
|
|
193
|
+
releaseLock(deps.lockPath);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
var STUDIO_URL = /^http:\/\/127\.0\.0\.1:\d{2,5}\/$/;
|
|
197
|
+
var STUDIO_META = '<meta name="scenri-studio" content="">';
|
|
198
|
+
function renderStartingPage(template, url) {
|
|
199
|
+
if (!STUDIO_URL.test(url)) throw new Error(`not a studio URL: ${url}`);
|
|
200
|
+
if (!template.includes(STUDIO_META)) throw new Error("starting page template has no scenri-studio meta");
|
|
201
|
+
return template.replace(STUDIO_META, `<meta name="scenri-studio" content="${url}">`);
|
|
202
|
+
}
|
|
203
|
+
function explain(tail2, port) {
|
|
204
|
+
if (/Port \d+ is in use by another app/.test(tail2)) {
|
|
205
|
+
return `Port ${port} is in use by another app. Quit that app, or open Terminal and start Scenri on another port: SCENRI_PORT=${port + 1} npx scenri`;
|
|
206
|
+
}
|
|
207
|
+
const newer = tail2.match(/This library was written by a newer Scenri[^\n]*/);
|
|
208
|
+
if (newer) return newer[0];
|
|
209
|
+
if (tail2.includes("a native component failed to load") || NATIVE_MARKERS.some((m) => tail2.includes(m))) {
|
|
210
|
+
return "Node.js changed since Scenri was installed. Open Terminal and run: npx scenri@latest";
|
|
211
|
+
}
|
|
212
|
+
if (NATIVE_MARKERS.some((m) => tail2.includes(m))) {
|
|
213
|
+
return "Scenri\u2019s files do not match this Node.js. Open Terminal and run: npx scenri";
|
|
214
|
+
}
|
|
215
|
+
return "Scenri could not start. Open Terminal and run npx scenri to see why.";
|
|
216
|
+
}
|
|
217
|
+
function takeLock(deps) {
|
|
218
|
+
try {
|
|
219
|
+
const held = JSON.parse(readFileSync(deps.lockPath, "utf8"));
|
|
220
|
+
const fresh = typeof held.at === "number" && deps.now() - held.at < LOCK_FRESH_MS;
|
|
221
|
+
if (fresh && typeof held.pid === "number" && held.pid !== process.pid && deps.pidAlive(held.pid)) return false;
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
writeFileSync(deps.lockPath, JSON.stringify({ pid: process.pid, at: deps.now() }));
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
function releaseLock(path) {
|
|
231
|
+
try {
|
|
232
|
+
const held = JSON.parse(readFileSync(path, "utf8"));
|
|
233
|
+
if (held.pid === process.pid) rmSync(path, { force: true });
|
|
234
|
+
} catch {
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/desktop/cli.ts
|
|
239
|
+
var assetsDirFor = (ownEntry) => join(dirname(ownEntry), "..", "launcher");
|
|
240
|
+
var runExecFile = (cmd, args, opts) => new Promise((resolve, reject) => {
|
|
241
|
+
execFile(
|
|
242
|
+
cmd,
|
|
243
|
+
args,
|
|
244
|
+
{ env: { ...process.env, ...opts?.env ?? {} }, windowsHide: true, encoding: "utf8", timeout: 3e4 },
|
|
245
|
+
(err, stdout) => err ? reject(err) : resolve(stdout)
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
function installDeps(ownEntry) {
|
|
249
|
+
return {
|
|
250
|
+
platform: process.platform,
|
|
251
|
+
homedir: homedir(),
|
|
252
|
+
home: defaultHome(),
|
|
253
|
+
execPath: process.execPath,
|
|
254
|
+
env: process.env,
|
|
255
|
+
version: readMeta().version,
|
|
256
|
+
assetsDir: assetsDirFor(ownEntry),
|
|
257
|
+
runImpl: runExecFile
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
async function runDesktopCommand(opts, ownEntry) {
|
|
261
|
+
if (opts.remove) {
|
|
262
|
+
const deps = installDeps(ownEntry);
|
|
263
|
+
const res = await removeDesktop(deps);
|
|
264
|
+
console.log(res.removed ? ` Removed Scenri from your desktop (${res.path}).` : " Nothing to remove.");
|
|
265
|
+
if (res.message) console.log(` ${res.message}`);
|
|
266
|
+
await rememberDecline(deps.home);
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
return (await addToDesktop(ownEntry)).ok ? 0 : 1;
|
|
270
|
+
}
|
|
271
|
+
async function addToDesktop(ownEntry, say = console.log, over = {}) {
|
|
272
|
+
const deps = { ...installDeps(ownEntry), ...over };
|
|
273
|
+
const pkg = over.pkg ?? readMeta().name;
|
|
274
|
+
const installKind = over.installKind ?? detectInstallKind(ownEntry, deps.home);
|
|
275
|
+
if (installKind === "dev") {
|
|
276
|
+
const message = "Running from a source checkout; there is no installed build to put on a desktop.";
|
|
277
|
+
say(` ${message}`);
|
|
278
|
+
return { ok: false, reason: "unsupported", message };
|
|
279
|
+
}
|
|
280
|
+
const adopted = await adoptRunningInstall({
|
|
281
|
+
home: deps.home,
|
|
282
|
+
pkg,
|
|
283
|
+
version: deps.version,
|
|
284
|
+
ownEntry,
|
|
285
|
+
installKind,
|
|
286
|
+
verifyImpl: over.verifyImpl
|
|
287
|
+
});
|
|
288
|
+
if (adopted.adopted) {
|
|
289
|
+
say(` keeping a copy of Scenri ${deps.version} in ${join(deps.home, "app")} so the icon works offline`);
|
|
290
|
+
}
|
|
291
|
+
if (!newestStaged(deps.home, pkg)) {
|
|
292
|
+
const message = "Scenri could not keep a runnable copy of this build for the icon. Run npx scenri once in a terminal, then try again.";
|
|
293
|
+
say(` ${message}`);
|
|
294
|
+
return { ok: false, reason: "failed", message };
|
|
295
|
+
}
|
|
296
|
+
const res = await installDesktop(deps);
|
|
297
|
+
if (res.ok) {
|
|
298
|
+
say(` Added Scenri to your desktop: ${res.path}`);
|
|
299
|
+
say(" Next time, double-click it; no terminal needed.");
|
|
300
|
+
} else {
|
|
301
|
+
say(` ${res.message}`);
|
|
302
|
+
}
|
|
303
|
+
return res;
|
|
304
|
+
}
|
|
305
|
+
async function rememberDecline(home) {
|
|
306
|
+
try {
|
|
307
|
+
const { createCore } = await import('./src-CSR34EBZ.js');
|
|
308
|
+
const core = createCore(home);
|
|
309
|
+
try {
|
|
310
|
+
core.store.setSetting("desktop.prompt", "declined");
|
|
311
|
+
} finally {
|
|
312
|
+
core.close();
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function runOpenCommand(ownEntry) {
|
|
318
|
+
const meta = readMeta();
|
|
319
|
+
const home = defaultHome();
|
|
320
|
+
const support = launcherDir(homedir());
|
|
321
|
+
const logs = logsDir(home);
|
|
322
|
+
const serverLogPath = join(logs, "scenri.log");
|
|
323
|
+
const platform = process.platform;
|
|
324
|
+
const previous = listStaged(home, meta.name).filter((v) => compareSemver(v, meta.version) < 0).reverse().map((v) => entryOf(home, meta.name, v));
|
|
325
|
+
return openScenri({
|
|
326
|
+
home,
|
|
327
|
+
pkg: meta.name,
|
|
328
|
+
version: meta.version,
|
|
329
|
+
ownEntry,
|
|
330
|
+
env: process.env,
|
|
331
|
+
execPath: process.execPath,
|
|
332
|
+
probe: async (url, timeoutMs) => {
|
|
333
|
+
try {
|
|
334
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
335
|
+
return res.ok ? await res.json() : null;
|
|
336
|
+
} catch {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
spawnImpl: spawn,
|
|
341
|
+
openBrowser: (url) => openInBrowser(url, platform, process.env),
|
|
342
|
+
showDialog: (message) => showDialog(platform, message, process.env),
|
|
343
|
+
log: (line) => appendLog(join(logs, "launcher.log"), line),
|
|
344
|
+
now: Date.now,
|
|
345
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
346
|
+
lockPath: join(support, "open.lock"),
|
|
347
|
+
pidAlive,
|
|
348
|
+
serverLogPath,
|
|
349
|
+
readLogTail: () => tail(serverLogPath),
|
|
350
|
+
startingTemplate: join(assetsDirFor(ownEntry), "starting.html"),
|
|
351
|
+
startingPage: join(support, "starting.html"),
|
|
352
|
+
previousEntries: previous
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function pidAlive(pid) {
|
|
356
|
+
try {
|
|
357
|
+
process.kill(pid, 0);
|
|
358
|
+
return true;
|
|
359
|
+
} catch (err) {
|
|
360
|
+
return err.code === "EPERM";
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function tail(path, bytes = 4096) {
|
|
364
|
+
try {
|
|
365
|
+
const size = statSync(path).size;
|
|
366
|
+
const buf = readFileSync(path);
|
|
367
|
+
return buf.subarray(Math.max(0, size - bytes)).toString("utf8");
|
|
368
|
+
} catch {
|
|
369
|
+
return "";
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
var supportInstalled = () => existsSync(join(launcherDir(homedir()), "launcher.json"));
|
|
373
|
+
|
|
374
|
+
export { addToDesktop, assetsDirFor, installDeps, runDesktopCommand, runExecFile, runOpenCommand, supportInstalled };
|
|
375
|
+
//# sourceMappingURL=cli-BVPVS4V5.js.map
|
|
376
|
+
//# sourceMappingURL=cli-BVPVS4V5.js.map
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,15 @@ function parseArgs(argv) {
|
|
|
10
10
|
if (first === "--help" || first === "-h") return { cmd: "help" };
|
|
11
11
|
if (first === "serve") return { cmd: "serve" };
|
|
12
12
|
if (first === "verify") return { cmd: "verify" };
|
|
13
|
+
if (first === "open") return { cmd: "open" };
|
|
14
|
+
if (first === "desktop") {
|
|
15
|
+
let remove = false;
|
|
16
|
+
for (const a of rest) {
|
|
17
|
+
if (a === "--remove") remove = true;
|
|
18
|
+
else return { cmd: "error", message: `unknown option '${a}' (try --help)` };
|
|
19
|
+
}
|
|
20
|
+
return { cmd: "desktop", remove };
|
|
21
|
+
}
|
|
13
22
|
if (first === "update") {
|
|
14
23
|
let check = false;
|
|
15
24
|
let from;
|
|
@@ -33,12 +42,14 @@ Usage
|
|
|
33
42
|
scenri serve start this exact build, no supervision
|
|
34
43
|
scenri update download and stage the newest version
|
|
35
44
|
--check only check, do not download
|
|
45
|
+
scenri desktop add Scenri to your desktop (or repair the icon)
|
|
46
|
+
--remove take it off again
|
|
36
47
|
scenri --version print the installed version
|
|
37
48
|
scenri --help this text
|
|
38
49
|
|
|
39
50
|
Configuration is via environment variables: SCENRI_PORT, SCENRI_HOST,
|
|
40
51
|
SCENRI_HOME, SCENRI_NO_OPEN, SCENRI_NO_UPDATE_CHECK, SCENRI_NO_CONTENT_FETCH,
|
|
41
|
-
SCENRI_CONTENT_URL.
|
|
52
|
+
SCENRI_CONTENT_URL, SCENRI_NO_DESKTOP.
|
|
42
53
|
`;
|
|
43
54
|
}
|
|
44
55
|
|
|
@@ -74,13 +85,23 @@ try {
|
|
|
74
85
|
process.exit(await runUpdateCommand(command));
|
|
75
86
|
break;
|
|
76
87
|
}
|
|
88
|
+
case "desktop": {
|
|
89
|
+
const { runDesktopCommand } = await import('./cli-BVPVS4V5.js');
|
|
90
|
+
process.exit(await runDesktopCommand(command, fileURLToPath(import.meta.url)));
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "open": {
|
|
94
|
+
const { runOpenCommand } = await import('./cli-BVPVS4V5.js');
|
|
95
|
+
process.exit(await runOpenCommand(fileURLToPath(import.meta.url)));
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
77
98
|
case "launch": {
|
|
78
99
|
const ownEntry = fileURLToPath(import.meta.url);
|
|
79
100
|
if (!isBuiltEntry(ownEntry)) {
|
|
80
101
|
await (await import('./serve.js')).serve();
|
|
81
102
|
break;
|
|
82
103
|
}
|
|
83
|
-
const { runLauncher } = await import('./launcher-
|
|
104
|
+
const { runLauncher } = await import('./launcher-IVFIYHPN.js');
|
|
84
105
|
const { readMeta } = await import('./meta-MPQPBPBK.js');
|
|
85
106
|
const { defaultHome } = await import('./versionsDir-ILQ4CI7B.js');
|
|
86
107
|
const meta = readMeta();
|
|
@@ -38,6 +38,10 @@ async function runLauncher(deps) {
|
|
|
38
38
|
}
|
|
39
39
|
child = spawnImpl(process.execPath, [entry, "serve"], {
|
|
40
40
|
stdio: "inherit",
|
|
41
|
+
// A supervisor the desktop icon started has no console; without this
|
|
42
|
+
// Windows would open one for serve, the very window the icon avoids.
|
|
43
|
+
// Under a terminal the console stays shared so Ctrl-C reaches serve.
|
|
44
|
+
windowsHide: baseEnv.SCENRI_HEADLESS === "1",
|
|
41
45
|
env: {
|
|
42
46
|
...baseEnv,
|
|
43
47
|
SCENRI_SUPERVISED: "1",
|
|
@@ -61,5 +65,5 @@ async function runLauncher(deps) {
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
export { LAUNCHER_PROTOCOL, RESTART_EXIT_CODE, runLauncher };
|
|
64
|
-
//# sourceMappingURL=launcher-
|
|
65
|
-
//# sourceMappingURL=launcher-
|
|
68
|
+
//# sourceMappingURL=launcher-IVFIYHPN.js.map
|
|
69
|
+
//# sourceMappingURL=launcher-IVFIYHPN.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createInterface } from 'readline';
|
|
2
|
+
|
|
3
|
+
// src/desktop/offer.ts
|
|
4
|
+
function shouldOfferDesktop(i) {
|
|
5
|
+
if (i.env.SCENRI_NO_DESKTOP === "1") return false;
|
|
6
|
+
if (!i.stdinTTY || !i.stdoutTTY) return false;
|
|
7
|
+
if (i.env.CI) return false;
|
|
8
|
+
if (i.env.SSH_TTY) return false;
|
|
9
|
+
if (i.platform !== "darwin" && i.platform !== "win32") return false;
|
|
10
|
+
if (i.installKind === "dev") return false;
|
|
11
|
+
if (i.launcherInstalled) return false;
|
|
12
|
+
if (i.declined) return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
var OFFER_QUESTION = " Add Scenri to your desktop? Then you can open it without a terminal. [Y/n] ";
|
|
16
|
+
async function offerDesktop(deps) {
|
|
17
|
+
let answer;
|
|
18
|
+
try {
|
|
19
|
+
answer = (await deps.ask(OFFER_QUESTION)).trim().toLowerCase();
|
|
20
|
+
} catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (answer === "" || answer.startsWith("y")) {
|
|
24
|
+
await deps.add();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
deps.decline();
|
|
28
|
+
deps.say(" Not now. Add it later with: npx scenri desktop, or from Settings > About.");
|
|
29
|
+
}
|
|
30
|
+
function askOnTerminal(question) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
|
|
33
|
+
let done = false;
|
|
34
|
+
rl.question(question, (answer) => {
|
|
35
|
+
done = true;
|
|
36
|
+
rl.close();
|
|
37
|
+
resolve(answer);
|
|
38
|
+
});
|
|
39
|
+
rl.once("close", () => {
|
|
40
|
+
if (!done) reject(new Error("stdin closed"));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { OFFER_QUESTION, askOnTerminal, offerDesktop, shouldOfferDesktop };
|
|
46
|
+
//# sourceMappingURL=offer-IVTNHIDJ.js.map
|
|
47
|
+
//# sourceMappingURL=offer-IVTNHIDJ.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { LAUNCHER_SCHEMA, desktopDir, isSupportedPlatform, launcherDir, launcherInstalled, launcherRecordPath, logsDir, readLauncherRecord, recordedEnv, writeLauncherRecord } from './chunk-4NQKRVO2.js';
|
|
2
|
+
//# sourceMappingURL=paths-36Y73GGY.js.map
|
|
3
|
+
//# sourceMappingURL=paths-36Y73GGY.js.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { adoptRunningInstall } from './chunk-QDODTMSN.js';
|
|
2
|
+
import { newestStaged, compareSemver } from './chunk-4MAFHYAD.js';
|
|
3
|
+
import { isOurMacBundle, runningNodeMajor, MAC_SCRIPT, macPlist, writeSupportFiles, writeMacBundle, writeLnk } from './chunk-HLMR33DT.js';
|
|
4
|
+
import { readLauncherRecord, launcherDir, LAUNCHER_SCHEMA, writeLauncherRecord } from './chunk-4NQKRVO2.js';
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
|
|
8
|
+
var ASSETS = ["launch.mjs", "Scenri.icns", "scenri.ico"];
|
|
9
|
+
async function refreshLauncher(deps) {
|
|
10
|
+
if (deps.env.SCENRI_NO_DESKTOP === "1") return {};
|
|
11
|
+
const record = readLauncherRecord(deps.homedir);
|
|
12
|
+
if (!record) return {};
|
|
13
|
+
const out = {};
|
|
14
|
+
if (deps.installKind === "npx" || deps.installKind === "global") {
|
|
15
|
+
const newest = newestStaged(deps.home, deps.pkg);
|
|
16
|
+
if (!newest || compareSemver(deps.version, newest) > 0) {
|
|
17
|
+
const r = await adoptRunningInstall({
|
|
18
|
+
home: deps.home,
|
|
19
|
+
pkg: deps.pkg,
|
|
20
|
+
version: deps.version,
|
|
21
|
+
ownEntry: deps.ownEntry,
|
|
22
|
+
installKind: deps.installKind,
|
|
23
|
+
verifyImpl: deps.verifyImpl
|
|
24
|
+
});
|
|
25
|
+
if (r.adopted) out.adopted = true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const support = launcherDir(deps.homedir);
|
|
29
|
+
const artifact = record.artifact.path;
|
|
30
|
+
const ours = existsSync(artifact) && (record.artifact.kind === "macos-app" ? isOurMacBundle(artifact) : true);
|
|
31
|
+
const schemaStale = record.schema !== LAUNCHER_SCHEMA;
|
|
32
|
+
const nodeGone = !existsSync(record.nodePath);
|
|
33
|
+
const nodePath = nodeGone ? deps.execPath : record.nodePath;
|
|
34
|
+
const nodeMajor = nodeGone ? runningNodeMajor() : record.nodeMajor ?? runningNodeMajor();
|
|
35
|
+
const nodeFilesStale = deps.platform === "darwin" && (readText(join(support, "node-path")) !== `${nodePath}
|
|
36
|
+
` || readText(join(support, "node-major")) !== `${nodeMajor}
|
|
37
|
+
`);
|
|
38
|
+
const supportStale = nodeFilesStale || ASSETS.some((f) => !sameFile(join(support, f), join(deps.assetsDir, f)));
|
|
39
|
+
const mac = ours && record.artifact.kind === "macos-app";
|
|
40
|
+
const iconStale = mac && !sameFile(join(artifact, "Contents", "Resources", "Scenri.icns"), join(deps.assetsDir, "Scenri.icns"));
|
|
41
|
+
const bundleStale = mac && (readText(join(artifact, "Contents", "MacOS", "Scenri")) !== MAC_SCRIPT || readText(join(artifact, "Contents", "Info.plist")) !== macPlist({ version: deps.version, schema: LAUNCHER_SCHEMA }));
|
|
42
|
+
if (schemaStale || supportStale || nodeGone) {
|
|
43
|
+
writeSupportFiles({ assetsDir: deps.assetsDir, execPath: nodePath, platform: deps.platform, nodeMajor }, support);
|
|
44
|
+
}
|
|
45
|
+
if (mac && (schemaStale || iconStale || bundleStale)) {
|
|
46
|
+
writeMacBundle({
|
|
47
|
+
path: artifact,
|
|
48
|
+
icns: join(support, "Scenri.icns"),
|
|
49
|
+
version: deps.version,
|
|
50
|
+
schema: LAUNCHER_SCHEMA
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (ours && record.artifact.kind === "windows-lnk" && (schemaStale || nodeGone)) {
|
|
54
|
+
await writeLnk(deps.runImpl, {
|
|
55
|
+
path: artifact,
|
|
56
|
+
target: nodePath,
|
|
57
|
+
args: `"${join(support, "launch.mjs")}"`,
|
|
58
|
+
workdir: support,
|
|
59
|
+
icon: `${join(support, "scenri.ico")},0`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (schemaStale || supportStale || nodeGone || iconStale || bundleStale) {
|
|
63
|
+
writeLauncherRecord(deps.homedir, {
|
|
64
|
+
...record,
|
|
65
|
+
schema: LAUNCHER_SCHEMA,
|
|
66
|
+
createdBy: deps.version,
|
|
67
|
+
nodePath,
|
|
68
|
+
nodeMajor
|
|
69
|
+
});
|
|
70
|
+
out.refreshed = true;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function readText(path) {
|
|
75
|
+
try {
|
|
76
|
+
return readFileSync(path, "utf8");
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function sameFile(a, b) {
|
|
82
|
+
try {
|
|
83
|
+
return readFileSync(a).equals(readFileSync(b));
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { refreshLauncher };
|
|
90
|
+
//# sourceMappingURL=refresh-7HXPUHLQ.js.map
|
|
91
|
+
//# sourceMappingURL=refresh-7HXPUHLQ.js.map
|