scenri 0.2.3 → 0.3.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.
package/ASSETS-LICENSE.md CHANGED
@@ -1,12 +1,12 @@
1
- # scenri assets license
1
+ # Scenri assets license
2
2
 
3
- scenri's code is AGPL-3.0-only (see LICENSE). The imagery and curated content are
3
+ Scenri's code is AGPL-3.0-only (see LICENSE). The imagery and curated content are
4
4
  licensed under these terms instead.
5
5
 
6
6
  This covers the starter content pack, everything under `templates/`, the library
7
- content scenri downloads at runtime, and the project's marketing imagery.
7
+ content Scenri downloads at runtime, and the project's marketing imagery.
8
8
 
9
- You may use this content freely within scenri, including for commercial work.
9
+ You may use this content freely within Scenri, including for commercial work.
10
10
  Everything you generate with it is yours.
11
11
 
12
12
  You may not redistribute, rebundle, resell, or republish the content itself, and
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1](https://github.com/tonygorb/scenri/compare/v0.3.0...v0.3.1) (2026-08-20)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * calm boot failures and stop the silent hang on a busy port ([d04946f](https://github.com/tonygorb/scenri/commit/d04946f011b204c230473f5338f0a32f8870a1d0))
9
+ * friendly first-run errors and the install guide ([b1782ad](https://github.com/tonygorb/scenri/commit/b1782adb4fc48807e384a208ee8894022d62d615))
10
+
11
+ ## [0.3.0](https://github.com/tonygorb/scenri/compare/v0.2.3...v0.3.0) (2026-08-20)
12
+
13
+
14
+ ### Features
15
+
16
+ * give every browser tab an intentional title ([e518396](https://github.com/tonygorb/scenri/commit/e5183961dae9f52ecd5801ae4c8cfce5fb397f3a))
17
+ * name the product Scenri across the interface, docs and CLI ([cce242b](https://github.com/tonygorb/scenri/commit/cce242be955520569fdffb170f14f91f752d1801))
18
+
3
19
  ## [0.2.3](https://github.com/tonygorb/scenri/compare/v0.2.2...v0.2.3) (2026-08-20)
4
20
 
5
21
 
package/LICENSE CHANGED
@@ -629,7 +629,7 @@ to attach them to the start of each source file to most effectively
629
629
  state the exclusion of warranty; and each file should have at least
630
630
  the "copyright" line and a pointer to where the full notice is found.
631
631
 
632
- scenri - the open studio for brand-consistent AI visuals
632
+ Scenri - the open studio for brand-consistent AI visuals
633
633
  Copyright (C) 2026 Tony Gorb
634
634
 
635
635
  This program is free software: you can redistribute it and/or modify
package/NOTICE CHANGED
@@ -1,4 +1,4 @@
1
- scenri
1
+ Scenri
2
2
  Copyright 2026 Tony Gorb
3
3
 
4
4
  This product includes software and assets developed by third parties.
@@ -46,11 +46,11 @@ Phosphor Icons (`@phosphor-icons/react`), MIT, Copyright (c) 2023 Phosphor Icons
46
46
 
47
47
  ## Third-party marks
48
48
 
49
- scenri shows each generation provider's own mark beside its name, so that the
49
+ Scenri shows each generation provider's own mark beside its name, so that the
50
50
  row asking for your key names the service you recognise rather than a generic
51
51
  glyph. Every mark is reproduced unmodified, in a variant its owner publishes,
52
52
  and is used referentially to identify which service a key or a sign-in belongs
53
- to. scenri is not built, supported, certified, endorsed by, or affiliated with
53
+ to. Scenri is not built, supported, certified, endorsed by, or affiliated with
54
54
  any of them, and makes no such claim.
55
55
 
56
56
  | Mark | File | Artwork source | Artwork licence |
@@ -63,7 +63,7 @@ Each mark sits on the plate its owner puts it on, in the colours its owner
63
63
  publishes, sampled from that owner's own icon: OpenAI's 180px favicon (white
64
64
  plate, black mark), OpenRouter's app icon (white plate, violet mark) and
65
65
  Replicate's favicon (red plate, white mark). The pairs are recorded in `BRAND` in
66
- `apps/studio/src/engines/providers.tsx` and are identical in both of scenri's
66
+ `apps/studio/src/engines/providers.tsx` and are identical in both of Scenri's
67
67
  themes, because none of these owners publishes a dark variant and inventing one
68
68
  would be redrawing their logo. No geometry is redrawn, distorted, recomposed,
69
69
  recoloured or tinted, and no mark is ever separated from its own plate.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <picture>
4
4
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/logo-on-dark.svg">
5
- <img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/logo-on-light.svg" alt="scenri" width="200">
5
+ <img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/logo-on-light.svg" alt="Scenri" width="200">
6
6
  </picture>
7
7
  <br><br>
8
8
 
@@ -19,7 +19,9 @@ Define a client's brand once. Then generate, branch, and art-direct on-brand ima
19
19
  npx scenri
20
20
  ```
21
21
 
22
- <img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/demo.gif" alt="The scenri composer: a dollar sign opens a product picker and Selvedge Trucker is chosen, an at sign picks the presenter Maren, a slash picks the Editorial Walk scene, a line of written direction is typed after the three chips, the shot renders as a card in the wall, and it opens to show the picture beside the brief that made it" width="820">
22
+ <sub>Needs [Node.js](https://nodejs.org) 22 or newer. New to any of this? The [install guide](docs/INSTALL.md) walks through every step.</sub>
23
+
24
+ <img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/demo.gif" alt="The Scenri composer: a dollar sign opens a product picker and Selvedge Trucker is chosen, an at sign picks the presenter Maren, a slash picks the Editorial Walk scene, a line of written direction is typed after the three chips, the shot renders as a card in the wall, and it opens to show the picture beside the brief that made it" width="820">
23
25
 
24
26
  <sub>Pick a product, pick a presenter, pick a scene, then write the direction. That is the brief.</sub>
25
27
 
@@ -29,7 +31,7 @@ npx scenri
29
31
 
30
32
  ## What it is
31
33
 
32
- Most AI image tools give you a prompt box and a slot machine. scenri gives you the part that actually takes the time: **art direction**.
34
+ Most AI image tools give you a prompt box and a slot machine. Scenri gives you the part that actually takes the time: **art direction**.
33
35
 
34
36
  Every shot keeps its lineage. Branch an edit from any shot, put the two side by side, and the drift heatmap shows you exactly where the model changed your product when it should not have.
35
37
 
@@ -47,7 +49,7 @@ All 97 examples on the home wall work the same way. Open one and it loads back i
47
49
 
48
50
  ## What you get out of the box
49
51
 
50
- <img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/library.gif" alt="A slow scroll down the scenri home wall: rows of finished example shots across product categories, with the composer docked at the bottom of the screen" width="820">
52
+ <img src="https://raw.githubusercontent.com/tonygorb/scenri/main/docs/media/library.gif" alt="A slow scroll down the Scenri home wall: rows of finished example shots across product categories, with the composer docked at the bottom of the screen" width="820">
51
53
 
52
54
  A library of scenes, presenters and demo products, so the app is useful before you have uploaded anything of your own. Filter by category, bookmark what fits the brand, or build your own from a few reference photos.
53
55
 
@@ -55,15 +57,17 @@ The brand kit sits one gesture away in Settings and is the part that keeps outpu
55
57
 
56
58
  ## Run it
57
59
 
60
+ Scenri runs on [Node.js](https://nodejs.org), version 22 or newer. If you are not sure you have it, or terminals are not part of your day, the **[install guide](docs/INSTALL.md)** covers every step for macOS, Windows and Linux. With Node in place:
61
+
58
62
  ```bash
59
63
  npx scenri
60
64
  ```
61
65
 
62
- That is the whole install. It opens `http://127.0.0.1:4747`.
66
+ That is the whole install. npm asks once whether to proceed, downloads the current release, and opens `http://127.0.0.1:4747`. Keep the terminal window open while you work; closing it stops Scenri. Tomorrow the same command opens it again, with everything where you left it.
63
67
 
64
- Generation runs on **Codex CLI**, an official helper from OpenAI that draws on your own ChatGPT plan. No API key to paste, and scenri never charges you. Each image draws on your plan's Codex usage. You do not have to set it up by hand: if it is missing, scenri offers to install it and to sign you in, both from the app. No ChatGPT plan? Add your own key from an image provider in Settings instead, see [Engines](#engines).
68
+ Generation runs on **Codex CLI**, an official helper from OpenAI that draws on your own ChatGPT plan. No API key to paste, and Scenri never charges you. Each image draws on your plan's Codex usage. You do not have to set it up by hand: if it is missing, Scenri offers to install it and to sign you in, both from the app. No ChatGPT plan? Add your own key from an image provider in Settings instead, see [Engines](#engines).
65
69
 
66
- Requires **Node 22 or newer**. Two dependencies (`better-sqlite3` and `sharp`) ship native binaries, so on recent npm you may be asked to approve their install scripts once.
70
+ Two dependencies (`better-sqlite3` and `sharp`) ship native binaries, so on recent npm you may be asked to approve their install scripts once. Something not starting? See [troubleshooting](docs/INSTALL.md#troubleshooting).
67
71
 
68
72
  <details>
69
73
  <summary>Run from source</summary>
@@ -80,7 +84,7 @@ pnpm dev # starts the server on 127.0.0.1:4747
80
84
 
81
85
  ## First five minutes
82
86
 
83
- 1. **Paste a website URL.** scenri reads the public page and drafts the kit: name, palette, logo, tone.
87
+ 1. **Paste a website URL.** Scenri reads the public page and drafts the kit: name, palette, logo, tone.
84
88
  2. **Describe a shot.** In the composer, `$` reaches for a product, `@` for a presenter, `/` for a scene, and `#` for a colour. Everything between them is your own words. The composer compiles a brief you can inspect before it runs.
85
89
  3. **Generate on Codex CLI.** Runs on your own ChatGPT plan, so there is no key and no per-image charge from us.
86
90
  4. **Branch an edit** from any shot, then hit compare. The heatmap shows what moved.
@@ -91,7 +95,7 @@ pnpm dev # starts the server on 127.0.0.1:4747
91
95
  - **Iteration is the product.** A version tree, not a prompt box. Branch, compare, keep the winners.
92
96
  - **Your brands are files, not hostages.** `.brand` is an open, documented format under a permissive license. Email one to a client. Any tool can adopt it.
93
97
  - **Your AI, your cost.** Bring your own Codex CLI session or an API key. Experiments cost raw API price, or nothing at all on a local session. No credits that burn on a miss.
94
- - **Local first, and it means it.** No account, no telemetry, no upload. The server binds to your machine only. scenri makes exactly two requests on its own behalf: a daily version-number check against npm so updates can announce themselves, and a one-time download of the library imagery archive, cached locally forever after. Nothing about you or your work is ever sent, and both turn off: in Settings or `SCENRI_NO_UPDATE_CHECK=1` for the first, `SCENRI_NO_CONTENT_FETCH=1` for the second ([how updates work](docs/updates.md)).
98
+ - **Local first, and it means it.** No account, no telemetry, no upload. The server binds to your machine only. Scenri makes exactly two requests on its own behalf: a daily version-number check against npm so updates can announce themselves, and a one-time download of the library imagery archive, cached locally forever after. Nothing about you or your work is ever sent, and both turn off: in Settings or `SCENRI_NO_UPDATE_CHECK=1` for the first, `SCENRI_NO_CONTENT_FETCH=1` for the second ([how updates work](docs/updates.md)).
95
99
 
96
100
  ## Engines
97
101
 
@@ -104,13 +108,13 @@ pnpm dev # starts the server on 127.0.0.1:4747
104
108
 
105
109
  Codex CLI is the default because it needs no key and because it carries the most reference images: a shot that has to keep both a product and a person accurate needs the room.
106
110
 
107
- It is not free. Every ChatGPT plan comes with some Codex usage and each image spends a little of it. OpenAI meters that, not scenri, so spend caps do not apply to this engine.
111
+ It is not free. Every ChatGPT plan comes with some Codex usage and each image spends a little of it. OpenAI meters that, not Scenri, so spend caps do not apply to this engine.
108
112
 
109
- **Without a ChatGPT plan**, use your own provider key. OpenRouter is the one to pick if your shots name a Product or a Presenter. Replicate and fal take no reference images, so scenri refuses those briefs on them rather than generating something that only looks right.
113
+ **Without a ChatGPT plan**, use your own provider key. OpenRouter is the one to pick if your shots name a Product or a Presenter. Replicate and fal take no reference images, so Scenri refuses those briefs on them rather than generating something that only looks right.
110
114
 
111
115
  Keys are stored in your local library folder, sent only to that provider, and never returned by the API. Set a monthly spend cap per engine in Settings.
112
116
 
113
- Your Codex session is yours: scenri runs the official `codex` commands on your machine and never reads, copies or stores the credential. That is also why scenri never pools user plans: a plan is licensed to the person who pays for it, not to a service reselling it to other people. Any hosted version of scenri, if one ever exists, would run API-priced engines only.
117
+ Your Codex session is yours: Scenri runs the official `codex` commands on your machine and never reads, copies or stores the credential. That is also why Scenri never pools user plans: a plan is licensed to the person who pays for it, not to a service reselling it to other people. Any hosted version of Scenri, if one ever exists, would run API-priced engines only.
114
118
 
115
119
  ## Configuration
116
120
 
@@ -127,7 +131,7 @@ Your Codex session is yours: scenri runs the official `codex` commands on your m
127
131
 
128
132
  `OPENROUTER_API_KEY`, `REPLICATE_API_TOKEN` and `FAL_KEY` are read from the environment as an alternative to entering them in Settings.
129
133
 
130
- **Before you change `SCENRI_HOST`:** scenri has no accounts, so anyone who can reach the port can spend your API keys and delete your library. Setting `SCENRI_HOST=0.0.0.0` opens it to your local network so you can use it from a phone. That path prints a URL carrying a one-time access token, refuses every request without it, and rejects unfamiliar `Host` headers. Treat it as convenience on a network you trust, not as a security boundary.
134
+ **Before you change `SCENRI_HOST`:** Scenri has no accounts, so anyone who can reach the port can spend your API keys and delete your library. Setting `SCENRI_HOST=0.0.0.0` opens it to your local network so you can use it from a phone. That path prints a URL carrying a one-time access token, refuses every request without it, and rejects unfamiliar `Host` headers. Treat it as convenience on a network you trust, not as a security boundary.
131
135
 
132
136
  ## Layout
133
137
 
@@ -158,7 +162,7 @@ Early. The version is `0.x` and interfaces can still move. Everything documented
158
162
 
159
163
  The application is [AGPL-3.0-only](LICENSE). The `.brand` format, its schema, and its validator live in [packages/brand-spec](https://github.com/tonygorb/scenri/tree/main/packages/brand-spec) under Apache-2.0, so any tool may implement or reuse them without taking on copyleft; the npm package carries that license text as `LICENSE-APACHE-2.0-brand-spec`. Contributions require a [CLA](CLA.md).
160
164
 
161
- The bundled imagery and curated content are licensed separately under [ASSETS-LICENSE.md](docs/ASSETS-LICENSE.md): free to use within scenri, commercial work included; no redistribution or rebundling. The npm package's `license` field describes the code; the starter imagery inside it stays under those asset terms.
165
+ The bundled imagery and curated content are licensed separately under [ASSETS-LICENSE.md](docs/ASSETS-LICENSE.md): free to use within Scenri, commercial work included; no redistribution or rebundling. The npm package's `license` field describes the code; the starter imagery inside it stays under those asset terms.
162
166
 
163
167
  The name and the mark are not part of the code license: [TRADEMARKS.md](TRADEMARKS.md) says what a fork may and may not call itself. What the app does and does not send anywhere is written down in [PRIVACY.md](PRIVACY.md).
164
168
 
@@ -202,5 +202,5 @@ async function stageVersion(deps) {
202
202
  }
203
203
 
204
204
  export { classify, createUpdateChecker, fetchDistTagLatest, findNpm, stageVersion };
205
- //# sourceMappingURL=chunk-VE675PSG.js.map
206
- //# sourceMappingURL=chunk-VE675PSG.js.map
205
+ //# sourceMappingURL=chunk-GA7P7K2M.js.map
206
+ //# sourceMappingURL=chunk-GA7P7K2M.js.map
@@ -0,0 +1,36 @@
1
+ // src/bootError.ts
2
+ var INSTALL_GUIDE = "https://github.com/tonygorb/scenri/blob/main/docs/INSTALL.md";
3
+ var NATIVE_MARKERS = ["NODE_MODULE_VERSION", "Could not locate the bindings file", "ERR_DLOPEN_FAILED"];
4
+ function portBusyLines(port) {
5
+ const next = port + 1;
6
+ return [
7
+ `Port ${port} is in use by another app.`,
8
+ "Start Scenri on a different port:",
9
+ ` macOS or Linux: SCENRI_PORT=${next} npx scenri`,
10
+ ` Windows PowerShell: $env:SCENRI_PORT=${next}; npx scenri`
11
+ ];
12
+ }
13
+ function bootErrorLines(err) {
14
+ const raw = err instanceof Error ? err.message : String(err);
15
+ const first = raw.split("\n")[0];
16
+ const code = err?.code;
17
+ const haystack = `${typeof code === "string" ? code : ""} ${raw}`;
18
+ if (NATIVE_MARKERS.some((marker) => haystack.includes(marker))) {
19
+ return [
20
+ "Scenri could not start: a native component failed to load.",
21
+ `(${first})`,
22
+ "This usually means Node changed since this copy of Scenri was installed.",
23
+ "Fix: install the current Node LTS from https://nodejs.org, then run npx scenri@latest."
24
+ ];
25
+ }
26
+ return [
27
+ `Scenri could not start: ${first}`,
28
+ "If this keeps happening, run npx scenri@latest, or see the install guide:",
29
+ INSTALL_GUIDE,
30
+ "Set SCENRI_DEBUG=1 to see the full error."
31
+ ];
32
+ }
33
+
34
+ export { bootErrorLines, portBusyLines };
35
+ //# sourceMappingURL=chunk-WGIZJXNE.js.map
36
+ //# sourceMappingURL=chunk-WGIZJXNE.js.map
@@ -1,4 +1,4 @@
1
- import { stageVersion, fetchDistTagLatest, findNpm } from './chunk-VE675PSG.js';
1
+ import { stageVersion, fetchDistTagLatest, findNpm } from './chunk-GA7P7K2M.js';
2
2
  import { readMeta } from './chunk-Y3ZPBPLP.js';
3
3
  import { defaultHome, newestStaged, compareSemver } from './chunk-4MAFHYAD.js';
4
4
 
@@ -34,7 +34,7 @@ async function runUpdateCommand(opts) {
34
34
  const effective = staged && compareSemver(staged, meta.version) > 0 ? staged : meta.version;
35
35
  console.log(` running ${meta.version}${staged ? ` \xB7 staged ${staged}` : ""} \xB7 latest ${latest}`);
36
36
  if (compareSemver(latest, effective) <= 0) {
37
- console.log(staged ? " the newest version is already staged. Just start scenri." : " already the newest.");
37
+ console.log(staged ? " the newest version is already staged. Just start Scenri." : " already the newest.");
38
38
  return 0;
39
39
  }
40
40
  if (opts.check) {
@@ -62,5 +62,5 @@ async function runUpdateCommand(opts) {
62
62
  }
63
63
 
64
64
  export { runUpdateCommand };
65
- //# sourceMappingURL=cli-JVN6MY72.js.map
66
- //# sourceMappingURL=cli-JVN6MY72.js.map
65
+ //# sourceMappingURL=cli-GQLRYYGU.js.map
66
+ //# sourceMappingURL=cli-GQLRYYGU.js.map
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { bootErrorLines } from './chunk-WGIZJXNE.js';
3
+
2
4
  // src/args.ts
3
5
  function parseArgs(argv) {
4
6
  const [first, ...rest] = argv;
@@ -23,10 +25,10 @@ function parseArgs(argv) {
23
25
  return { cmd: "error", message: `unknown command '${first}' (try --help)` };
24
26
  }
25
27
  function helpText() {
26
- return `scenri: local brand studio
28
+ return `Scenri: local brand studio
27
29
 
28
30
  Usage
29
- scenri start scenri (auto-restarts after updates)
31
+ scenri start Scenri (auto-restarts after updates)
30
32
  scenri serve start this exact build, no supervision
31
33
  scenri update download and stage the newest version
32
34
  --check only check, do not download
@@ -41,43 +43,51 @@ SCENRI_CONTENT_URL.
41
43
 
42
44
  // src/index.ts
43
45
  var command = parseArgs(process.argv.slice(2));
44
- switch (command.cmd) {
45
- case "version": {
46
- const { readMeta } = await import('./meta-MPQPBPBK.js');
47
- console.log(readMeta().version);
48
- break;
49
- }
50
- case "help":
51
- console.log(helpText());
52
- break;
53
- case "error":
54
- console.error(command.message);
55
- process.exit(2);
56
- break;
57
- case "verify":
58
- await (await import('./serve.js')).verify();
59
- break;
60
- case "serve":
61
- await (await import('./serve.js')).serve();
62
- break;
63
- case "update": {
64
- const { runUpdateCommand } = await import('./cli-JVN6MY72.js');
65
- process.exit(await runUpdateCommand(command));
66
- break;
67
- }
68
- case "launch": {
69
- const ownEntry = process.argv[1] ?? "";
70
- if (!/[\\/]dist[\\/]/.test(ownEntry)) {
46
+ try {
47
+ switch (command.cmd) {
48
+ case "version": {
49
+ const { readMeta } = await import('./meta-MPQPBPBK.js');
50
+ console.log(readMeta().version);
51
+ break;
52
+ }
53
+ case "help":
54
+ console.log(helpText());
55
+ break;
56
+ case "error":
57
+ console.error(command.message);
58
+ process.exit(2);
59
+ break;
60
+ case "verify":
61
+ await (await import('./serve.js')).verify();
62
+ break;
63
+ case "serve":
71
64
  await (await import('./serve.js')).serve();
72
65
  break;
66
+ case "update": {
67
+ const { runUpdateCommand } = await import('./cli-GQLRYYGU.js');
68
+ process.exit(await runUpdateCommand(command));
69
+ break;
70
+ }
71
+ case "launch": {
72
+ const ownEntry = process.argv[1] ?? "";
73
+ if (!/[\\/]dist[\\/]/.test(ownEntry)) {
74
+ await (await import('./serve.js')).serve();
75
+ break;
76
+ }
77
+ const { runLauncher } = await import('./launcher-RETYUGSR.js');
78
+ const { readMeta } = await import('./meta-MPQPBPBK.js');
79
+ const { defaultHome } = await import('./versionsDir-ILQ4CI7B.js');
80
+ const meta = readMeta();
81
+ process.exit(await runLauncher({ home: defaultHome(), pkg: meta.name, ownVersion: meta.version, ownEntry }));
82
+ break;
73
83
  }
74
- const { runLauncher } = await import('./launcher-YT5KSSTN.js');
75
- const { readMeta } = await import('./meta-MPQPBPBK.js');
76
- const { defaultHome } = await import('./versionsDir-ILQ4CI7B.js');
77
- const meta = readMeta();
78
- process.exit(await runLauncher({ home: defaultHome(), pkg: meta.name, ownVersion: meta.version, ownEntry }));
79
- break;
80
84
  }
85
+ } catch (err) {
86
+ if (process.env.SCENRI_DEBUG === "1") throw err;
87
+ console.error("");
88
+ for (const line of bootErrorLines(err)) console.error(` ${line}`);
89
+ console.error("");
90
+ process.exit(1);
81
91
  }
82
92
  //# sourceMappingURL=index.js.map
83
93
  //# sourceMappingURL=index.js.map
@@ -31,7 +31,7 @@ async function runLauncher(deps) {
31
31
  starts.push(startedAt);
32
32
  while (starts.length > 0 && startedAt - starts[0] > LOOP_WINDOW_MS) starts.shift();
33
33
  if (starts.length > LOOP_MAX_STARTS) {
34
- log(" scenri is restarting in a loop and has stopped trying.");
34
+ log(" Scenri is restarting in a loop and has stopped trying.");
35
35
  log(" Recover with: rm -rf ~/.scenri/app (app versions only, your library is untouched)");
36
36
  log(" then start fresh: npx scenri@latest");
37
37
  return 1;
@@ -51,7 +51,7 @@ async function runLauncher(deps) {
51
51
  if (signalled) return code ?? 0;
52
52
  if (code === RESTART_EXIT_CODE) continue;
53
53
  if (code !== 0 && useStaged && now() - startedAt < QUICK_DEATH_MS) {
54
- log(` scenri ${staged} failed to start. Falling back to ${deps.ownVersion}.`);
54
+ log(` Scenri ${staged} failed to start. Falling back to ${deps.ownVersion}.`);
55
55
  log(" If this keeps happening: rm -rf ~/.scenri/app");
56
56
  distrustStaged = true;
57
57
  continue;
@@ -61,5 +61,5 @@ async function runLauncher(deps) {
61
61
  }
62
62
 
63
63
  export { LAUNCHER_PROTOCOL, RESTART_EXIT_CODE, runLauncher };
64
- //# sourceMappingURL=launcher-YT5KSSTN.js.map
65
- //# sourceMappingURL=launcher-YT5KSSTN.js.map
64
+ //# sourceMappingURL=launcher-RETYUGSR.js.map
65
+ //# sourceMappingURL=launcher-RETYUGSR.js.map
package/dist/serve.js CHANGED
@@ -1,4 +1,5 @@
1
- import { createUpdateChecker, classify, stageVersion, findNpm } from './chunk-VE675PSG.js';
1
+ import { portBusyLines } from './chunk-WGIZJXNE.js';
2
+ import { createUpdateChecker, classify, stageVersion, findNpm } from './chunk-GA7P7K2M.js';
2
3
  import { readMeta, repoSlug } from './chunk-Y3ZPBPLP.js';
3
4
  import { newestStaged, compareSemver } from './chunk-4MAFHYAD.js';
4
5
  import { networkInterfaces, homedir, tmpdir } from 'os';
@@ -297,7 +298,7 @@ var SCHEMA_VERSION = 1;
297
298
  var SchemaTooNewError = class extends Error {
298
299
  constructor(found, supported) {
299
300
  super(
300
- `This library was written by a newer scenri (schema ${found}; this build understands ${supported}). Update and retry: npx scenri@latest`
301
+ `This library was written by a newer Scenri (schema ${found}; this build understands ${supported}). Update and retry: npx scenri@latest`
301
302
  );
302
303
  this.name = "SchemaTooNewError";
303
304
  }
@@ -2149,7 +2150,7 @@ function createCodexSetup(opts = {}) {
2149
2150
  ok: false,
2150
2151
  fallbackCommand: INSTALL_COMMAND,
2151
2152
  docsUrl: INSTALL_DOCS_URL,
2152
- detail: "Codex installed but is not on this app\u2019s PATH yet. Restart scenri, or install it yourself."
2153
+ detail: "Codex installed but is not on this app\u2019s PATH yet. Restart Scenri, or install it yourself."
2153
2154
  };
2154
2155
  }
2155
2156
  return { ok: true };
@@ -2308,7 +2309,7 @@ function createDemoEngine(saveImage) {
2308
2309
  <rect width="100%" height="100%" fill="url(#g)"/>
2309
2310
  <circle cx="${w * (0.25 + 0.5 * (seed * 37 % 100) / 100)}" cy="${h * 0.38}" r="${Math.min(w, h) * 0.18}" fill="#ffffff" opacity="0.25"/>
2310
2311
  <text x="24" y="${h - 48}" font-family="Helvetica, Arial" font-size="${Math.max(14, Math.round(w / 42))}" fill="#ffffff" opacity="0.92">${esc(label)}</text>
2311
- <text x="24" y="${h - 22}" font-family="Helvetica, Arial" font-size="12" fill="#ffffff" opacity="0.6">scenri demo engine</text>
2312
+ <text x="24" y="${h - 22}" font-family="Helvetica, Arial" font-size="12" fill="#ffffff" opacity="0.6">Scenri demo engine</text>
2312
2313
  </svg>`;
2313
2314
  return sharp5(Buffer.from(svg)).png().toBuffer();
2314
2315
  }
@@ -2946,7 +2947,7 @@ function createContentFetcher(deps) {
2946
2947
  const root = contentCacheRoot(env);
2947
2948
  const staging = `${root}.staging`;
2948
2949
  if (!deps.store.getSetting("content.disclosed")) {
2949
- log(" fetching the scenri library (~95 MB, once, cached; set SCENRI_NO_CONTENT_FETCH=1 to disable)");
2950
+ log(" fetching the Scenri library (~95 MB, once, cached; set SCENRI_NO_CONTENT_FETCH=1 to disable)");
2950
2951
  deps.store.setSetting("content.disclosed", "1");
2951
2952
  }
2952
2953
  try {
@@ -2982,7 +2983,7 @@ function createContentFetcher(deps) {
2982
2983
  deps.store.setSetting("content.version", String(meta.version ?? ""));
2983
2984
  } catch {
2984
2985
  }
2985
- log(" scenri library ready");
2986
+ log(" Scenri library ready");
2986
2987
  return { ok: true, updated: true, error: null };
2987
2988
  } catch (err) {
2988
2989
  rmSync(staging, { recursive: true, force: true });
@@ -5900,11 +5901,11 @@ function registerAssetBuildRoutes(app, deps) {
5900
5901
  engineId: engine?.capabilities().id ?? null,
5901
5902
  engineName: engine?.capabilities().displayName ?? null,
5902
5903
  /**
5903
- * True when scenri cannot price this per image, because it is not billed
5904
+ * True when Scenri cannot price this per image, because it is not billed
5904
5905
  * through a key we hold. NOT the same as costing the user nothing: the
5905
5906
  * local Codex engine spends the Codex allowance on their ChatGPT plan,
5906
5907
  * which only OpenAI can meter. Copy built on this flag must say "nothing
5907
- * billed through scenri", never "free".
5908
+ * billed through Scenri", never "free".
5908
5909
  */
5909
5910
  free: engine ? await engine.costEstimate(COST_PROBE).catch(() => 0) <= 0 : true
5910
5911
  };
@@ -6514,6 +6515,31 @@ function registerImageRoutes(app, deps) {
6514
6515
 
6515
6516
  // src/release/notes.data.ts
6516
6517
  var RELEASES = [
6518
+ {
6519
+ version: "0.3.1",
6520
+ date: "2026-08-20",
6521
+ sections: [
6522
+ {
6523
+ heading: "Fixes",
6524
+ body: "Starting Scenri is calmer when something is wrong. A busy port or a failed start now explains itself in plain words instead of a stack trace, and the terminal says to keep its window open while Scenri runs."
6525
+ }
6526
+ ]
6527
+ },
6528
+ {
6529
+ version: "0.3.0",
6530
+ date: "2026-08-20",
6531
+ title: "The name is Scenri.",
6532
+ sections: [
6533
+ {
6534
+ heading: "The name",
6535
+ body: "Written out, the product is Scenri. Every screen, every page of the documentation and every line the terminal prints now spells it that way. Lowercase stays where a machine reads it: the command you type, the folder your library lives in, and the settings that configure it are all unchanged."
6536
+ },
6537
+ {
6538
+ heading: "Browser tabs",
6539
+ body: "A tab now names what is open in it rather than the section it came from, so eight products in eight tabs read as eight product names. Settings and What's new say so, an open shot says Shot, and a tab whose page is still loading names its section instead of going blank."
6540
+ }
6541
+ ]
6542
+ },
6517
6543
  {
6518
6544
  version: "0.2.3",
6519
6545
  date: "2026-08-20",
@@ -6700,7 +6726,7 @@ function registerUpdateRoutes(app, deps) {
6700
6726
  const apply = effectiveApply();
6701
6727
  if (apply.phase !== "ready") return reply.status(409).send({ error: "no staged update to restart into" });
6702
6728
  if (!runtime.supervised) {
6703
- return reply.status(409).send({ error: "not supervised; restart scenri yourself", blockReason: "unsupervised" });
6729
+ return reply.status(409).send({ error: "not supervised; restart Scenri yourself", blockReason: "unsupervised" });
6704
6730
  }
6705
6731
  reply.send({ ok: true });
6706
6732
  const exit = deps.exitImpl ?? ((code) => process.exit(code));
@@ -7500,12 +7526,14 @@ async function run() {
7500
7526
  continue;
7501
7527
  }
7502
7528
  try {
7503
- const res = await fetch(`http://127.0.0.1:${PORT}/api/version`);
7529
+ const res = await fetch(`http://127.0.0.1:${PORT}/api/version`, {
7530
+ signal: AbortSignal.timeout(2e3)
7531
+ });
7504
7532
  const info = await res.json();
7505
7533
  if (info.name === readMeta().name) {
7506
7534
  const url = `http://127.0.0.1:${PORT}`;
7507
7535
  console.log(`
7508
- scenri is already running \u2192 ${url}
7536
+ Scenri is already running \u2192 ${url}
7509
7537
  `);
7510
7538
  if (process.env.SCENRI_NO_OPEN !== "1") {
7511
7539
  try {
@@ -7518,6 +7546,10 @@ async function run() {
7518
7546
  }
7519
7547
  } catch {
7520
7548
  }
7549
+ console.error("");
7550
+ for (const line of portBusyLines(PORT)) console.error(` ${line}`);
7551
+ console.error("");
7552
+ process.exit(1);
7521
7553
  }
7522
7554
  throw err;
7523
7555
  }
@@ -7527,7 +7559,7 @@ async function run() {
7527
7559
  if (closing) return;
7528
7560
  closing = true;
7529
7561
  console.log(`
7530
- ${signal}: closing scenri\u2026`);
7562
+ ${signal}: closing Scenri\u2026`);
7531
7563
  setTimeout(() => process.exit(1), 5e3).unref();
7532
7564
  app.drain().then(() => process.exit(0)).catch(() => process.exit(1));
7533
7565
  };
@@ -7538,10 +7570,10 @@ async function run() {
7538
7570
  const query = token ? `/?t=${token}` : "";
7539
7571
  const localUrl = `http://127.0.0.1:${PORT}${query}`;
7540
7572
  console.log(`
7541
- scenri studio \u2192 ${localUrl}`);
7573
+ Scenri Studio \u2192 ${localUrl}`);
7542
7574
  for (const ip of reachableAt) console.log(` on your network \u2192 http://${ip}:${PORT}${query}`);
7543
- console.log(` data dir \u2192 ${core.home}
7544
- `);
7575
+ console.log(` data dir \u2192 ${core.home}`);
7576
+ console.log(" Keep this window open while Scenri is running.\n");
7545
7577
  if (!studioDist) console.log(" (studio UI not built, API only. Run: pnpm build)\n");
7546
7578
  if (token) {
7547
7579
  console.log(" Warning: the studio is reachable from your network.");
@@ -7551,10 +7583,22 @@ async function run() {
7551
7583
  console.log(" For this machine only, unset SCENRI_HOST.\n");
7552
7584
  }
7553
7585
  if (process.env.SCENRI_NO_OPEN !== "1") {
7586
+ let told = false;
7587
+ const tellUrl = () => {
7588
+ if (told) return;
7589
+ told = true;
7590
+ console.log(` Open ${localUrl} in your browser.
7591
+ `);
7592
+ };
7554
7593
  try {
7555
7594
  const { default: open } = await import('open');
7556
- await open(localUrl);
7595
+ const child = await open(localUrl);
7596
+ child.once("error", tellUrl);
7597
+ child.once("exit", (openExit) => {
7598
+ if (openExit !== null && openExit !== 0) tellUrl();
7599
+ });
7557
7600
  } catch {
7601
+ tellUrl();
7558
7602
  }
7559
7603
  }
7560
7604
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenri",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",