scenri 0.10.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.11.0](https://github.com/tonygorb/Scenri/compare/v0.10.3...v0.11.0) (2026-09-18)
4
+
5
+
6
+ ### Features
7
+
8
+ * first use, a tutor that walks the real task, with First steps and Learn ([47e9775](https://github.com/tonygorb/Scenri/commit/47e977569049803f0544ce19ad0289cdc8e187f5))
9
+ * Learn, every lesson in one place, begun and continued with the same tutor ([1d378e1](https://github.com/tonygorb/Scenri/commit/1d378e17f22a8ebf64878d67caa0996027e5f1f6))
10
+ * **studio:** New lands on Create with the add panel open ([674bf9c](https://github.com/tonygorb/Scenri/commit/674bf9cbb369443e75b78586210a0b0b12c94826))
11
+ * **studio:** New's plus rides a gold disc ([673a17b](https://github.com/tonygorb/Scenri/commit/673a17b1e48eb491ecc2c9a1d122955d11e24d0d))
12
+ * **studio:** the brand menu leads with where you have been and stays one height ([e28a857](https://github.com/tonygorb/Scenri/commit/e28a8573f797f75815687239cd58c1b4d01518e7))
13
+ * **studio:** the new top bar ([e1d262b](https://github.com/tonygorb/Scenri/commit/e1d262b9eb21a4fd788dae8acd487096223b87c6))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * a product made from the picker is named, and Enter on a control is that control's ([7383c7b](https://github.com/tonygorb/Scenri/commit/7383c7baa6fde61dcbf060c68cb429960caee73a))
19
+ * a task ended on a surface is not begun there again, and an empty chip panel foot is not drawn ([2b450fd](https://github.com/tonygorb/Scenri/commit/2b450fd3b7aeb875fee5649b670f3839af0f7894))
20
+ * closing Learn gives the keyboard back to Help when what opened it is gone ([c12d690](https://github.com/tonygorb/Scenri/commit/c12d690fad3e7ce4d1cfaed05aa6982a97d5de4c))
21
+ * in the walk a chip comes in through its ask and leaves only through Back ([a0e9fea](https://github.com/tonygorb/Scenri/commit/a0e9fea7426755ba020f766f0a0678df96542e2c))
22
+ * **studio:** a bar menu answers the next click, even while it is closing ([7b0af47](https://github.com/tonygorb/Scenri/commit/7b0af476d0af0691be60616687955c3521171787))
23
+ * **studio:** a brand icon that is its own ground fills its circle ([0f356fb](https://github.com/tonygorb/Scenri/commit/0f356fbbd19e899ae41751851f873fedbce4d9b5))
24
+ * **studio:** a brand's circle shows its icon, or its initial, never a shrunken wordmark ([6c8415d](https://github.com/tonygorb/Scenri/commit/6c8415da61b261629cee734366a1cece630a58c4))
25
+ * **studio:** New pressed with the add panel open leaves it open and focuses the brief ([37050a4](https://github.com/tonygorb/Scenri/commit/37050a4e4b99f431649f97423ab6596f8ba52dad))
26
+ * **studio:** New wears round ends, the mark shows its box, and Clear all joins its list ([2ed412e](https://github.com/tonygorb/Scenri/commit/2ed412e1faf4d60378b0996f02373ea804b27fe9))
27
+ * **studio:** New's plus sits on an ink disc, and the bar no longer shrinks ([86ceaa3](https://github.com/tonygorb/Scenri/commit/86ceaa33dc16856a1c3fa2e4950360d1a77c5eec))
28
+ * **studio:** pointing at New's plus lights the whole button ([d3a4973](https://github.com/tonygorb/Scenri/commit/d3a4973dc8ee9a0ce090e24fd3f3f8a591c1b0f5))
29
+ * **studio:** the bar wears the full Scenri lockup, bare ([25efd02](https://github.com/tonygorb/Scenri/commit/25efd0292c1d0982ec1f027364489306cf294bea))
30
+ * **studio:** the brand list ends on a whole row ([a20da23](https://github.com/tonygorb/Scenri/commit/a20da23fd89240fd533944ad222c62144795a12e))
31
+ * **studio:** the brand list fades into the line under it while there is more ([18073f8](https://github.com/tonygorb/Scenri/commit/18073f8ea67157c64bbb7b676eb9aade51276614))
32
+ * **studio:** the brand list is ten rows tall ([df5b4e1](https://github.com/tonygorb/Scenri/commit/df5b4e1b834aabf9b4dc22338e83ce99a19550e3))
33
+ * **studio:** the brand list runs straight into the line under it ([402e017](https://github.com/tonygorb/Scenri/commit/402e0176e3bb7ae5cc799af77de4852e92608ba1))
34
+ * **studio:** the brand menu is one compact list ([1e3dc70](https://github.com/tonygorb/Scenri/commit/1e3dc7002cae58fbc8bacc567cf3a924a38a6a1c))
35
+ * **studio:** the brand menu is one list, and the help button leaves this branch ([2e717d1](https://github.com/tonygorb/Scenri/commit/2e717d1474f45e625e85bef36b69d3397cb20925))
36
+ * the tutor keeps what a moment is about in sight, and lets people go their own way ([41e667e](https://github.com/tonygorb/Scenri/commit/41e667e94a930d85bf1ad88fbba4bf00ae6f5ece))
37
+ * the welcome reads top to bottom, speaks plainly, and opens again from Help ([7edb94f](https://github.com/tonygorb/Scenri/commit/7edb94f289e17c1c4255cc6ef683758bc21aea17))
38
+
39
+ ## [0.10.3](https://github.com/tonygorb/Scenri/compare/v0.10.2...v0.10.3) (2026-09-18)
40
+
41
+
42
+ ### Bug Fixes
43
+
44
+ * look for npm without freezing the server ([db1bde3](https://github.com/tonygorb/Scenri/commit/db1bde3e1450353bd0f62c7ecf2a2796ea2af75d))
45
+ * look for npm without freezing the server ([cec4b03](https://github.com/tonygorb/Scenri/commit/cec4b0327f2dd4f1846e9c9192926876db851ada))
46
+
3
47
  ## [0.10.2](https://github.com/tonygorb/Scenri/compare/v0.10.1...v0.10.2) (2026-09-17)
4
48
 
5
49
 
@@ -1344,6 +1344,55 @@ function createStore(db) {
1344
1344
  setOverlays(id, overlays) {
1345
1345
  db.prepare("UPDATE nodes SET overlays=? WHERE id=?").run(JSON.stringify(overlays), id);
1346
1346
  },
1347
+ // first use: what the library already proves, read by the guide record
1348
+ /** The database's own clock, in the format every node's created_at uses. */
1349
+ now() {
1350
+ return db.prepare("SELECT strftime('%Y-%m-%d %H:%M:%f','now') AS t").get().t;
1351
+ },
1352
+ /** Whether any brand holds a finished shot, or a finished refinement, with a picture. */
1353
+ hasFinishedNode(kind) {
1354
+ return !!db.prepare("SELECT 1 FROM nodes WHERE kind=? AND status='done' AND images != '[]' LIMIT 1").get(kind);
1355
+ },
1356
+ /** Whether any brand has imported a product from a store. */
1357
+ hasCatalogProduct() {
1358
+ return !!db.prepare("SELECT 1 FROM catalog_products LIMIT 1").get();
1359
+ },
1360
+ /** A brand's shots of one kind made at or after a moment, newest first: what a guided task has made. */
1361
+ nodesSince(brandId, kind, since, limit = 8) {
1362
+ const rows = db.prepare(
1363
+ `SELECT id, kind, status, images, created_at FROM nodes
1364
+ WHERE project_id IN (SELECT id FROM projects WHERE brand_id = ?)
1365
+ AND kind = ? AND created_at >= ?
1366
+ ORDER BY created_at DESC, id DESC LIMIT ?`
1367
+ ).all(brandId, kind, since, limit);
1368
+ return rows.map((r) => ({
1369
+ id: r.id,
1370
+ kind: r.kind,
1371
+ status: r.status,
1372
+ images: (() => {
1373
+ try {
1374
+ const a = JSON.parse(r.images);
1375
+ return Array.isArray(a) ? a.length : 0;
1376
+ } catch {
1377
+ return 0;
1378
+ }
1379
+ })(),
1380
+ createdAt: r.created_at
1381
+ }));
1382
+ },
1383
+ /** The newest presenter draft a brand started at or after a moment, never an edit session's. */
1384
+ presenterDraftSince(brandId, since) {
1385
+ const rows = db.prepare(
1386
+ "SELECT id, json FROM presenter_drafts WHERE brand_id = ? AND created_at >= ? ORDER BY created_at DESC, id DESC"
1387
+ ).all(brandId, since);
1388
+ for (const r of rows) {
1389
+ try {
1390
+ if (!JSON.parse(r.json)?.presenterId) return r.id;
1391
+ } catch {
1392
+ }
1393
+ }
1394
+ return null;
1395
+ },
1347
1396
  // settings
1348
1397
  getSetting(key) {
1349
1398
  const r = db.prepare("SELECT value FROM settings WHERE key=?").get(key);
@@ -2175,5 +2224,5 @@ function createCore(homeDir = defaultHome()) {
2175
2224
  }
2176
2225
 
2177
2226
  export { ASPECT_TOLERANCE, BUDGET_EXHAUSTED, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, SCHEMA_VERSION, STEM_MIN, SchemaTooNewError, SpendCapError, TRIGRAM_MIN, budgetSize, createCatalogStore, createCore, createStore, defaultHome, fold, ftsMatch, matchesQuery, ratioLabel, searchTerms, termMatches, uniqueProjectSlug, uniqueSetSlug, uniqueSlug };
2178
- //# sourceMappingURL=chunk-UUYMUOZQ.js.map
2179
- //# sourceMappingURL=chunk-UUYMUOZQ.js.map
2227
+ //# sourceMappingURL=chunk-K6IELFL3.js.map
2228
+ //# sourceMappingURL=chunk-K6IELFL3.js.map
@@ -1,5 +1,5 @@
1
1
  import { stagingDir, versionsDir, isValidVersionDir, pruneStaged, entryOf } from './chunk-4MAFHYAD.js';
2
- import { spawnSync, spawn } from 'child_process';
2
+ import { spawn, execFile } from 'child_process';
3
3
  import { existsSync, rmSync, mkdirSync, readFileSync, renameSync } from 'fs';
4
4
  import { join, dirname } from 'path';
5
5
 
@@ -122,37 +122,46 @@ function createUpdateChecker(deps) {
122
122
  }
123
123
  };
124
124
  }
125
- function findNpm(opts = {}) {
125
+ async function findNpm(opts = {}) {
126
126
  const env = opts.env ?? process.env;
127
127
  const platform = opts.platform ?? process.platform;
128
- const canRun = opts.canRun ?? ((argv) => {
129
- try {
130
- return spawnSync(argv[0], [...argv.slice(1), "--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
131
- } catch {
132
- return false;
133
- }
134
- });
135
- if (platform !== "win32" && canRun(["npm"])) return ["npm"];
128
+ const canRun = opts.canRun ?? answersVersion;
129
+ if (platform !== "win32" && await canRun(["npm"])) return ["npm"];
136
130
  const ep = env.npm_execpath?.replace(/npx-cli\.js$/, "npm-cli.js");
137
- if (ep && /npm-cli\.js$/.test(ep) && canRun([process.execPath, ep])) return [process.execPath, ep];
131
+ if (ep && /npm-cli\.js$/.test(ep) && await canRun([process.execPath, ep])) return [process.execPath, ep];
138
132
  if (platform === "win32") {
139
- const cli = windowsNpmCli();
140
- if (cli && canRun([process.execPath, cli])) return [process.execPath, cli];
133
+ const cli = await windowsNpmCli();
134
+ if (cli && await canRun([process.execPath, cli])) return [process.execPath, cli];
141
135
  }
142
136
  return null;
143
137
  }
138
+ function answersVersion(argv) {
139
+ return new Promise((resolve) => {
140
+ try {
141
+ const child = spawn(argv[0], [...argv.slice(1), "--version"], { stdio: "ignore", timeout: 1e4 });
142
+ child.on("error", () => resolve(false));
143
+ child.on("exit", (code) => resolve(code === 0));
144
+ } catch {
145
+ resolve(false);
146
+ }
147
+ });
148
+ }
144
149
  function windowsNpmCli() {
145
- try {
146
- const out = spawnSync("where", ["npm"], { encoding: "utf8", timeout: 1e4 });
147
- if (out.status !== 0 || !out.stdout) return null;
148
- for (const line of out.stdout.split(/\r?\n/)) {
149
- if (!/npm(\.cmd)?$/i.test(line.trim())) continue;
150
- const cli = join(dirname(line.trim()), "node_modules", "npm", "bin", "npm-cli.js");
151
- if (existsSync(cli)) return cli;
150
+ return new Promise((resolve) => {
151
+ try {
152
+ execFile("where", ["npm"], { encoding: "utf8", timeout: 1e4 }, (err, stdout) => {
153
+ if (err || !stdout) return resolve(null);
154
+ for (const line of stdout.split(/\r?\n/)) {
155
+ if (!/npm(\.cmd)?$/i.test(line.trim())) continue;
156
+ const cli = join(dirname(line.trim()), "node_modules", "npm", "bin", "npm-cli.js");
157
+ if (existsSync(cli)) return resolve(cli);
158
+ }
159
+ resolve(null);
160
+ });
161
+ } catch {
162
+ resolve(null);
152
163
  }
153
- } catch {
154
- }
155
- return null;
164
+ });
156
165
  }
157
166
  function run(argv) {
158
167
  return new Promise((resolve) => {
@@ -181,7 +190,7 @@ async function runVerify(entry) {
181
190
  }
182
191
  async function stageVersion(deps) {
183
192
  const env = deps.env ?? process.env;
184
- const npmArgv = deps.npmArgv !== void 0 ? deps.npmArgv : findNpm({ env });
193
+ const npmArgv = deps.npmArgv !== void 0 ? deps.npmArgv : await findNpm({ env });
185
194
  if (!npmArgv) {
186
195
  return { ok: false, reason: "no-npm", detail: "npm is not reachable from this process" };
187
196
  }
@@ -247,5 +256,5 @@ async function stageVersion(deps) {
247
256
  }
248
257
 
249
258
  export { classify, createUpdateChecker, fetchDistTagLatest, findNpm, isReleaseTriplet, stageVersion };
250
- //# sourceMappingURL=chunk-FYQ5BAFA.js.map
251
- //# sourceMappingURL=chunk-FYQ5BAFA.js.map
259
+ //# sourceMappingURL=chunk-YK37H6DM.js.map
260
+ //# sourceMappingURL=chunk-YK37H6DM.js.map
@@ -463,7 +463,7 @@ async function addOrRepair(ownEntry, say, over) {
463
463
  }
464
464
  async function rememberDecline(home) {
465
465
  try {
466
- const { createCore } = await import('./src-QZZDZM7I.js');
466
+ const { createCore } = await import('./src-TC4UQ7H6.js');
467
467
  const core = createCore(home);
468
468
  try {
469
469
  core.store.setSetting("desktop.prompt", "declined");
@@ -530,5 +530,5 @@ function tail(path, bytes = 4096) {
530
530
  }
531
531
 
532
532
  export { addToDesktop, assetsDirFor, installDeps, runDesktopCommand, runExecFile, runOpenCommand };
533
- //# sourceMappingURL=cli-3KKS7BE4.js.map
534
- //# sourceMappingURL=cli-3KKS7BE4.js.map
533
+ //# sourceMappingURL=cli-GMRPOISO.js.map
534
+ //# sourceMappingURL=cli-GMRPOISO.js.map
@@ -1,4 +1,4 @@
1
- import { stageVersion, fetchDistTagLatest, findNpm } from './chunk-FYQ5BAFA.js';
1
+ import { stageVersion, fetchDistTagLatest, findNpm } from './chunk-YK37H6DM.js';
2
2
  import { readMeta } from './chunk-Y3ZPBPLP.js';
3
3
  import { defaultHome, newestStaged, compareSemver } from './chunk-4MAFHYAD.js';
4
4
 
@@ -41,7 +41,7 @@ async function runUpdateCommand(opts) {
41
41
  console.log(` ${latest} is available. Stage it with: scenri update`);
42
42
  return 0;
43
43
  }
44
- if (!findNpm()) {
44
+ if (!await findNpm()) {
45
45
  console.error(" npm is not reachable from here, so nothing can be downloaded.");
46
46
  console.error(" Run the newest version directly with: npx scenri@latest");
47
47
  console.error(" (or install Node.js with npm and try again)");
@@ -63,5 +63,5 @@ async function runUpdateCommand(opts) {
63
63
  }
64
64
 
65
65
  export { runUpdateCommand };
66
- //# sourceMappingURL=cli-KSKENGIR.js.map
67
- //# sourceMappingURL=cli-KSKENGIR.js.map
66
+ //# sourceMappingURL=cli-VBJGIJI4.js.map
67
+ //# sourceMappingURL=cli-VBJGIJI4.js.map
package/dist/index.js CHANGED
@@ -81,17 +81,17 @@ try {
81
81
  await (await import('./serve.js')).serve();
82
82
  break;
83
83
  case "update": {
84
- const { runUpdateCommand } = await import('./cli-KSKENGIR.js');
84
+ const { runUpdateCommand } = await import('./cli-VBJGIJI4.js');
85
85
  process.exit(await runUpdateCommand(command));
86
86
  break;
87
87
  }
88
88
  case "desktop": {
89
- const { runDesktopCommand } = await import('./cli-3KKS7BE4.js');
89
+ const { runDesktopCommand } = await import('./cli-GMRPOISO.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-3KKS7BE4.js');
94
+ const { runOpenCommand } = await import('./cli-GMRPOISO.js');
95
95
  process.exit(await runOpenCommand(fileURLToPath(import.meta.url)));
96
96
  break;
97
97
  }
package/dist/serve.js CHANGED
@@ -1,6 +1,6 @@
1
- import { createUpdateChecker, classify, isReleaseTriplet, findNpm, stageVersion } from './chunk-FYQ5BAFA.js';
1
+ import { createUpdateChecker, classify, findNpm, isReleaseTriplet, stageVersion } from './chunk-YK37H6DM.js';
2
2
  import { shouldAdoptRunning, anotherScenriLines, portBusyLines } from './chunk-CMLJGPYC.js';
3
- import { SchemaTooNewError, createCore, BUDGET_EXHAUSTED, SpendCapError, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, ratioLabel, searchTerms, termMatches, budgetSize, SCHEMA_VERSION, ASPECT_TOLERANCE } from './chunk-UUYMUOZQ.js';
3
+ import { SchemaTooNewError, createCore, BUDGET_EXHAUSTED, SpendCapError, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, ratioLabel, searchTerms, termMatches, budgetSize, SCHEMA_VERSION, ASPECT_TOLERANCE } from './chunk-K6IELFL3.js';
4
4
  import { detectInstallKind } from './chunk-PAIAXRAC.js';
5
5
  export { detectInstallKind } from './chunk-PAIAXRAC.js';
6
6
  import { readMeta, repoSlug } from './chunk-Y3ZPBPLP.js';
@@ -1287,12 +1287,12 @@ function createCodexAnalyzer(opts = {}) {
1287
1287
  };
1288
1288
  }
1289
1289
  function buildPrompt(req, refCount, problems) {
1290
- const evidence = refCount === 0 ? "You have no reference images. Work from the description alone." : refCount === 1 ? "One reference image is attached." : `${refCount} reference images are attached.`;
1290
+ const evidence2 = refCount === 0 ? "You have no reference images. Work from the description alone." : refCount === 1 ? "One reference image is attached." : `${refCount} reference images are attached.`;
1291
1291
  const body = req.kind === "presenter" ? presenterBody(req, refCount) : sceneBody(req);
1292
1292
  const revision = req.priorDraft ? ` You are revising an existing record, not starting over: keep everything that is not being corrected. Current record: ${JSON.stringify(req.priorDraft)}.` : "";
1293
1293
  const correction = req.correction ? ` The correction to apply: ${req.correction}.` : "";
1294
1294
  const retry = problems.length ? ` Your last answer was rejected: ${problems.join(" ")} Fix exactly that and write the file again.` : "";
1295
- return `${evidence}${revision}${correction} ${body} Write strict JSON, and nothing but JSON, to a file called ${OUT_FILE} in the current directory (you may run the commands needed to write it). Do not browse the web or explore files. No prose, no markdown fences, no commentary.${retry}`;
1295
+ return `${evidence2}${revision}${correction} ${body} Write strict JSON, and nothing but JSON, to a file called ${OUT_FILE} in the current directory (you may run the commands needed to write it). Do not browse the web or explore files. No prose, no markdown fences, no commentary.${retry}`;
1296
1296
  }
1297
1297
  function presenterBody(req, refCount) {
1298
1298
  const same = refCount > 1 ? " Every attached photograph is the same person." : "";
@@ -3248,7 +3248,7 @@ function compileBrief(brief, ctx) {
3248
3248
  );
3249
3249
  if (names.length) {
3250
3250
  warnings.push(
3251
- `${names.join(" and ")} ${names.length === 1 ? "was" : "were"} left out \u2014 ${ctx.engineCaps.displayName} reads no reference images.`
3251
+ `${names.join(" and ")} ${names.length === 1 ? "was" : "were"} left out: ${ctx.engineCaps.displayName} reads no reference images.`
3252
3252
  );
3253
3253
  }
3254
3254
  }
@@ -5207,18 +5207,18 @@ function mergeScrape(existing, scraped) {
5207
5207
  async function inspectMark(buf, toPng2) {
5208
5208
  const png = await toPng2(buf);
5209
5209
  const meta = await sharp21(png).metadata();
5210
- let blank = false;
5210
+ let blank2 = false;
5211
5211
  try {
5212
5212
  const flat = await sharp21(png).flatten({ background: "#ffffff" }).toBuffer();
5213
5213
  const stats = await sharp21(flat).stats();
5214
- blank = stats.channels.slice(0, 3).every((c) => c.min > 200);
5214
+ blank2 = stats.channels.slice(0, 3).every((c) => c.min > 200);
5215
5215
  } catch {
5216
5216
  }
5217
5217
  return {
5218
5218
  longEdge: Math.max(meta.width ?? 0, meta.height ?? 0) || null,
5219
5219
  width: meta.width ?? null,
5220
5220
  height: meta.height ?? null,
5221
- blank
5221
+ blank: blank2
5222
5222
  };
5223
5223
  }
5224
5224
 
@@ -11287,7 +11287,7 @@ function defaultShowcaseDir() {
11287
11287
 
11288
11288
  // src/routes/showcase.ts
11289
11289
  function registerShowcaseRoutes(app, deps) {
11290
- const { templatesRoot } = deps;
11290
+ const { templatesRoot, thumbs } = deps;
11291
11291
  const { showcase } = loadShowcase(join(templatesRoot, "showcase"));
11292
11292
  const showcaseHeroPath = (id) => contentFile(templatesRoot, "previews", "showcase", `${id}.jpg`);
11293
11293
  const decorateShowcase = (s) => ({
@@ -11301,7 +11301,8 @@ function registerShowcaseRoutes(app, deps) {
11301
11301
  app.get("/api/showcase-previews/:file", async (req, reply) => {
11302
11302
  const m = /^([a-z0-9-]+)\.jpg$/.exec(String(req.params.file));
11303
11303
  if (!m || !existsSync(showcaseHeroPath(m[1]))) return reply.status(404).send({ error: "no preview" });
11304
- return serveJpeg(req, reply, showcaseHeroPath(m[1]));
11304
+ const path = showcaseHeroPath(m[1]);
11305
+ return serveJpegSized(req, reply, path, thumbs, fileKey("showcase", m[1], path));
11305
11306
  });
11306
11307
  }
11307
11308
 
@@ -11703,6 +11704,39 @@ function registerImageRoutes(app, deps) {
11703
11704
 
11704
11705
  // src/release/notes.data.ts
11705
11706
  var RELEASES = [
11707
+ {
11708
+ version: "0.11.0",
11709
+ date: "2026-09-18",
11710
+ title: "A new top bar, and a tutor that makes your first shot with you.",
11711
+ sections: [
11712
+ {
11713
+ heading: "Top bar",
11714
+ body: "The five places sit in the middle of the bar, with the one you are on underlined. New opens Create with the add panel open, and its arrow adds a product, presenter or scene instead. Activity replaces the bell: while a shot renders it shows that picture inside a filling ring, and its panel lists what is in progress and your notifications together, with Mark all read and Clear all. On a phone every panel in the bar opens as a sheet."
11715
+ },
11716
+ {
11717
+ heading: "Brand",
11718
+ body: "The brand menu is one short list, the brand you are in first and the rest A to Z, with a search once it grows long, so Settings and Shut down stay in reach. A brand's circle shows its icon, or its initial, never a shrunken wordmark."
11719
+ },
11720
+ {
11721
+ heading: "Learn",
11722
+ body: "A new install is welcomed and makes its first shot with a tutor, one step at a time: a product, a presenter, a scene, then the words. First steps on Home lists four lessons to start with and leaves once they are done, and Learn holds all five, from adding your product to refining a shot. Help sits in the corner of the page from 1024 pixels wide and in the bar below that, with Learn, the welcome, keyboard shortcuts and What's new."
11723
+ },
11724
+ {
11725
+ heading: "Fixes",
11726
+ body: "A product made from the picker goes into the brief by its name instead of as a missing product, and Enter on a focused control in Create presses that control rather than opening the selected shot."
11727
+ }
11728
+ ]
11729
+ },
11730
+ {
11731
+ version: "0.10.3",
11732
+ date: "2026-09-18",
11733
+ sections: [
11734
+ {
11735
+ heading: "Fixes",
11736
+ body: "Scenri no longer stops answering for a moment right after the studio opens, while it checks whether one-click updates can run here. On Windows that pause could last several seconds, long enough for a second click on the desktop icon to start Scenri again instead of opening the one already running."
11737
+ }
11738
+ ]
11739
+ },
11706
11740
  {
11707
11741
  version: "0.10.2",
11708
11742
  date: "2026-09-17",
@@ -12530,14 +12564,22 @@ function registerUpdateRoutes(app, deps) {
12530
12564
  return applyState;
12531
12565
  };
12532
12566
  const REQUIRED_PROTOCOL = 1;
12533
- let npmProbe = null;
12567
+ let npmFound = false;
12568
+ let npmAsked = null;
12569
+ const npmKnown = () => {
12570
+ if (runtime.installKind === "dev" || !runtime.supervised || deps.stageImpl) return Promise.resolve();
12571
+ npmAsked ??= findNpm().then((argv) => {
12572
+ npmFound = argv !== null;
12573
+ }).catch(() => {
12574
+ });
12575
+ return npmAsked;
12576
+ };
12534
12577
  const blockReason = () => {
12535
12578
  if (runtime.installKind === "dev") return "dev";
12536
12579
  if (!runtime.supervised) return "unsupervised";
12537
12580
  if ((runtime.launcherProtocol ?? 1) < REQUIRED_PROTOCOL) return "launcher-too-old";
12538
12581
  if (deps.stageImpl) return null;
12539
- npmProbe ??= findNpm() !== null;
12540
- return npmProbe ? null : "no-npm";
12582
+ return npmFound ? null : "no-npm";
12541
12583
  };
12542
12584
  const startStaging = (target) => {
12543
12585
  applyState = { phase: "staging", version: target, error: null };
@@ -12565,12 +12607,13 @@ function registerUpdateRoutes(app, deps) {
12565
12607
  };
12566
12608
  updates.onResult(maybeAutoStage);
12567
12609
  const bootLook = setTimeout(() => {
12568
- updates.check().then(maybeAutoStage).catch(() => {
12610
+ npmKnown().then(() => updates.check()).then(maybeAutoStage).catch(() => {
12569
12611
  });
12570
12612
  }, deps.bootLookMs ?? 15e3);
12571
12613
  bootLook.unref();
12572
12614
  app.addHook("onClose", async () => clearTimeout(bootLook));
12573
12615
  const updateStatus = async (force = false) => {
12616
+ await npmKnown();
12574
12617
  const r = await updates.check(force);
12575
12618
  const kind = r.latest ? classify(meta.version, r.latest) : null;
12576
12619
  const apply = effectiveApply();
@@ -12601,6 +12644,7 @@ function registerUpdateRoutes(app, deps) {
12601
12644
  if (busy > 0) {
12602
12645
  return reply.status(409).send({ error: `work is still running (${busy} task${busy === 1 ? "" : "s"})` });
12603
12646
  }
12647
+ await npmKnown();
12604
12648
  const block = blockReason();
12605
12649
  if (block)
12606
12650
  return reply.status(409).send({ error: `one-click update cannot run here (${block})`, blockReason: block });
@@ -12745,6 +12789,165 @@ function registerSystemRoutes(app, deps) {
12745
12789
  });
12746
12790
  }
12747
12791
 
12792
+ // src/routes/guide.ts
12793
+ var TASKS = ["first-shot", "refine", "product", "presenter", "scene"];
12794
+ var MILESTONES = ["shot", "refine", "product", "presenter", "scene"];
12795
+ var KEY = "guide";
12796
+ var isTask = (t) => TASKS.includes(t);
12797
+ var isMilestone = (m) => MILESTONES.includes(m);
12798
+ var num2 = (n) => typeof n === "number" && Number.isFinite(n) && n >= 0 ? n : 0;
12799
+ function blank(eligible) {
12800
+ return { v: 2, eligible, welcome: null, hidden: null, done: {}, dismissed: [], active: null };
12801
+ }
12802
+ function parse2(raw) {
12803
+ if (!raw) return null;
12804
+ try {
12805
+ const j = JSON.parse(raw);
12806
+ if (typeof j.eligible !== "boolean") return null;
12807
+ if (j.v !== 2) return null;
12808
+ const done = {};
12809
+ if (j.done && typeof j.done === "object") {
12810
+ for (const [k, at] of Object.entries(j.done)) if (isMilestone(k) && typeof at === "string") done[k] = at;
12811
+ }
12812
+ const a = j.active;
12813
+ const b = a?.baseline;
12814
+ const active = a && isTask(a.task) && typeof a.brandId === "string" && typeof a.since === "string" ? {
12815
+ task: a.task,
12816
+ brandId: a.brandId,
12817
+ since: a.since,
12818
+ baseline: { products: num2(b?.products), presenters: num2(b?.presenters), scenes: num2(b?.scenes) },
12819
+ ...a.paused === true ? { paused: true } : {}
12820
+ } : null;
12821
+ return {
12822
+ v: 2,
12823
+ eligible: j.eligible,
12824
+ welcome: j.welcome === "taken" || j.welcome === "declined" ? j.welcome : null,
12825
+ hidden: typeof j.hidden === "boolean" ? j.hidden : null,
12826
+ done,
12827
+ dismissed: Array.isArray(j.dismissed) ? j.dismissed.filter(isTask) : [],
12828
+ active
12829
+ };
12830
+ } catch {
12831
+ return null;
12832
+ }
12833
+ }
12834
+ function write(store, r) {
12835
+ store.setSetting(KEY, JSON.stringify(r));
12836
+ }
12837
+ function stampGuide(store, version) {
12838
+ if (store.getSetting(KEY) !== null) return;
12839
+ const eligible = store.listBrands().length === 0;
12840
+ if (eligible) {
12841
+ if (!store.getSetting("install.firstVersion")) store.setSetting("install.firstVersion", version);
12842
+ const seen = store.getSetting("whatsnew.seen");
12843
+ if (!seen || compareSemver(seen, version) < 0) store.setSetting("whatsnew.seen", version);
12844
+ }
12845
+ write(store, blank(eligible));
12846
+ }
12847
+ function countsOf(core, brandId) {
12848
+ const brand = core.store.getBrand(brandId);
12849
+ if (!brand) return null;
12850
+ const json = brand.json;
12851
+ const characters = Array.isArray(json?.characters) ? json.characters : [];
12852
+ return {
12853
+ presenters: characters.filter((c) => c?.origin === "custom" && !c?.supersededBy).length,
12854
+ scenes: Array.isArray(json?.scenes) ? json.scenes.length : 0,
12855
+ products: core.catalog.listLibraryProducts(brandId, json).length
12856
+ };
12857
+ }
12858
+ var ownPresenter = (b) => (b.json?.characters ?? []).some((c) => c?.origin === "custom" && !c?.supersededBy);
12859
+ function evidence(core, m) {
12860
+ const { store } = core;
12861
+ switch (m) {
12862
+ case "shot":
12863
+ return store.hasFinishedNode("generation");
12864
+ case "refine":
12865
+ return store.hasFinishedNode("edit");
12866
+ case "presenter":
12867
+ return store.listBrands().some(ownPresenter);
12868
+ case "scene":
12869
+ return store.listBrands().some((b) => (b.json?.scenes ?? []).length > 0);
12870
+ case "product":
12871
+ return store.hasCatalogProduct() || store.listBrands().some((b) => (b.json?.products ?? []).length > 0);
12872
+ }
12873
+ }
12874
+ function load2(store) {
12875
+ return parse2(store.getSetting(KEY)) ?? blank(false);
12876
+ }
12877
+ function readGuide(core, env) {
12878
+ const r = load2(core.store);
12879
+ let changed = false;
12880
+ const at = (/* @__PURE__ */ new Date()).toISOString();
12881
+ for (const m of MILESTONES) {
12882
+ if (!r.done[m] && evidence(core, m)) {
12883
+ r.done[m] = at;
12884
+ changed = true;
12885
+ }
12886
+ }
12887
+ if (r.active && !core.store.getBrand(r.active.brandId)) {
12888
+ r.active = null;
12889
+ changed = true;
12890
+ }
12891
+ if (changed) write(core.store, r);
12892
+ const a = r.active;
12893
+ const nodeKind = a?.task === "first-shot" ? "generation" : a?.task === "refine" ? "edit" : null;
12894
+ const eligible = r.eligible && env.SCENRI_NO_GUIDE !== "1";
12895
+ return {
12896
+ eligible,
12897
+ welcome: r.welcome,
12898
+ hidden: r.hidden ?? !eligible,
12899
+ done: r.done,
12900
+ dismissed: r.dismissed,
12901
+ active: a,
12902
+ activeNodes: a && nodeKind ? core.store.nodesSince(a.brandId, nodeKind, a.since) : [],
12903
+ activeDraftId: a?.task === "presenter" ? core.store.presenterDraftSince(a.brandId, a.since) : null,
12904
+ counts: a ? countsOf(core, a.brandId) : null
12905
+ };
12906
+ }
12907
+ function applyIntent(core, body) {
12908
+ const b = body ?? {};
12909
+ const r = load2(core.store);
12910
+ if ("welcome" in b) {
12911
+ if (b.welcome !== "taken" && b.welcome !== "declined") return "welcome is taken or declined";
12912
+ r.welcome = b.welcome;
12913
+ } else if ("start" in b) {
12914
+ const s = b.start ?? {};
12915
+ if (!isTask(s.task)) return "unknown task";
12916
+ if (typeof s.brandId !== "string") return "brandId required";
12917
+ const baseline = countsOf(core, s.brandId);
12918
+ if (!baseline) return "brand not found";
12919
+ const held = r.active;
12920
+ if (held && held.paused && held.task === s.task && held.brandId === s.brandId) {
12921
+ const { paused: _, ...going } = held;
12922
+ r.active = going;
12923
+ } else r.active = { task: s.task, brandId: s.brandId, since: core.store.now(), baseline };
12924
+ r.dismissed = r.dismissed.filter((t) => t !== s.task);
12925
+ } else if ("finish" in b) {
12926
+ if (!isTask(b.finish)) return "unknown task";
12927
+ if (r.active?.task === b.finish) r.active = null;
12928
+ } else if ("dismiss" in b) {
12929
+ if (!isTask(b.dismiss)) return "unknown task";
12930
+ if (!r.dismissed.includes(b.dismiss)) r.dismissed.push(b.dismiss);
12931
+ if (r.active?.task === b.dismiss) r.active = { ...r.active, paused: true };
12932
+ } else if ("hidden" in b) {
12933
+ if (typeof b.hidden !== "boolean") return "hidden is true or false";
12934
+ r.hidden = b.hidden;
12935
+ } else return "unknown intent";
12936
+ write(core.store, r);
12937
+ return null;
12938
+ }
12939
+ function registerGuideRoutes(app, deps) {
12940
+ const { core } = deps;
12941
+ const env = deps.env ?? process.env;
12942
+ stampGuide(core.store, deps.version);
12943
+ app.get("/api/guide", async () => readGuide(core, env));
12944
+ app.post("/api/guide", async (req, reply) => {
12945
+ const error = applyIntent(core, req.body);
12946
+ if (error) return reply.status(400).send({ error });
12947
+ return readGuide(core, env);
12948
+ });
12949
+ }
12950
+
12748
12951
  // src/routes/desktop.ts
12749
12952
  function registerDesktopRoutes(app, deps) {
12750
12953
  const { core, runtime } = deps;
@@ -12759,7 +12962,7 @@ function registerDesktopRoutes(app, deps) {
12759
12962
  record: null
12760
12963
  };
12761
12964
  }
12762
- const { installDeps } = await import('./cli-3KKS7BE4.js');
12965
+ const { installDeps } = await import('./cli-GMRPOISO.js');
12763
12966
  const { desktopStatus } = await import('./install-VXJPLYWM.js');
12764
12967
  return desktopStatus(installDeps(runtime.entry));
12765
12968
  });
@@ -12767,7 +12970,7 @@ function registerDesktopRoutes(app, deps) {
12767
12970
  if (!runtime.entry) {
12768
12971
  return { ok: false, reason: "unsupported", message: "Desktop shortcuts are not available on this system yet." };
12769
12972
  }
12770
- const { addToDesktop } = await import('./cli-3KKS7BE4.js');
12973
+ const { addToDesktop } = await import('./cli-GMRPOISO.js');
12771
12974
  return addToDesktop(runtime.entry);
12772
12975
  });
12773
12976
  app.get("/api/desktop", async () => {
@@ -13060,7 +13263,7 @@ function buildServer(opts) {
13060
13263
  const { demoProducts } = loadDemoProducts(join(templatesRoot, "demo-products"));
13061
13264
  const demoProductById = demoProductResolver(demoProducts);
13062
13265
  registerDemoProductRoutes(app, { templatesRoot, demoProducts, demoProductById, thumbs });
13063
- registerShowcaseRoutes(app, { templatesRoot });
13266
+ registerShowcaseRoutes(app, { templatesRoot, thumbs });
13064
13267
  app.get("/api/formats", async () => FORMATS);
13065
13268
  function briefInputsOnly(brief) {
13066
13269
  const {
@@ -14240,6 +14443,7 @@ function buildServer(opts) {
14240
14443
  return drained;
14241
14444
  });
14242
14445
  registerSystemRoutes(app, { core, thumbs });
14446
+ registerGuideRoutes(app, { core, version: meta.version });
14243
14447
  registerDesktopRoutes(app, {
14244
14448
  core,
14245
14449
  runtime,
@@ -14407,7 +14611,7 @@ async function run() {
14407
14611
  }
14408
14612
  }
14409
14613
  const ownEntry = fileURLToPath(import.meta.url);
14410
- const { addToDesktop, installDeps } = await import('./cli-3KKS7BE4.js');
14614
+ const { addToDesktop, installDeps } = await import('./cli-GMRPOISO.js');
14411
14615
  const { refreshLauncher } = await import('./refresh-5HBDLLLB.js');
14412
14616
  const { askOnTerminal, offerDesktop, shouldOfferDesktop } = await import('./offer-W6NJXMWT.js');
14413
14617
  const { launcherInstalled } = await import('./paths-6ZXRZUHY.js');
@@ -1,3 +1,3 @@
1
- export { ASPECT_TOLERANCE, BUDGET_EXHAUSTED, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, SCHEMA_VERSION, STEM_MIN, SchemaTooNewError, SpendCapError, TRIGRAM_MIN, budgetSize, createCatalogStore, createCore, createStore, defaultHome, fold, ftsMatch, matchesQuery, ratioLabel, searchTerms, termMatches, uniqueProjectSlug, uniqueSetSlug, uniqueSlug } from './chunk-UUYMUOZQ.js';
2
- //# sourceMappingURL=src-QZZDZM7I.js.map
3
- //# sourceMappingURL=src-QZZDZM7I.js.map
1
+ export { ASPECT_TOLERANCE, BUDGET_EXHAUSTED, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, SCHEMA_VERSION, STEM_MIN, SchemaTooNewError, SpendCapError, TRIGRAM_MIN, budgetSize, createCatalogStore, createCore, createStore, defaultHome, fold, ftsMatch, matchesQuery, ratioLabel, searchTerms, termMatches, uniqueProjectSlug, uniqueSetSlug, uniqueSlug } from './chunk-K6IELFL3.js';
2
+ //# sourceMappingURL=src-TC4UQ7H6.js.map
3
+ //# sourceMappingURL=src-TC4UQ7H6.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenri",
3
- "version": "0.10.2",
3
+ "version": "0.11.0",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",