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.
@@ -1,37 +1,48 @@
1
- import { adoptRunningInstall } from './chunk-QDODTMSN.js';
1
+ import { adoptRunningInstall } from './chunk-ECS5LRHB.js';
2
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';
3
+ import { runningNodeMajor, stableExecPath, isOurMacBundle, MAC_SCRIPT, macPlist, writeSupportFiles, writeMacBundle, writeLnk } from './chunk-SDVL5OJF.js';
4
+ import { readLauncherRecord, launcherDir, LAUNCHER_SCHEMA, writeLauncherRecord } from './chunk-2IGINCPU.js';
5
5
  import { existsSync, readFileSync } from 'fs';
6
6
  import { join } from 'path';
7
7
 
8
8
  var ASSETS = ["launch.mjs", "Scenri.icns", "scenri.ico"];
9
9
  async function refreshLauncher(deps) {
10
10
  if (deps.env.SCENRI_NO_DESKTOP === "1") return {};
11
+ if (deps.installKind === "dev") return {};
11
12
  const record = readLauncherRecord(deps.homedir);
12
13
  if (!record) return {};
13
14
  const out = {};
15
+ const running = runningNodeMajor();
16
+ const ownNode = stableExecPath(deps.execPath, deps.platform);
17
+ const nodeGone = !existsSync(record.nodePath);
18
+ const majorChanged = (nodeGone || samePath(record.nodePath, ownNode, deps.platform) || samePath(record.nodePath, deps.execPath, deps.platform)) && record.nodeMajor !== void 0 && record.nodeMajor !== running;
19
+ let rebuilt = false;
14
20
  if (deps.installKind === "npx" || deps.installKind === "global") {
15
21
  const newest = newestStaged(deps.home, deps.pkg);
16
- if (!newest || compareSemver(deps.version, newest) > 0) {
22
+ const newer = !newest || compareSemver(deps.version, newest) > 0;
23
+ const rebuild = majorChanged && newest !== null && compareSemver(deps.version, newest) >= 0;
24
+ if (newer || rebuild) {
17
25
  const r = await adoptRunningInstall({
18
26
  home: deps.home,
19
27
  pkg: deps.pkg,
20
28
  version: deps.version,
21
29
  ownEntry: deps.ownEntry,
22
30
  installKind: deps.installKind,
23
- verifyImpl: deps.verifyImpl
31
+ verifyImpl: deps.verifyImpl,
32
+ force: rebuild
24
33
  });
25
- if (r.adopted) out.adopted = true;
34
+ if (r.adopted) {
35
+ out.adopted = true;
36
+ rebuilt = rebuild;
37
+ }
26
38
  }
27
39
  }
28
40
  const support = launcherDir(deps.homedir);
29
41
  const artifact = record.artifact.path;
30
42
  const ours = existsSync(artifact) && (record.artifact.kind === "macos-app" ? isOurMacBundle(artifact) : true);
31
43
  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();
44
+ const nodePath = nodeGone ? ownNode : record.nodePath;
45
+ const nodeMajor = nodeGone || rebuilt ? running : record.nodeMajor ?? running;
35
46
  const nodeFilesStale = deps.platform === "darwin" && (readText(join(support, "node-path")) !== `${nodePath}
36
47
  ` || readText(join(support, "node-major")) !== `${nodeMajor}
37
48
  `);
@@ -59,7 +70,7 @@ async function refreshLauncher(deps) {
59
70
  icon: `${join(support, "scenri.ico")},0`
60
71
  });
61
72
  }
62
- if (schemaStale || supportStale || nodeGone || iconStale || bundleStale) {
73
+ if (schemaStale || supportStale || nodeGone || iconStale || bundleStale || rebuilt) {
63
74
  writeLauncherRecord(deps.homedir, {
64
75
  ...record,
65
76
  schema: LAUNCHER_SCHEMA,
@@ -71,6 +82,9 @@ async function refreshLauncher(deps) {
71
82
  }
72
83
  return out;
73
84
  }
85
+ function samePath(a, b, platform) {
86
+ return platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
87
+ }
74
88
  function readText(path) {
75
89
  try {
76
90
  return readFileSync(path, "utf8");
@@ -87,5 +101,5 @@ function sameFile(a, b) {
87
101
  }
88
102
 
89
103
  export { refreshLauncher };
90
- //# sourceMappingURL=refresh-7HXPUHLQ.js.map
91
- //# sourceMappingURL=refresh-7HXPUHLQ.js.map
104
+ //# sourceMappingURL=refresh-FKQABJOC.js.map
105
+ //# sourceMappingURL=refresh-FKQABJOC.js.map
package/dist/serve.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createUpdateChecker, classify, isReleaseTriplet, findNpm, stageVersion } from './chunk-FYQ5BAFA.js';
2
- import { portBusyLines } from './chunk-WGIZJXNE.js';
2
+ import { shouldAdoptRunning, anotherScenriLines, portBusyLines } from './chunk-CMLJGPYC.js';
3
3
  import { SchemaTooNewError, createCore, BUDGET_EXHAUSTED, SpendCapError, budgetSize, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, ratioLabel, searchTerms, termMatches, SCHEMA_VERSION, ASPECT_TOLERANCE } from './chunk-OJAG3FRX.js';
4
4
  import { detectInstallKind } from './chunk-PAIAXRAC.js';
5
5
  export { detectInstallKind } from './chunk-PAIAXRAC.js';
@@ -740,6 +740,10 @@ function codexFailureDetail(stderr, stdout) {
740
740
  const marked = body.startsWith("ERROR:") ? body : errorAt >= 0 ? body.slice(errorAt + 1) : "";
741
741
  return tailOf(marked) || tailOf(body) || tailOf(stdout.trim()) || tailOf(stderr.trim());
742
742
  }
743
+ var MODEL_NEEDS_NEWER_CODEX = /The '([^']+)' model requires a newer version of Codex/;
744
+ function tooOldForModel(version, model) {
745
+ return `Codex CLI ${version ?? "on this computer"} is too old for the model it is set to, ${model}.`;
746
+ }
743
747
  function killTree(child, platform, spawnImpl) {
744
748
  if (platform === "win32" && child.pid) {
745
749
  try {
@@ -783,6 +787,8 @@ function createRunner(opts = {}) {
783
787
  resolved ??= await resolveCodex(platform, spawnImpl);
784
788
  return resolved;
785
789
  }
790
+ let knownVersion = null;
791
+ let tooOldFor = null;
786
792
  const winArg = (a) => `"${a.replace(/[\r\n]+/g, " ").replace(/"/g, "'").replace(/%/g, " percent ")}"`;
787
793
  const spawnCodex = (exe, args, stdinOpen) => {
788
794
  const stdio = [stdinOpen ? "pipe" : "ignore", "pipe", "pipe"];
@@ -890,6 +896,17 @@ function createRunner(opts = {}) {
890
896
  );
891
897
  return;
892
898
  }
899
+ const newer = MODEL_NEEDS_NEWER_CODEX.exec(stderr);
900
+ if (newer) {
901
+ const model = newer[1];
902
+ tooOldFor = { version: knownVersion, model };
903
+ invalidateProbe();
904
+ finish(
905
+ `exit-${code ?? "unknown"}`,
906
+ () => reject(new Error(`${tooOldForModel(knownVersion, model)} Update Codex CLI, then run this again.`))
907
+ );
908
+ return;
909
+ }
893
910
  const snippet2 = codexFailureDetail(stderr, stdout);
894
911
  finish(
895
912
  `exit-${code ?? "unknown"}`,
@@ -971,6 +988,7 @@ function createRunner(opts = {}) {
971
988
  return verdict({ ok: false, reason: NOT_INSTALLED_REASON, code: "not-installed" }, exe, null);
972
989
  }
973
990
  const version = parseCodexVersion(ver.stdout);
991
+ knownVersion = version;
974
992
  if (version && !versionAtLeast(version, MIN_CODEX_VERSION)) {
975
993
  return verdict(
976
994
  {
@@ -982,6 +1000,16 @@ function createRunner(opts = {}) {
982
1000
  version
983
1001
  );
984
1002
  }
1003
+ if (tooOldFor) {
1004
+ if (version === tooOldFor.version) {
1005
+ return verdict(
1006
+ { ok: false, reason: tooOldForModel(version, tooOldFor.model), code: "update-needed" },
1007
+ exe,
1008
+ version
1009
+ );
1010
+ }
1011
+ tooOldFor = null;
1012
+ }
985
1013
  const login = await probeSpawn(exe, ["login", "status"]);
986
1014
  if (login.outcome === "ok") {
987
1015
  return verdict({ ok: true }, exe, version);
@@ -1495,7 +1523,7 @@ function createCodexEngine(opts) {
1495
1523
  }
1496
1524
  };
1497
1525
  function isFatalSetupError(err) {
1498
- return /failed to spawn|ENOENT|not logged in|login required|401|unauthorized/i.test(
1526
+ return /failed to spawn|ENOENT|not logged in|login required|401|unauthorized|is too old/i.test(
1499
1527
  String(err?.message ?? err)
1500
1528
  );
1501
1529
  }
@@ -7789,6 +7817,26 @@ function registerImageRoutes(app, deps) {
7789
7817
 
7790
7818
  // src/release/notes.data.ts
7791
7819
  var RELEASES = [
7820
+ {
7821
+ version: "0.9.2",
7822
+ date: "2026-09-07",
7823
+ sections: [
7824
+ {
7825
+ heading: "Fixes",
7826
+ body: "If the Codex app has switched to a model your Codex CLI is too old for, a shot now stops with a plain sentence naming the version and the model, and the setup dialog opens on the update step rather than a green check. Update the CLI once and generation resumes."
7827
+ }
7828
+ ]
7829
+ },
7830
+ {
7831
+ version: "0.9.1",
7832
+ date: "2026-09-07",
7833
+ sections: [
7834
+ {
7835
+ heading: "Desktop",
7836
+ body: "The desktop icon on Windows now ends in the studio or in one plain sentence, never in silence. On both systems the Starting page and the studio open in your default browser, a start that fails says why, and after a Node.js upgrade the next terminal start rebuilds the copy the icon runs."
7837
+ }
7838
+ ]
7839
+ },
7792
7840
  {
7793
7841
  version: "0.9.0",
7794
7842
  date: "2026-09-06",
@@ -8739,15 +8787,15 @@ function registerDesktopRoutes(app, deps) {
8739
8787
  record: null
8740
8788
  };
8741
8789
  }
8742
- const { installDeps } = await import('./cli-BVPVS4V5.js');
8743
- const { desktopStatus } = await import('./install-V6DAA7TT.js');
8790
+ const { installDeps } = await import('./cli-FYWM4HBN.js');
8791
+ const { desktopStatus } = await import('./install-4W6AARK6.js');
8744
8792
  return desktopStatus(installDeps(runtime.entry));
8745
8793
  });
8746
8794
  const install = deps.installImpl ?? (async () => {
8747
8795
  if (!runtime.entry) {
8748
8796
  return { ok: false, reason: "unsupported", message: "Desktop shortcuts are not available on this system yet." };
8749
8797
  }
8750
- const { addToDesktop } = await import('./cli-BVPVS4V5.js');
8798
+ const { addToDesktop } = await import('./cli-FYWM4HBN.js');
8751
8799
  return addToDesktop(runtime.entry);
8752
8800
  });
8753
8801
  app.get("/api/desktop", async () => {
@@ -10260,6 +10308,14 @@ async function run() {
10260
10308
  });
10261
10309
  const info = await res.json();
10262
10310
  if (info.name === readMeta().name) {
10311
+ if (!shouldAdoptRunning({ installKind, ourHome: core.home, theirHome: info.home })) {
10312
+ console.error("");
10313
+ for (const line of anotherScenriLines(PORT, info.home ?? "another library", core.home)) {
10314
+ console.error(` ${line}`);
10315
+ }
10316
+ console.error("");
10317
+ process.exit(1);
10318
+ }
10263
10319
  const url = `http://127.0.0.1:${PORT}`;
10264
10320
  console.log(`
10265
10321
  Scenri is already running \u2192 ${url}
@@ -10332,10 +10388,10 @@ async function run() {
10332
10388
  }
10333
10389
  }
10334
10390
  const ownEntry = fileURLToPath(import.meta.url);
10335
- const { addToDesktop, installDeps } = await import('./cli-BVPVS4V5.js');
10336
- const { refreshLauncher } = await import('./refresh-7HXPUHLQ.js');
10391
+ const { addToDesktop, installDeps } = await import('./cli-FYWM4HBN.js');
10392
+ const { refreshLauncher } = await import('./refresh-FKQABJOC.js');
10337
10393
  const { askOnTerminal, offerDesktop, shouldOfferDesktop } = await import('./offer-IVTNHIDJ.js');
10338
- const { launcherInstalled } = await import('./paths-36Y73GGY.js');
10394
+ const { launcherInstalled } = await import('./paths-6ZXRZUHY.js');
10339
10395
  const meta = readMeta();
10340
10396
  void refreshLauncher({ ...installDeps(ownEntry), ownEntry, installKind, pkg: meta.name }).then((r) => {
10341
10397
  if (r.adopted)
@@ -22,7 +22,9 @@ const record = readJson(join(here, 'launcher.json'));
22
22
  const home = typeof record?.home === 'string' && record.home ? record.home : join(homedir(), '.scenri');
23
23
  const logPath = join(home, 'logs', 'launcher.log');
24
24
 
25
- log(`bootstrap v${SCHEMA}: invoked from ${here}`);
25
+ log(
26
+ `bootstrap v${SCHEMA}: invoked from ${here} by ${process.execPath} (node ${process.version}, ${process.platform}), home ${home}`,
27
+ );
26
28
  const version = newestVersion(home);
27
29
  if (!version) {
28
30
  log(`bootstrap v${SCHEMA}: no valid version under ${join(home, 'app', 'versions')}`);
@@ -49,6 +51,13 @@ const child = spawn(process.execPath, [entry, 'open'], {
49
51
  env,
50
52
  });
51
53
  if (fd !== null) closeSync(fd);
54
+ // A node that cannot be executed arrives as an event, not an exit, and unhandled
55
+ // it would end this process with nothing on screen.
56
+ child.on('error', (err) => {
57
+ log(`bootstrap v${SCHEMA}: could not start ${entry} (${err.message})`);
58
+ dialog(`Scenri could not start (${err.message}). Open a terminal and run: npx scenri`);
59
+ process.exit(1);
60
+ });
52
61
  log(`bootstrap v${SCHEMA}: handing off to ${entry} (pid ${child.pid ?? 'unknown'})`);
53
62
  if (wait) {
54
63
  child.on('exit', (code) => process.exit(code ?? 1));
@@ -127,8 +136,10 @@ function dialog(message) {
127
136
  { stdio: 'ignore' },
128
137
  );
129
138
  } else if (process.platform === 'win32') {
139
+ // windowsHide keeps the console off screen and the box still shows;
140
+ // probed on a real Windows desktop, where a detached PowerShell shows nothing.
130
141
  spawnSync(
131
- 'powershell.exe',
142
+ powershell(),
132
143
  [
133
144
  '-NoProfile',
134
145
  '-NonInteractive',
@@ -142,3 +153,10 @@ function dialog(message) {
142
153
  /* the log line above is the record */
143
154
  }
144
155
  }
156
+
157
+ /** Windows PowerShell 5.1 by its absolute path: a Desktop click has Explorer's PATH, not the terminal's. */
158
+ function powershell() {
159
+ const root = process.env.SystemRoot || process.env.windir || 'C:\\Windows';
160
+ const absolute = join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
161
+ return existsSync(absolute) ? absolute : 'powershell.exe';
162
+ }
@@ -49,9 +49,12 @@
49
49
  var bar = document.getElementById('bar');
50
50
  var deadline = Date.now() + 90000;
51
51
  var done = false;
52
+ var failed = false;
52
53
  function go() { if (done) return; done = true; location.replace(target); }
54
+ // The fail text is a verdict on the wait, not on the server: a start that
55
+ // finishes late (a first Defender scan of a fresh install) still hands over.
53
56
  function fail(message) {
54
- if (done) return; done = true;
57
+ if (done || failed) return; failed = true;
55
58
  title.textContent = 'Scenri did not start';
56
59
  text.innerHTML = message;
57
60
  bar.hidden = true;
@@ -59,7 +62,7 @@
59
62
  if (!ok) { fail('This page needs to be opened by Scenri itself. Open a terminal and run: <code>npx scenri</code>'); return; }
60
63
  function probe() {
61
64
  if (done) return;
62
- if (Date.now() > deadline) { fail('Open a terminal and run <code>npx scenri</code> to see why.'); return; }
65
+ if (Date.now() > deadline) fail('Open a terminal and run <code>npx scenri</code> to see why.');
63
66
  // An image load needs no CORS and works from a file: URL in every browser;
64
67
  // the opaque fetch is the second opinion for browsers that block images
65
68
  // from a local page.
@@ -69,7 +72,7 @@
69
72
  if (window.fetch) {
70
73
  fetch(target + 'api/version', { mode: 'no-cors', cache: 'no-store' }).then(go, () => {});
71
74
  }
72
- setTimeout(probe, 500);
75
+ setTimeout(probe, failed ? 2000 : 500);
73
76
  }
74
77
  probe();
75
78
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenri",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",