scenri 0.8.3 → 0.9.1

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.
@@ -0,0 +1,526 @@
1
+ import { adoptRunningInstall } from './chunk-ECS5LRHB.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 } from './chunk-SDVL5OJF.js';
6
+ import { launcherDir, logsDir, powershellPath } from './chunk-2IGINCPU.js';
7
+ import { execFile, spawn } from 'child_process';
8
+ import { existsSync, closeSync, readFileSync, writeFileSync, mkdirSync, openSync, rmSync, statSync, renameSync, appendFileSync } from 'fs';
9
+ import { homedir } from 'os';
10
+ import { join, dirname, delimiter, win32 } from 'path';
11
+ import { createServer } from 'http';
12
+
13
+ var HELPER_TIMEOUT_MS = 1e4;
14
+ async function openInBrowser(url, platform, env, impl = {}) {
15
+ const timeoutMs = impl.timeoutMs ?? HELPER_TIMEOUT_MS;
16
+ if (platform === "win32") {
17
+ const spawnImpl = impl.spawn ?? spawn;
18
+ const root = env.SystemRoot || env.SYSTEMROOT || env.windir || "C:\\Windows";
19
+ const helpers = [
20
+ {
21
+ cmd: powershellPath(env),
22
+ args: [...POWERSHELL_ARGS, "Start-Process $env:SCENRI_URL"],
23
+ env: { ...env, SCENRI_URL: url },
24
+ ok: (code) => code === 0
25
+ },
26
+ {
27
+ cmd: win32.join(root, "System32", "rundll32.exe"),
28
+ args: ["url.dll,FileProtocolHandler", url],
29
+ ok: (c) => c === 0
30
+ },
31
+ // explorer.exe answers 1 even when it did the job; only a failure to start counts.
32
+ { cmd: win32.join(root, "explorer.exe"), args: [url], ok: (code) => code !== null }
33
+ ];
34
+ const reasons = [];
35
+ for (const helper of helpers) {
36
+ const result = await runHelper(spawnImpl, helper, env, timeoutMs);
37
+ if (result.ok) return;
38
+ reasons.push(`${win32.basename(helper.cmd)}: ${result.reason}`);
39
+ }
40
+ throw new Error(`no helper could open the browser (${reasons.join("; ")})`);
41
+ }
42
+ const openImpl = impl.open ?? (async (u) => await (await import('open')).default(u));
43
+ const child = await openImpl(url);
44
+ child.ref?.();
45
+ await new Promise((resolve, reject) => {
46
+ let done = false;
47
+ const finish = (err) => {
48
+ if (done) return;
49
+ done = true;
50
+ clearTimeout(timer);
51
+ err ? reject(err) : resolve();
52
+ };
53
+ const timer = setTimeout(() => finish(), timeoutMs);
54
+ child.once("error", (err) => finish(err));
55
+ child.once(
56
+ "close",
57
+ (code) => finish(code === 0 || code === null ? void 0 : new Error(`the opener exited with ${code}`))
58
+ );
59
+ });
60
+ }
61
+ function runHelper(spawnImpl, helper, env, timeoutMs) {
62
+ return new Promise((resolve) => {
63
+ let done = false;
64
+ let stderr = "";
65
+ const finish = (result) => {
66
+ if (done) return;
67
+ done = true;
68
+ clearTimeout(timer);
69
+ resolve(result);
70
+ };
71
+ let child;
72
+ try {
73
+ child = spawnImpl(helper.cmd, helper.args, {
74
+ stdio: ["ignore", "ignore", "pipe"],
75
+ windowsHide: true,
76
+ env: helper.env ?? env
77
+ });
78
+ } catch (err) {
79
+ return finish({ ok: false, reason: err instanceof Error ? err.message : String(err) });
80
+ }
81
+ const timer = setTimeout(() => {
82
+ try {
83
+ child.kill();
84
+ } catch {
85
+ }
86
+ finish({ ok: false, reason: `no answer within ${Math.round(timeoutMs / 1e3)} s` });
87
+ }, timeoutMs);
88
+ child.stderr?.on("data", (d) => {
89
+ stderr += String(d);
90
+ });
91
+ child.once("error", (err) => finish({ ok: false, reason: err.message }));
92
+ child.once("close", (code) => {
93
+ if (helper.ok(code)) return finish({ ok: true });
94
+ const detail = stderr.trim().split(/\r?\n/)[0];
95
+ finish({ ok: false, reason: `exit ${code}${detail ? `: ${detail}` : ""}` });
96
+ });
97
+ });
98
+ }
99
+ var MESSAGE_BOX = "Add-Type -AssemblyName System.Windows.Forms | Out-Null; [System.Windows.Forms.MessageBox]::Show($env:SCENRI_MESSAGE, 'Scenri') | Out-Null";
100
+ function showDialog(platform, message, env, spawnImpl = spawn) {
101
+ if (env.SCENRI_NO_DIALOG === "1") return Promise.resolve();
102
+ let cmd;
103
+ let args;
104
+ let opts;
105
+ if (platform === "darwin") {
106
+ cmd = "/usr/bin/osascript";
107
+ args = [
108
+ "-e",
109
+ "on run argv",
110
+ "-e",
111
+ 'display dialog (item 1 of argv) with title "Scenri" buttons {"OK"} default button 1 with icon stop',
112
+ "-e",
113
+ "end run",
114
+ "--",
115
+ message
116
+ ];
117
+ opts = { stdio: "ignore" };
118
+ } else if (platform === "win32") {
119
+ cmd = powershellPath(env);
120
+ args = [...POWERSHELL_ARGS, MESSAGE_BOX];
121
+ opts = { stdio: "ignore", windowsHide: true, env: { ...env, SCENRI_MESSAGE: message } };
122
+ } else {
123
+ console.error(`
124
+ ${message}
125
+ `);
126
+ return Promise.resolve();
127
+ }
128
+ return new Promise((resolve) => {
129
+ try {
130
+ const child = spawnImpl(cmd, args, opts);
131
+ child.once("error", () => resolve());
132
+ child.once("exit", () => resolve());
133
+ } catch {
134
+ resolve();
135
+ }
136
+ });
137
+ }
138
+ var DEFAULT_MAX = 1024 * 1024;
139
+ function rotate(path, maxBytes) {
140
+ try {
141
+ if (statSync(path).size > maxBytes) renameSync(path, `${path}.1`);
142
+ } catch {
143
+ }
144
+ }
145
+ function appendLog(path, line, opts = {}) {
146
+ try {
147
+ mkdirSync(dirname(path), { recursive: true });
148
+ rotate(path, opts.maxBytes ?? DEFAULT_MAX);
149
+ appendFileSync(path, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
150
+ `);
151
+ } catch {
152
+ }
153
+ }
154
+ function openLogFd(path, opts = {}) {
155
+ mkdirSync(dirname(path), { recursive: true });
156
+ rotate(path, opts.maxBytes ?? 5 * DEFAULT_MAX);
157
+ return openSync(path, "a");
158
+ }
159
+ var LOCK_FRESH_MS = 6e4;
160
+ var READY_TIMEOUT_MS = 18e4;
161
+ var PAGE_FETCH_GRACE_MS = 15e3;
162
+ var QUICK_DEATH_MS = 1e4;
163
+ var NATIVE_MARKERS = ["NODE_MODULE_VERSION", "Could not locate the bindings file", "ERR_DLOPEN_FAILED"];
164
+ async function showBrowser(deps, url) {
165
+ try {
166
+ await deps.openBrowser(url);
167
+ } catch (err) {
168
+ deps.log(`open: the browser could not be opened (${err instanceof Error ? err.message : String(err)})`);
169
+ const message = `Scenri is running at ${url} but the browser could not be opened. Open that address in your browser.`;
170
+ deps.log(`open: dialog: ${message}`);
171
+ await deps.showDialog(message);
172
+ }
173
+ }
174
+ async function openScenri(deps) {
175
+ const port = Number(deps.env.SCENRI_PORT || 4747);
176
+ const url = `http://127.0.0.1:${port}/`;
177
+ const noOpen = deps.env.SCENRI_NO_OPEN === "1";
178
+ deps.log(
179
+ `open: invoked by ${deps.version} (${deps.execPath}, node ${process.version}, ${process.platform}, cwd ${process.cwd()}) for port ${port}`
180
+ );
181
+ let page = null;
182
+ if (!takeLock(deps)) {
183
+ const running = await deps.probe(`${url}api/version`, 2e3);
184
+ if (running?.name === deps.pkg) {
185
+ deps.log("open: another launch is in progress and Scenri already answers, opening the browser");
186
+ if (!noOpen) await showBrowser(deps, url);
187
+ return 0;
188
+ }
189
+ deps.log("open: another launch is in progress, stepping aside");
190
+ return 0;
191
+ }
192
+ try {
193
+ const running = await deps.probe(`${url}api/version`, 2e3);
194
+ if (running?.name === deps.pkg) {
195
+ deps.log(`open: Scenri ${running.version ?? ""} already running, opening the browser`);
196
+ if (!noOpen) await showBrowser(deps, url);
197
+ return 0;
198
+ }
199
+ const fd = openLogFd(deps.serverLogPath);
200
+ const started = deps.now();
201
+ let exit = null;
202
+ let spawnError = null;
203
+ let child;
204
+ try {
205
+ child = deps.spawnImpl(deps.execPath, [deps.ownEntry], {
206
+ detached: true,
207
+ stdio: ["ignore", fd, fd],
208
+ windowsHide: true,
209
+ env: {
210
+ ...deps.env,
211
+ SCENRI_NO_OPEN: "1",
212
+ SCENRI_HEADLESS: "1",
213
+ // npm sits beside node for nvm, fnm, Volta, Homebrew and the installer,
214
+ // and a Finder or Explorer PATH has none of them: without this the
215
+ // server boots but one-click updates report no npm.
216
+ PATH: [dirname(deps.execPath), deps.env.PATH].filter(Boolean).join(delimiter)
217
+ }
218
+ });
219
+ } catch (err) {
220
+ closeSync(fd);
221
+ return await couldNotStart(deps, err);
222
+ }
223
+ child.unref();
224
+ closeSync(fd);
225
+ child.on("error", (err) => {
226
+ spawnError = err;
227
+ exit = { code: null };
228
+ });
229
+ child.on("exit", (code) => {
230
+ exit = { code };
231
+ });
232
+ deps.log(`open: started the supervisor, pid ${child.pid ?? "unknown"}, log ${deps.serverLogPath}`);
233
+ let shown = false;
234
+ if (!noOpen && deps.startingTemplate && existsSync(deps.startingTemplate) && deps.startingServer) {
235
+ try {
236
+ page = await deps.startingServer(renderStartingPage(readFileSync(deps.startingTemplate, "utf8"), url));
237
+ deps.log(`open: serving the starting page at ${page.url}`);
238
+ await deps.openBrowser(page.url);
239
+ shown = true;
240
+ deps.log("open: showing the starting page");
241
+ } catch (err) {
242
+ deps.log(`open: could not show the starting page (${err instanceof Error ? err.message : String(err)})`);
243
+ }
244
+ }
245
+ const deadline = started + (deps.readyTimeoutMs ?? READY_TIMEOUT_MS);
246
+ for (; ; ) {
247
+ const info = await deps.probe(`${url}api/version`, 1e3);
248
+ if (info?.name === deps.pkg) {
249
+ deps.log(`open: ready in ${deps.now() - started}ms`);
250
+ if (!noOpen) await handOver(deps, url, shown ? page : null);
251
+ return 0;
252
+ }
253
+ if (exit) break;
254
+ if (deps.now() >= deadline) {
255
+ const message2 = "Scenri is taking too long to start. Open Terminal and run npx scenri to see what it is doing.";
256
+ deps.log(`open: dialog: ${message2}`);
257
+ await deps.showDialog(message2);
258
+ return 1;
259
+ }
260
+ await deps.sleep(deps.pollMs ?? 250);
261
+ }
262
+ if (spawnError) return await couldNotStart(deps, spawnError);
263
+ const lived = deps.now() - started;
264
+ deps.log(`open: the server exited with ${exit.code} after ${lived}ms`);
265
+ const previous = deps.previousEntries[0];
266
+ if (lived < QUICK_DEATH_MS && previous && !deps.env.SCENRI_DESKTOP_FALLBACK) {
267
+ deps.log(`open: falling back to ${previous}`);
268
+ releaseLock(deps.lockPath);
269
+ const fallback = deps.spawnImpl(deps.execPath, [previous, "open"], {
270
+ stdio: "inherit",
271
+ // This process has no console; without the hint Windows would give
272
+ // the fallback a visible one.
273
+ windowsHide: true,
274
+ env: {
275
+ ...deps.env,
276
+ SCENRI_DESKTOP_FALLBACK: deps.version,
277
+ // the page is already up and polling; a second tab would be noise
278
+ ...shown ? { SCENRI_NO_OPEN: "1" } : {}
279
+ }
280
+ });
281
+ return await new Promise((resolve) => {
282
+ fallback.on("error", (err) => {
283
+ deps.log(`open: could not start the fallback (${err.message})`);
284
+ resolve(1);
285
+ });
286
+ fallback.on("exit", (code) => resolve(code ?? 1));
287
+ });
288
+ }
289
+ const message = explain(deps.readLogTail(), port);
290
+ deps.log(`open: dialog: ${message}`);
291
+ await deps.showDialog(message);
292
+ return 1;
293
+ } finally {
294
+ page?.close();
295
+ releaseLock(deps.lockPath);
296
+ }
297
+ }
298
+ async function handOver(deps, url, page) {
299
+ if (!page) return showBrowser(deps, url);
300
+ const until = deps.now() + PAGE_FETCH_GRACE_MS;
301
+ while (!page.wasFetched() && deps.now() < until) await deps.sleep(deps.pollMs ?? 250);
302
+ if (page.wasFetched()) {
303
+ deps.log(`open: the browser fetched the starting page (${page.userAgent() ?? "unknown browser"})`);
304
+ return;
305
+ }
306
+ deps.log("open: the browser never fetched the starting page, opening the studio directly");
307
+ await showBrowser(deps, url);
308
+ }
309
+ var STUDIO_URL = /^http:\/\/127\.0\.0\.1:\d{2,5}\/$/;
310
+ var STUDIO_META = '<meta name="scenri-studio" content="">';
311
+ function renderStartingPage(template, url) {
312
+ if (!STUDIO_URL.test(url)) throw new Error(`not a studio URL: ${url}`);
313
+ if (!template.includes(STUDIO_META)) throw new Error("starting page template has no scenri-studio meta");
314
+ return template.replace(STUDIO_META, `<meta name="scenri-studio" content="${url}">`);
315
+ }
316
+ function explain(tail2, port) {
317
+ if (/Port \d+ is in use by another app/.test(tail2)) {
318
+ 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`;
319
+ }
320
+ const newer = tail2.match(/This library was written by a newer Scenri[^\n]*/);
321
+ if (newer) return newer[0];
322
+ if (tail2.includes("a native component failed to load") || NATIVE_MARKERS.some((m) => tail2.includes(m))) {
323
+ return "Node.js changed since Scenri was installed. Open Terminal and run: npx scenri@latest";
324
+ }
325
+ return "Scenri could not start. Open Terminal and run npx scenri to see why.";
326
+ }
327
+ async function couldNotStart(deps, err) {
328
+ const reason = err instanceof Error ? err.message : String(err);
329
+ deps.log(`open: could not start the supervisor (${reason})`);
330
+ const message = `Scenri could not start (${reason}). Open Terminal and run npx scenri to see why.`;
331
+ deps.log(`open: dialog: ${message}`);
332
+ await deps.showDialog(message);
333
+ return 1;
334
+ }
335
+ function takeLock(deps) {
336
+ try {
337
+ const held = JSON.parse(readFileSync(deps.lockPath, "utf8"));
338
+ const fresh = typeof held.at === "number" && deps.now() - held.at < LOCK_FRESH_MS;
339
+ if (fresh && typeof held.pid === "number" && held.pid !== process.pid && deps.pidAlive(held.pid)) return false;
340
+ } catch {
341
+ }
342
+ try {
343
+ writeFileSync(deps.lockPath, JSON.stringify({ pid: process.pid, at: deps.now() }));
344
+ } catch {
345
+ }
346
+ return true;
347
+ }
348
+ function releaseLock(path) {
349
+ try {
350
+ const held = JSON.parse(readFileSync(path, "utf8"));
351
+ if (held.pid === process.pid) rmSync(path, { force: true });
352
+ } catch {
353
+ }
354
+ }
355
+ function serveStartingPage(html) {
356
+ let fetched = false;
357
+ let agent = null;
358
+ const server = createServer((req, res) => {
359
+ if (req.method !== "GET" && req.method !== "HEAD" || req.url !== "/") {
360
+ res.statusCode = 404;
361
+ res.end();
362
+ return;
363
+ }
364
+ fetched = true;
365
+ const ua = req.headers["user-agent"];
366
+ agent = typeof ua === "string" ? ua : null;
367
+ res.setHeader("content-type", "text/html; charset=utf-8");
368
+ res.setHeader("cache-control", "no-store");
369
+ res.end(html);
370
+ });
371
+ return new Promise((resolve, reject) => {
372
+ server.once("error", reject);
373
+ server.listen(0, "127.0.0.1", () => {
374
+ const { port } = server.address();
375
+ resolve({
376
+ url: `http://127.0.0.1:${port}/`,
377
+ wasFetched: () => fetched,
378
+ userAgent: () => agent,
379
+ close: () => {
380
+ server.close();
381
+ server.closeAllConnections();
382
+ }
383
+ });
384
+ });
385
+ });
386
+ }
387
+
388
+ // src/desktop/cli.ts
389
+ var assetsDirFor = (ownEntry) => join(dirname(ownEntry), "..", "launcher");
390
+ var runExecFile = (cmd, args, opts) => new Promise((resolve, reject) => {
391
+ execFile(
392
+ cmd,
393
+ args,
394
+ { env: { ...process.env, ...opts?.env ?? {} }, windowsHide: true, encoding: "utf8", timeout: 3e4 },
395
+ (err, stdout) => err ? reject(err) : resolve(stdout)
396
+ );
397
+ });
398
+ function installDeps(ownEntry) {
399
+ return {
400
+ platform: process.platform,
401
+ homedir: homedir(),
402
+ home: defaultHome(),
403
+ execPath: process.execPath,
404
+ env: process.env,
405
+ version: readMeta().version,
406
+ assetsDir: assetsDirFor(ownEntry),
407
+ runImpl: runExecFile
408
+ };
409
+ }
410
+ async function runDesktopCommand(opts, ownEntry) {
411
+ if (opts.remove) {
412
+ const deps = installDeps(ownEntry);
413
+ const res = await removeDesktop(deps);
414
+ console.log(res.removed ? ` Removed Scenri from your desktop (${res.path}).` : " Nothing to remove.");
415
+ if (res.message) console.log(` ${res.message}`);
416
+ await rememberDecline(deps.home);
417
+ return 0;
418
+ }
419
+ return (await addToDesktop(ownEntry)).ok ? 0 : 1;
420
+ }
421
+ async function addToDesktop(ownEntry, say = console.log, over = {}) {
422
+ const deps = { ...installDeps(ownEntry), ...over };
423
+ const pkg = over.pkg ?? readMeta().name;
424
+ const installKind = over.installKind ?? detectInstallKind(ownEntry, deps.home);
425
+ if (installKind === "dev") {
426
+ const message = "Running from a source checkout; there is no installed build to put on a desktop.";
427
+ say(` ${message}`);
428
+ return { ok: false, reason: "unsupported", message };
429
+ }
430
+ const adopted = await adoptRunningInstall({
431
+ home: deps.home,
432
+ pkg,
433
+ version: deps.version,
434
+ ownEntry,
435
+ installKind,
436
+ verifyImpl: over.verifyImpl
437
+ });
438
+ if (adopted.adopted) {
439
+ say(` keeping a copy of Scenri ${deps.version} in ${join(deps.home, "app")} so the icon works offline`);
440
+ }
441
+ if (!newestStaged(deps.home, pkg)) {
442
+ 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.";
443
+ say(` ${message}`);
444
+ return { ok: false, reason: "failed", message };
445
+ }
446
+ const res = await installDesktop(deps);
447
+ if (res.ok) {
448
+ say(` Added Scenri to your desktop: ${res.path}`);
449
+ say(" Next time, double-click it; no terminal needed.");
450
+ } else {
451
+ say(` ${res.message}`);
452
+ }
453
+ return res;
454
+ }
455
+ async function rememberDecline(home) {
456
+ try {
457
+ const { createCore } = await import('./src-CSR34EBZ.js');
458
+ const core = createCore(home);
459
+ try {
460
+ core.store.setSetting("desktop.prompt", "declined");
461
+ } finally {
462
+ core.close();
463
+ }
464
+ } catch {
465
+ }
466
+ }
467
+ async function runOpenCommand(ownEntry) {
468
+ const meta = readMeta();
469
+ const home = defaultHome();
470
+ const support = launcherDir(homedir());
471
+ const logs = logsDir(home);
472
+ const serverLogPath = join(logs, "scenri.log");
473
+ const platform = process.platform;
474
+ const previous = listStaged(home, meta.name).filter((v) => compareSemver(v, meta.version) < 0).reverse().map((v) => entryOf(home, meta.name, v));
475
+ return openScenri({
476
+ home,
477
+ pkg: meta.name,
478
+ version: meta.version,
479
+ ownEntry,
480
+ env: process.env,
481
+ execPath: process.execPath,
482
+ probe: async (url, timeoutMs) => {
483
+ try {
484
+ const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
485
+ return res.ok ? await res.json() : null;
486
+ } catch {
487
+ return null;
488
+ }
489
+ },
490
+ spawnImpl: spawn,
491
+ openBrowser: (url) => openInBrowser(url, platform, process.env),
492
+ showDialog: (message) => showDialog(platform, message, process.env),
493
+ log: (line) => appendLog(join(logs, "launcher.log"), line),
494
+ now: Date.now,
495
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
496
+ lockPath: join(support, "open.lock"),
497
+ pidAlive,
498
+ serverLogPath,
499
+ readLogTail: () => tail(serverLogPath),
500
+ startingTemplate: join(assetsDirFor(ownEntry), "starting.html"),
501
+ startingServer: serveStartingPage,
502
+ previousEntries: previous
503
+ });
504
+ }
505
+ function pidAlive(pid) {
506
+ try {
507
+ process.kill(pid, 0);
508
+ return true;
509
+ } catch (err) {
510
+ return err.code === "EPERM";
511
+ }
512
+ }
513
+ function tail(path, bytes = 4096) {
514
+ try {
515
+ const size = statSync(path).size;
516
+ const buf = readFileSync(path);
517
+ return buf.subarray(Math.max(0, size - bytes)).toString("utf8");
518
+ } catch {
519
+ return "";
520
+ }
521
+ }
522
+ var supportInstalled = () => existsSync(join(launcherDir(homedir()), "launcher.json"));
523
+
524
+ export { addToDesktop, assetsDirFor, installDeps, runDesktopCommand, runExecFile, runOpenCommand, supportInstalled };
525
+ //# sourceMappingURL=cli-FYWM4HBN.js.map
526
+ //# sourceMappingURL=cli-FYWM4HBN.js.map
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { bootErrorLines } from './chunk-WGIZJXNE.js';
2
+ import { bootErrorLines } from './chunk-CMLJGPYC.js';
3
3
  import { fileURLToPath } from 'url';
4
4
 
5
5
  // src/args.ts
@@ -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-FYWM4HBN.js');
90
+ process.exit(await runDesktopCommand(command, fileURLToPath(import.meta.url)));
91
+ break;
92
+ }
93
+ case "open": {
94
+ const { runOpenCommand } = await import('./cli-FYWM4HBN.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-DIZ4MQRF.js');
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();
@@ -0,0 +1,4 @@
1
+ export { UNSUPPORTED, desktopStatus, installDesktop, removeDesktop, runningNodeMajor, stableExecPath, writeSupportFiles } from './chunk-SDVL5OJF.js';
2
+ import './chunk-2IGINCPU.js';
3
+ //# sourceMappingURL=install-4W6AARK6.js.map
4
+ //# sourceMappingURL=install-4W6AARK6.js.map
@@ -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-DIZ4MQRF.js.map
65
- //# sourceMappingURL=launcher-DIZ4MQRF.js.map
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, PS_UTF8, desktopDir, isSupportedPlatform, launcherDir, launcherInstalled, launcherRecordPath, logsDir, powershellPath, readLauncherRecord, recordedEnv, writeLauncherRecord } from './chunk-2IGINCPU.js';
2
+ //# sourceMappingURL=paths-6ZXRZUHY.js.map
3
+ //# sourceMappingURL=paths-6ZXRZUHY.js.map