scenri 0.9.0 → 0.9.2

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 CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.2](https://github.com/tonygorb/Scenri/compare/v0.9.1...v0.9.2) (2026-09-07)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **codex:** name the model the installed cli is too old for ([91868da](https://github.com/tonygorb/Scenri/commit/91868daf6ec2d8f3d2c6e636923683d7650aafbf))
9
+ * **codex:** name the model the installed cli is too old for ([66b6b81](https://github.com/tonygorb/Scenri/commit/66b6b819303c4ed99fc40e6137bf00725b4d9673))
10
+
11
+ ## [0.9.1](https://github.com/tonygorb/Scenri/compare/v0.9.0...v0.9.1) (2026-09-07)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **cli:** the desktop icon on Windows ends in the studio or a sentence, never in silence ([4a5dacb](https://github.com/tonygorb/Scenri/commit/4a5dacbdc1522fa04e506c92354182d0c3f08309))
17
+ * **cli:** the desktop icon on Windows ends in the studio or a sentence, never in silence ([86bdc1e](https://github.com/tonygorb/Scenri/commit/86bdc1e24728d3a706e690c13c08b0adb27714ec))
18
+ * **cli:** the dialog PowerShell keeps windowsHide, because a detached one shows nothing on Windows ([f140c04](https://github.com/tonygorb/Scenri/commit/f140c04b47008359ea4f2e194a08435b9321842a))
19
+
3
20
  ## [0.9.0](https://github.com/tonygorb/Scenri/compare/v0.8.3...v0.9.0) (2026-09-06)
4
21
 
5
22
 
@@ -1,5 +1,5 @@
1
- import { readFileSync, mkdirSync, writeFileSync, renameSync, existsSync } from 'fs';
2
- import { join } from 'path';
1
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, renameSync } from 'fs';
2
+ import { join, win32 } from 'path';
3
3
 
4
4
  // src/desktop/paths.ts
5
5
  var LAUNCHER_SCHEMA = 1;
@@ -32,15 +32,21 @@ function recordedEnv(env) {
32
32
  return out;
33
33
  }
34
34
  var KNOWN_FOLDER_DESKTOP = "[Environment]::GetFolderPath('Desktop')";
35
+ var PS_UTF8 = "[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; ";
36
+ function powershellPath(env, exists = existsSync) {
37
+ const root = env.SystemRoot || env.SYSTEMROOT || env.windir || "C:\\Windows";
38
+ const absolute = win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
39
+ return exists(absolute) ? absolute : "powershell.exe";
40
+ }
35
41
  async function desktopDir(deps) {
36
42
  if (deps.env.SCENRI_DESKTOP_DIR) return deps.env.SCENRI_DESKTOP_DIR;
37
43
  if (deps.platform === "darwin") return join(deps.homedir, "Desktop");
38
44
  if (deps.platform === "win32") {
39
- const out = await deps.runImpl("powershell.exe", [
45
+ const out = await deps.runImpl(powershellPath(deps.env), [
40
46
  "-NoProfile",
41
47
  "-NonInteractive",
42
48
  "-Command",
43
- KNOWN_FOLDER_DESKTOP
49
+ PS_UTF8 + KNOWN_FOLDER_DESKTOP
44
50
  ]);
45
51
  const dir = out.trim();
46
52
  return dir || null;
@@ -50,6 +56,6 @@ async function desktopDir(deps) {
50
56
  var isSupportedPlatform = (platform) => platform === "darwin" || platform === "win32";
51
57
  var launcherInstalled = (homedir) => existsSync(launcherRecordPath(homedir));
52
58
 
53
- export { LAUNCHER_SCHEMA, desktopDir, isSupportedPlatform, launcherDir, launcherInstalled, launcherRecordPath, logsDir, readLauncherRecord, recordedEnv, writeLauncherRecord };
54
- //# sourceMappingURL=chunk-4NQKRVO2.js.map
55
- //# sourceMappingURL=chunk-4NQKRVO2.js.map
59
+ export { LAUNCHER_SCHEMA, PS_UTF8, desktopDir, isSupportedPlatform, launcherDir, launcherInstalled, launcherRecordPath, logsDir, powershellPath, readLauncherRecord, recordedEnv, writeLauncherRecord };
60
+ //# sourceMappingURL=chunk-2IGINCPU.js.map
61
+ //# sourceMappingURL=chunk-2IGINCPU.js.map
@@ -1,3 +1,6 @@
1
+ import { realpathSync } from 'fs';
2
+ import { resolve } from 'path';
3
+
1
4
  // src/bootError.ts
2
5
  var INSTALL_GUIDE = "https://github.com/tonygorb/scenri/blob/main/docs/INSTALL.md";
3
6
  var NATIVE_MARKERS = ["NODE_MODULE_VERSION", "Could not locate the bindings file", "ERR_DLOPEN_FAILED"];
@@ -30,7 +33,26 @@ function bootErrorLines(err) {
30
33
  "Set SCENRI_DEBUG=1 to see the full error."
31
34
  ];
32
35
  }
36
+ function shouldAdoptRunning(input) {
37
+ if (input.installKind !== "dev" || !input.theirHome) return true;
38
+ return realDir(input.theirHome) === realDir(input.ourHome);
39
+ }
40
+ function realDir(p) {
41
+ try {
42
+ return realpathSync(p);
43
+ } catch {
44
+ return resolve(p);
45
+ }
46
+ }
47
+ function anotherScenriLines(port, theirs, ours) {
48
+ return [
49
+ `Port ${port} is held by another Scenri, serving ${theirs}.`,
50
+ `This checkout would serve ${ours}, so it did not start.`,
51
+ "Stop that server, or start this checkout on a different port:",
52
+ ` SCENRI_PORT=${port + 1} pnpm dev`
53
+ ];
54
+ }
33
55
 
34
- export { bootErrorLines, portBusyLines };
35
- //# sourceMappingURL=chunk-WGIZJXNE.js.map
36
- //# sourceMappingURL=chunk-WGIZJXNE.js.map
56
+ export { anotherScenriLines, bootErrorLines, portBusyLines, shouldAdoptRunning };
57
+ //# sourceMappingURL=chunk-CMLJGPYC.js.map
58
+ //# sourceMappingURL=chunk-CMLJGPYC.js.map
@@ -6,7 +6,7 @@ import { dirname, join } from 'path';
6
6
  async function adoptRunningInstall(deps) {
7
7
  if (deps.installKind === "dev") return { adopted: false, reason: "dev" };
8
8
  if (deps.installKind === "managed") return { adopted: false, reason: "managed" };
9
- if (isValidVersionDir(deps.home, deps.pkg, deps.version)) return { adopted: false, reason: "present" };
9
+ if (!deps.force && isValidVersionDir(deps.home, deps.pkg, deps.version)) return { adopted: false, reason: "present" };
10
10
  const pkgRoot = dirname(dirname(deps.ownEntry));
11
11
  if (!existsSync(deps.ownEntry) || manifestVersion(pkgRoot) !== deps.version) {
12
12
  return { adopted: false, reason: "no-source" };
@@ -23,8 +23,15 @@ async function adoptRunningInstall(deps) {
23
23
  if (!await (deps.verifyImpl ?? verifyEntry)(copied)) return { adopted: false, reason: "no-source" };
24
24
  const target = join(versionsDir(deps.home), deps.version);
25
25
  mkdirSync(dirname(target), { recursive: true });
26
- rmSync(target, { recursive: true, force: true });
27
- renameSync(work, target);
26
+ const aside = join(stagingDir(deps.home), `replaced-${deps.version}`);
27
+ rmSync(aside, { recursive: true, force: true });
28
+ try {
29
+ if (existsSync(target)) renameSync(target, aside);
30
+ renameSync(work, target);
31
+ } catch {
32
+ return { adopted: false, reason: "busy" };
33
+ }
34
+ rmSync(aside, { recursive: true, force: true });
28
35
  } finally {
29
36
  rmSync(work, { recursive: true, force: true });
30
37
  }
@@ -58,5 +65,5 @@ function manifestVersion(pkgRoot) {
58
65
  }
59
66
 
60
67
  export { adoptRunningInstall };
61
- //# sourceMappingURL=chunk-QDODTMSN.js.map
62
- //# sourceMappingURL=chunk-QDODTMSN.js.map
68
+ //# sourceMappingURL=chunk-ECS5LRHB.js.map
69
+ //# sourceMappingURL=chunk-ECS5LRHB.js.map
@@ -1,5 +1,5 @@
1
- import { isSupportedPlatform, desktopDir, launcherDir, LAUNCHER_SCHEMA, writeLauncherRecord, recordedEnv, readLauncherRecord } from './chunk-4NQKRVO2.js';
2
- import { existsSync, rmSync, mkdirSync, writeFileSync, chmodSync, copyFileSync, renameSync, utimesSync } from 'fs';
1
+ import { powershellPath, isSupportedPlatform, desktopDir, launcherDir, LAUNCHER_SCHEMA, writeLauncherRecord, recordedEnv, readLauncherRecord, PS_UTF8 } from './chunk-2IGINCPU.js';
2
+ import { realpathSync, existsSync, rmSync, mkdirSync, writeFileSync, chmodSync, copyFileSync, renameSync, utimesSync } from 'fs';
3
3
  import { join, dirname } from 'path';
4
4
 
5
5
  var MAC_BUNDLE_ID = "co.scenri.desktop-launcher";
@@ -114,10 +114,10 @@ function writeMacBundle(opts) {
114
114
  }
115
115
 
116
116
  // src/desktop/windows.ts
117
- var POWERSHELL = "powershell.exe";
117
+ var POWERSHELL = powershellPath(process.env);
118
118
  var POWERSHELL_ARGS = ["-NoProfile", "-NonInteractive", "-Command"];
119
119
  var CREATE_LNK_COMMAND = "$s = New-Object -ComObject WScript.Shell; $l = $s.CreateShortcut($env:SCENRI_LNK); $l.TargetPath = $env:SCENRI_TARGET; $l.Arguments = $env:SCENRI_ARGS; $l.WorkingDirectory = $env:SCENRI_WORKDIR; $l.IconLocation = $env:SCENRI_ICON; $l.Description = 'Open Scenri'; $l.WindowStyle = 7; $l.Save()";
120
- var READ_LNK_COMMAND = "$s = New-Object -ComObject WScript.Shell; $l = $s.CreateShortcut($env:SCENRI_LNK); Write-Output $l.TargetPath; Write-Output $l.Arguments";
120
+ var READ_LNK_COMMAND = `${PS_UTF8}$s = New-Object -ComObject WScript.Shell; $l = $s.CreateShortcut($env:SCENRI_LNK); Write-Output $l.TargetPath; Write-Output $l.Arguments`;
121
121
  async function readLnk(runImpl, path) {
122
122
  const out = await runImpl(POWERSHELL, [...POWERSHELL_ARGS, READ_LNK_COMMAND], { env: { SCENRI_LNK: path } });
123
123
  const [target = "", args = ""] = out.split(/\r?\n/);
@@ -163,7 +163,8 @@ async function installDesktop(deps) {
163
163
  if (!ours) return { ok: false, reason: "collision", message: COLLISION };
164
164
  }
165
165
  const nodeMajor = runningNodeMajor();
166
- writeSupportFiles({ ...deps, nodeMajor }, support);
166
+ const nodePath = stableExecPath(deps.execPath, deps.platform);
167
+ writeSupportFiles({ ...deps, execPath: nodePath, nodeMajor }, support);
167
168
  try {
168
169
  if (darwin) {
169
170
  writeMacBundle({
@@ -175,7 +176,7 @@ async function installDesktop(deps) {
175
176
  } else {
176
177
  await writeLnk(deps.runImpl, {
177
178
  path: artifact,
178
- target: deps.execPath,
179
+ target: nodePath,
179
180
  args: `"${bootstrap}"`,
180
181
  workdir: support,
181
182
  icon: `${join(support, "scenri.ico")},0`
@@ -192,7 +193,7 @@ async function installDesktop(deps) {
192
193
  schema: LAUNCHER_SCHEMA,
193
194
  createdBy: deps.version,
194
195
  home: deps.home,
195
- nodePath: deps.execPath,
196
+ nodePath,
196
197
  nodeMajor,
197
198
  env: recordedEnv(deps.env),
198
199
  artifact: { kind, path: artifact }
@@ -200,6 +201,14 @@ async function installDesktop(deps) {
200
201
  return { ok: true, kind, path: artifact };
201
202
  }
202
203
  var runningNodeMajor = () => Number(process.versions.node.split(".")[0]);
204
+ function stableExecPath(execPath, platform, realpath = realpathSync) {
205
+ if (platform !== "win32") return execPath;
206
+ try {
207
+ return realpath(execPath);
208
+ } catch {
209
+ return execPath;
210
+ }
211
+ }
203
212
  function writeSupportFiles(deps, support) {
204
213
  mkdirSync(support, { recursive: true });
205
214
  for (const f of SUPPORT_FILES) copyFileSync(join(deps.assetsDir, f), join(support, f));
@@ -241,6 +250,6 @@ async function desktopStatus(deps) {
241
250
  return { supported, platform: deps.platform, installed, path, current, record };
242
251
  }
243
252
 
244
- export { MAC_SCRIPT, POWERSHELL, POWERSHELL_ARGS, UNSUPPORTED, desktopStatus, installDesktop, isOurMacBundle, macPlist, removeDesktop, runningNodeMajor, writeLnk, writeMacBundle, writeSupportFiles };
245
- //# sourceMappingURL=chunk-HLMR33DT.js.map
246
- //# sourceMappingURL=chunk-HLMR33DT.js.map
253
+ export { MAC_SCRIPT, POWERSHELL_ARGS, UNSUPPORTED, desktopStatus, installDesktop, isOurMacBundle, macPlist, removeDesktop, runningNodeMajor, stableExecPath, writeLnk, writeMacBundle, writeSupportFiles };
254
+ //# sourceMappingURL=chunk-SDVL5OJF.js.map
255
+ //# sourceMappingURL=chunk-SDVL5OJF.js.map
@@ -1,39 +1,107 @@
1
- import { adoptRunningInstall } from './chunk-QDODTMSN.js';
1
+ import { adoptRunningInstall } from './chunk-ECS5LRHB.js';
2
2
  import { detectInstallKind } from './chunk-PAIAXRAC.js';
3
3
  import { readMeta } from './chunk-Y3ZPBPLP.js';
4
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';
5
+ import { removeDesktop, installDesktop, POWERSHELL_ARGS } from './chunk-SDVL5OJF.js';
6
+ import { launcherDir, logsDir, powershellPath } from './chunk-2IGINCPU.js';
7
7
  import { execFile, spawn } from 'child_process';
8
- import { existsSync, closeSync, mkdirSync, writeFileSync, readFileSync, openSync, rmSync, statSync, renameSync, appendFileSync } from 'fs';
8
+ import { existsSync, closeSync, readFileSync, writeFileSync, mkdirSync, openSync, rmSync, statSync, renameSync, appendFileSync } from 'fs';
9
9
  import { homedir } from 'os';
10
- import { join, dirname, delimiter } from 'path';
11
- import { pathToFileURL } from 'url';
10
+ import { join, dirname, delimiter, win32 } from 'path';
11
+ import { createServer } from 'http';
12
12
 
13
- async function openInBrowser(url, platform, env) {
13
+ var HELPER_TIMEOUT_MS = 1e4;
14
+ async function openInBrowser(url, platform, env, impl = {}) {
15
+ const timeoutMs = impl.timeoutMs ?? HELPER_TIMEOUT_MS;
14
16
  if (platform === "win32") {
15
- await new Promise((resolve) => {
16
- const child = spawn(POWERSHELL, [...POWERSHELL_ARGS, "Start-Process $env:SCENRI_URL"], {
17
- stdio: "ignore",
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"],
18
75
  windowsHide: true,
19
- detached: true,
20
- env: { ...env, SCENRI_URL: url }
76
+ env: helper.env ?? env
21
77
  });
22
- child.once("error", () => resolve());
23
- child.once("spawn", () => resolve());
24
- child.unref();
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);
25
90
  });
26
- return;
27
- }
28
- const { default: open } = await import('open');
29
- await open(url);
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
+ });
30
98
  }
31
99
  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) {
100
+ function showDialog(platform, message, env, spawnImpl = spawn) {
33
101
  if (env.SCENRI_NO_DIALOG === "1") return Promise.resolve();
34
102
  let cmd;
35
103
  let args;
36
- let childEnv;
104
+ let opts;
37
105
  if (platform === "darwin") {
38
106
  cmd = "/usr/bin/osascript";
39
107
  args = [
@@ -46,10 +114,11 @@ function showDialog(platform, message, env) {
46
114
  "--",
47
115
  message
48
116
  ];
117
+ opts = { stdio: "ignore" };
49
118
  } else if (platform === "win32") {
50
- cmd = POWERSHELL;
119
+ cmd = powershellPath(env);
51
120
  args = [...POWERSHELL_ARGS, MESSAGE_BOX];
52
- childEnv = { ...env, SCENRI_MESSAGE: message };
121
+ opts = { stdio: "ignore", windowsHide: true, env: { ...env, SCENRI_MESSAGE: message } };
53
122
  } else {
54
123
  console.error(`
55
124
  ${message}
@@ -58,7 +127,7 @@ function showDialog(platform, message, env) {
58
127
  }
59
128
  return new Promise((resolve) => {
60
129
  try {
61
- const child = spawn(cmd, args, { stdio: "ignore", windowsHide: true, env: childEnv });
130
+ const child = spawnImpl(cmd, args, opts);
62
131
  child.once("error", () => resolve());
63
132
  child.once("exit", () => resolve());
64
133
  } catch {
@@ -88,6 +157,8 @@ function openLogFd(path, opts = {}) {
88
157
  return openSync(path, "a");
89
158
  }
90
159
  var LOCK_FRESH_MS = 6e4;
160
+ var READY_TIMEOUT_MS = 18e4;
161
+ var PAGE_FETCH_GRACE_MS = 15e3;
91
162
  var QUICK_DEATH_MS = 1e4;
92
163
  var NATIVE_MARKERS = ["NODE_MODULE_VERSION", "Could not locate the bindings file", "ERR_DLOPEN_FAILED"];
93
164
  async function showBrowser(deps, url) {
@@ -104,8 +175,17 @@ async function openScenri(deps) {
104
175
  const port = Number(deps.env.SCENRI_PORT || 4747);
105
176
  const url = `http://127.0.0.1:${port}/`;
106
177
  const noOpen = deps.env.SCENRI_NO_OPEN === "1";
107
- deps.log(`open: invoked by ${deps.version} for port ${port}`);
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;
108
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
+ }
109
189
  deps.log("open: another launch is in progress, stepping aside");
110
190
  return 0;
111
191
  }
@@ -117,46 +197,57 @@ async function openScenri(deps) {
117
197
  return 0;
118
198
  }
119
199
  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
200
  const started = deps.now();
137
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
+ });
138
229
  child.on("exit", (code) => {
139
230
  exit = { code };
140
231
  });
141
232
  deps.log(`open: started the supervisor, pid ${child.pid ?? "unknown"}, log ${deps.serverLogPath}`);
142
233
  let shown = false;
143
- if (!noOpen && deps.startingTemplate && existsSync(deps.startingTemplate)) {
234
+ if (!noOpen && deps.startingTemplate && existsSync(deps.startingTemplate) && deps.startingServer) {
144
235
  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);
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);
148
239
  shown = true;
149
240
  deps.log("open: showing the starting page");
150
241
  } catch (err) {
151
242
  deps.log(`open: could not show the starting page (${err instanceof Error ? err.message : String(err)})`);
152
243
  }
153
244
  }
154
- const deadline = started + (deps.readyTimeoutMs ?? 9e4);
245
+ const deadline = started + (deps.readyTimeoutMs ?? READY_TIMEOUT_MS);
155
246
  for (; ; ) {
156
247
  const info = await deps.probe(`${url}api/version`, 1e3);
157
248
  if (info?.name === deps.pkg) {
158
249
  deps.log(`open: ready in ${deps.now() - started}ms`);
159
- if (!noOpen && !shown) await showBrowser(deps, url);
250
+ if (!noOpen) await handOver(deps, url, shown ? page : null);
160
251
  return 0;
161
252
  }
162
253
  if (exit) break;
@@ -168,6 +259,7 @@ async function openScenri(deps) {
168
259
  }
169
260
  await deps.sleep(deps.pollMs ?? 250);
170
261
  }
262
+ if (spawnError) return await couldNotStart(deps, spawnError);
171
263
  const lived = deps.now() - started;
172
264
  deps.log(`open: the server exited with ${exit.code} after ${lived}ms`);
173
265
  const previous = deps.previousEntries[0];
@@ -176,6 +268,9 @@ async function openScenri(deps) {
176
268
  releaseLock(deps.lockPath);
177
269
  const fallback = deps.spawnImpl(deps.execPath, [previous, "open"], {
178
270
  stdio: "inherit",
271
+ // This process has no console; without the hint Windows would give
272
+ // the fallback a visible one.
273
+ windowsHide: true,
179
274
  env: {
180
275
  ...deps.env,
181
276
  SCENRI_DESKTOP_FALLBACK: deps.version,
@@ -183,16 +278,34 @@ async function openScenri(deps) {
183
278
  ...shown ? { SCENRI_NO_OPEN: "1" } : {}
184
279
  }
185
280
  });
186
- return await new Promise((resolve) => fallback.on("exit", (code) => resolve(code ?? 1)));
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
+ });
187
288
  }
188
289
  const message = explain(deps.readLogTail(), port);
189
290
  deps.log(`open: dialog: ${message}`);
190
291
  await deps.showDialog(message);
191
292
  return 1;
192
293
  } finally {
294
+ page?.close();
193
295
  releaseLock(deps.lockPath);
194
296
  }
195
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
+ }
196
309
  var STUDIO_URL = /^http:\/\/127\.0\.0\.1:\d{2,5}\/$/;
197
310
  var STUDIO_META = '<meta name="scenri-studio" content="">';
198
311
  function renderStartingPage(template, url) {
@@ -209,11 +322,16 @@ function explain(tail2, port) {
209
322
  if (tail2.includes("a native component failed to load") || NATIVE_MARKERS.some((m) => tail2.includes(m))) {
210
323
  return "Node.js changed since Scenri was installed. Open Terminal and run: npx scenri@latest";
211
324
  }
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
325
  return "Scenri could not start. Open Terminal and run npx scenri to see why.";
216
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
+ }
217
335
  function takeLock(deps) {
218
336
  try {
219
337
  const held = JSON.parse(readFileSync(deps.lockPath, "utf8"));
@@ -234,6 +352,38 @@ function releaseLock(path) {
234
352
  } catch {
235
353
  }
236
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
+ }
237
387
 
238
388
  // src/desktop/cli.ts
239
389
  var assetsDirFor = (ownEntry) => join(dirname(ownEntry), "..", "launcher");
@@ -348,7 +498,7 @@ async function runOpenCommand(ownEntry) {
348
498
  serverLogPath,
349
499
  readLogTail: () => tail(serverLogPath),
350
500
  startingTemplate: join(assetsDirFor(ownEntry), "starting.html"),
351
- startingPage: join(support, "starting.html"),
501
+ startingServer: serveStartingPage,
352
502
  previousEntries: previous
353
503
  });
354
504
  }
@@ -372,5 +522,5 @@ function tail(path, bytes = 4096) {
372
522
  var supportInstalled = () => existsSync(join(launcherDir(homedir()), "launcher.json"));
373
523
 
374
524
  export { addToDesktop, assetsDirFor, installDeps, runDesktopCommand, runExecFile, runOpenCommand, supportInstalled };
375
- //# sourceMappingURL=cli-BVPVS4V5.js.map
376
- //# sourceMappingURL=cli-BVPVS4V5.js.map
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
@@ -86,12 +86,12 @@ try {
86
86
  break;
87
87
  }
88
88
  case "desktop": {
89
- const { runDesktopCommand } = await import('./cli-BVPVS4V5.js');
89
+ const { runDesktopCommand } = await import('./cli-FYWM4HBN.js');
90
90
  process.exit(await runDesktopCommand(command, fileURLToPath(import.meta.url)));
91
91
  break;
92
92
  }
93
93
  case "open": {
94
- const { runOpenCommand } = await import('./cli-BVPVS4V5.js');
94
+ const { runOpenCommand } = await import('./cli-FYWM4HBN.js');
95
95
  process.exit(await runOpenCommand(fileURLToPath(import.meta.url)));
96
96
  break;
97
97
  }
@@ -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
@@ -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