scenri 0.3.3 → 0.3.5

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,29 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.5](https://github.com/tonygorb/Scenri/compare/v0.3.4...v0.3.5) (2026-08-20)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * keep untrusted text out of the Windows shell lines ([f564c35](https://github.com/tonygorb/Scenri/commit/f564c358dc018f2e7e52e00eae004122690dc2be))
9
+ * make the Codex engine and updates work on Windows ([f94f420](https://github.com/tonygorb/Scenri/commit/f94f42069049b65920dfc95491a074cfd3617b8f))
10
+ * make the Codex engine and updates work on Windows ([f21cb9d](https://github.com/tonygorb/Scenri/commit/f21cb9d513f291bfc64585f9f3a3f67ea5738bde))
11
+
12
+
13
+ ### Performance Improvements
14
+
15
+ * cut the e2e wall clock roughly in half ([2cacbce](https://github.com/tonygorb/Scenri/commit/2cacbced34b6c58541c0b7942cd107aaa234f477))
16
+
17
+ ## [0.3.4](https://github.com/tonygorb/Scenri/compare/v0.3.3...v0.3.4) (2026-08-20)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * presenter references are identity, not wardrobe ([dc372fd](https://github.com/tonygorb/Scenri/commit/dc372fd40d7e15d7d8dc72b5ec8f41993e078ae9))
23
+ * presenter references are identity, not wardrobe ([c8bca43](https://github.com/tonygorb/Scenri/commit/c8bca431730a2d4c41d2783fb6eba2c564af5141))
24
+ * stop custom presenter and scene pages freezing the whole app ([fcf88dd](https://github.com/tonygorb/Scenri/commit/fcf88ddbcd57d01354c833cf9f8456fa1b0b0434))
25
+ * stop custom presenter and scene pages freezing the whole app ([8595078](https://github.com/tonygorb/Scenri/commit/859507893892cb23a7ff2f132f273db4dbd0a0f9))
26
+
3
27
  ## [0.3.3](https://github.com/tonygorb/Scenri/compare/v0.3.2...v0.3.3) (2026-08-20)
4
28
 
5
29
 
@@ -1,6 +1,6 @@
1
1
  import { stagingDir, versionsDir, isValidVersionDir, pruneStaged, entryOf } from './chunk-4MAFHYAD.js';
2
2
  import { spawnSync, spawn } from 'child_process';
3
- import { rmSync, mkdirSync, readFileSync, renameSync } from 'fs';
3
+ import { existsSync, rmSync, mkdirSync, readFileSync, renameSync } from 'fs';
4
4
  import { join, dirname } from 'path';
5
5
 
6
6
  // src/update/check.ts
@@ -97,6 +97,7 @@ function createUpdateChecker(deps) {
97
97
  }
98
98
  function findNpm(opts = {}) {
99
99
  const env = opts.env ?? process.env;
100
+ const platform = opts.platform ?? process.platform;
100
101
  const canRun = opts.canRun ?? ((argv) => {
101
102
  try {
102
103
  return spawnSync(argv[0], [...argv.slice(1), "--version"], { stdio: "ignore", timeout: 1e4 }).status === 0;
@@ -104,9 +105,26 @@ function findNpm(opts = {}) {
104
105
  return false;
105
106
  }
106
107
  });
107
- if (canRun(["npm"])) return ["npm"];
108
- const ep = env.npm_execpath;
108
+ if (platform !== "win32" && canRun(["npm"])) return ["npm"];
109
+ const ep = env.npm_execpath?.replace(/npx-cli\.js$/, "npm-cli.js");
109
110
  if (ep && /npm-cli\.js$/.test(ep) && canRun([process.execPath, ep])) return [process.execPath, ep];
111
+ if (platform === "win32") {
112
+ const cli = windowsNpmCli();
113
+ if (cli && canRun([process.execPath, cli])) return [process.execPath, cli];
114
+ }
115
+ return null;
116
+ }
117
+ function windowsNpmCli() {
118
+ try {
119
+ const out = spawnSync("where", ["npm"], { encoding: "utf8", timeout: 1e4 });
120
+ if (out.status !== 0 || !out.stdout) return null;
121
+ for (const line of out.stdout.split(/\r?\n/)) {
122
+ if (!/npm(\.cmd)?$/i.test(line.trim())) continue;
123
+ const cli = join(dirname(line.trim()), "node_modules", "npm", "bin", "npm-cli.js");
124
+ if (existsSync(cli)) return cli;
125
+ }
126
+ } catch {
127
+ }
110
128
  return null;
111
129
  }
112
130
  function run(argv) {
@@ -202,5 +220,5 @@ async function stageVersion(deps) {
202
220
  }
203
221
 
204
222
  export { classify, createUpdateChecker, fetchDistTagLatest, findNpm, stageVersion };
205
- //# sourceMappingURL=chunk-GA7P7K2M.js.map
206
- //# sourceMappingURL=chunk-GA7P7K2M.js.map
223
+ //# sourceMappingURL=chunk-3W2RHBZC.js.map
224
+ //# sourceMappingURL=chunk-3W2RHBZC.js.map
@@ -1,4 +1,4 @@
1
- import { stageVersion, fetchDistTagLatest, findNpm } from './chunk-GA7P7K2M.js';
1
+ import { stageVersion, fetchDistTagLatest, findNpm } from './chunk-3W2RHBZC.js';
2
2
  import { readMeta } from './chunk-Y3ZPBPLP.js';
3
3
  import { defaultHome, newestStaged, compareSemver } from './chunk-4MAFHYAD.js';
4
4
 
@@ -43,7 +43,8 @@ async function runUpdateCommand(opts) {
43
43
  }
44
44
  if (!findNpm()) {
45
45
  console.error(" npm is not reachable from here, so nothing can be downloaded.");
46
- console.error(" Install Node.js with npm, or run: npx scenri@latest");
46
+ console.error(" Run the newest version directly with: npx scenri@latest");
47
+ console.error(" (or install Node.js with npm and try again)");
47
48
  return 1;
48
49
  }
49
50
  console.log(` downloading ${meta.name} ${latest}\u2026`);
@@ -62,5 +63,5 @@ async function runUpdateCommand(opts) {
62
63
  }
63
64
 
64
65
  export { runUpdateCommand };
65
- //# sourceMappingURL=cli-GQLRYYGU.js.map
66
- //# sourceMappingURL=cli-GQLRYYGU.js.map
66
+ //# sourceMappingURL=cli-HF5KPURA.js.map
67
+ //# sourceMappingURL=cli-HF5KPURA.js.map
package/dist/index.js CHANGED
@@ -64,7 +64,7 @@ try {
64
64
  await (await import('./serve.js')).serve();
65
65
  break;
66
66
  case "update": {
67
- const { runUpdateCommand } = await import('./cli-GQLRYYGU.js');
67
+ const { runUpdateCommand } = await import('./cli-HF5KPURA.js');
68
68
  process.exit(await runUpdateCommand(command));
69
69
  break;
70
70
  }
package/dist/serve.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { portBusyLines } from './chunk-WGIZJXNE.js';
2
- import { createUpdateChecker, classify, stageVersion, findNpm } from './chunk-GA7P7K2M.js';
2
+ import { createUpdateChecker, classify, stageVersion, findNpm } from './chunk-3W2RHBZC.js';
3
3
  import { readMeta, repoSlug } from './chunk-Y3ZPBPLP.js';
4
4
  import { newestStaged, compareSemver } from './chunk-4MAFHYAD.js';
5
5
  import { networkInterfaces, homedir, tmpdir } from 'os';
@@ -1271,7 +1271,7 @@ function createCatalogStore(db) {
1271
1271
  // ../core/src/engine.ts
1272
1272
  var REFERENCE_ROLE_DIRECTIVE = {
1273
1273
  product: "the exact product \u2014 preserve its label, shape, colors and design faithfully; do not redesign it",
1274
- character: "the exact person \u2014 preserve their face, hair and build faithfully; do not restyle them",
1274
+ character: "the exact person \u2014 match their face, facial structure, skin, hair and build exactly; their clothing, pose and background are capture context, not styling to reproduce",
1275
1275
  brand: "the brand's own mark \u2014 if the direction calls for the mark to appear, reproduce it exactly as drawn, same colours, letterforms and proportions; otherwise take only its colour and treatment, and never its subject, geometry or composition",
1276
1276
  scene: "a reference for the environment and light only \u2014 take no subject, product or person from it",
1277
1277
  composition: "a reference for framing, camera angle and pose only \u2014 take no subject, color, material or branding from it",
@@ -1280,7 +1280,7 @@ var REFERENCE_ROLE_DIRECTIVE = {
1280
1280
  };
1281
1281
  var EDIT_REFERENCE_ROLE_DIRECTIVE = {
1282
1282
  product: "the exact product: keep or restore its label, shape and design faithfully",
1283
- character: "the exact person: keep their face, hair and build faithfully",
1283
+ character: "the exact person: keep their face, facial structure, skin, hair and build faithfully; take no clothing, pose or background from this reference, and keep the source image's existing outfit unless the instruction changes it",
1284
1284
  brand: "the brand's own mark: reproduce it exactly as drawn wherever it appears, never redrawn or re-lettered",
1285
1285
  scene: "a reference for environment and light only",
1286
1286
  composition: "a reference for framing and pose only",
@@ -1857,11 +1857,17 @@ function execArgs(dir, promptText, effort = "low") {
1857
1857
  function createRunner(opts = {}) {
1858
1858
  const spawnImpl = opts.spawnImpl ?? spawn;
1859
1859
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
1860
+ const platform = opts.platform ?? process.platform;
1861
+ const winArg = (a) => `"${a.replace(/[\r\n]+/g, " ").replace(/"/g, "'").replace(/%/g, " percent ")}"`;
1862
+ const spawnCodex = (args) => platform === "win32" ? spawnImpl(["codex", ...args].map(winArg).join(" "), [], {
1863
+ stdio: ["ignore", "pipe", "pipe"],
1864
+ shell: true
1865
+ }) : spawnImpl("codex", args, { stdio: ["ignore", "pipe", "pipe"] });
1860
1866
  function run2(args, signal) {
1861
1867
  return new Promise((resolve, reject) => {
1862
1868
  let child;
1863
1869
  try {
1864
- child = spawnImpl("codex", args, { stdio: ["ignore", "pipe", "pipe"] });
1870
+ child = spawnCodex(args);
1865
1871
  } catch (err) {
1866
1872
  reject(new Error(`Failed to spawn codex: ${err.message}`));
1867
1873
  return;
@@ -1930,7 +1936,7 @@ function createRunner(opts = {}) {
1930
1936
  };
1931
1937
  let child;
1932
1938
  try {
1933
- child = spawnImpl("codex", args, { stdio: ["ignore", "pipe", "pipe"] });
1939
+ child = spawnCodex(args);
1934
1940
  } catch {
1935
1941
  done(false);
1936
1942
  return;
@@ -1944,6 +1950,9 @@ function createRunner(opts = {}) {
1944
1950
  });
1945
1951
  }
1946
1952
  async function probe() {
1953
+ if (process.env.SCENRI_NO_CODEX === "1") {
1954
+ return { ok: false, reason: NOT_INSTALLED_REASON, code: "not-installed" };
1955
+ }
1947
1956
  if (!await exitedZero(["--version"])) {
1948
1957
  return { ok: false, reason: NOT_INSTALLED_REASON, code: "not-installed" };
1949
1958
  }
@@ -2105,6 +2114,7 @@ function stateFrom(avail) {
2105
2114
  }
2106
2115
  function createCodexSetup(opts = {}) {
2107
2116
  const spawnImpl = opts.spawnImpl ?? spawn;
2117
+ const platform = opts.platform ?? process.platform;
2108
2118
  const runner = createRunner(opts);
2109
2119
  const installTimeoutMs = opts.installTimeoutMs ?? DEFAULT_INSTALL_TIMEOUT_MS;
2110
2120
  function run2(cmd, args, timeoutMs) {
@@ -2123,7 +2133,7 @@ function createCodexSetup(opts = {}) {
2123
2133
  done({ code: null, stderr, spawnError: `${cmd} timed out after ${timeoutMs}ms` });
2124
2134
  }, timeoutMs);
2125
2135
  try {
2126
- child = spawnImpl(cmd, args, { stdio: ["ignore", "pipe", "pipe"], shell: process.platform === "win32" });
2136
+ child = spawnImpl(cmd, args, { stdio: ["ignore", "pipe", "pipe"], shell: platform === "win32" });
2127
2137
  } catch (err) {
2128
2138
  done({ code: null, stderr, spawnError: err.message });
2129
2139
  return;
@@ -2156,7 +2166,7 @@ function createCodexSetup(opts = {}) {
2156
2166
  }
2157
2167
  return { ok: true };
2158
2168
  }
2159
- if (process.platform !== "win32" && /EACCES|permission denied/i.test(res.stderr)) {
2169
+ if (platform !== "win32" && /EACCES|permission denied/i.test(res.stderr)) {
2160
2170
  return {
2161
2171
  ok: false,
2162
2172
  fallbackCommand: INSTALL_COMMAND_SUDO,
@@ -2487,7 +2497,14 @@ async function resolvePresenterImages(core, templatesRoot, presenter) {
2487
2497
  const hash = core.images.save(png);
2488
2498
  shots.push({ file: `asset:${hash}`, angle, locked: true });
2489
2499
  }
2490
- return shots.length ? { id: presenter.id, name: presenter.name, shots } : null;
2500
+ if (!shots.length) return null;
2501
+ return {
2502
+ id: presenter.id,
2503
+ name: presenter.name,
2504
+ ...presenter.identityNotes ? { identityNotes: presenter.identityNotes } : {},
2505
+ ...presenter.negativeConstraints?.length ? { negativeConstraints: presenter.negativeConstraints } : {},
2506
+ shots
2507
+ };
2491
2508
  }
2492
2509
  async function brandJsonWithResolvedPresenters(core, templatesRoot, presenters, brandJson, tokens) {
2493
2510
  const existing = brandJson?.characters ?? [];
@@ -2793,7 +2810,7 @@ function compileBrief(brief, ctx) {
2793
2810
  attachments.push({ role: "character", id: c.id, label: c.name, hash: chash, essential: i === 0 });
2794
2811
  });
2795
2812
  personDirectives.push(
2796
- "The attached person reference is the same person every time: hold their face, hair and build, and do not restyle them."
2813
+ "The attached person reference is the same person every time: match their face, facial structure, skin, hair and build exactly. Their outfit, pose, background and lighting are neutral studio capture conditions, not styling direction: dress and style them for this shot, to a commercial standard, following any wardrobe the direction itself specifies."
2797
2814
  );
2798
2815
  if (c.identityNotes) personDirectives.push(String(c.identityNotes));
2799
2816
  if (c.negativeConstraints?.length)
@@ -2891,10 +2908,14 @@ function compileBrief(brief, ctx) {
2891
2908
  const sceneCamera = inlineTemplates[0]?.camera?.trim() || ctx.template?.camera?.trim() || "";
2892
2909
  const cameraDirectives = sceneCamera && !shotSpecifiesCamera(sentence) ? [`Camera for this shot: ${sceneCamera}`] : [];
2893
2910
  const guard = scene ? sceneGuardDirectives({ hasProduct: !!productId, hasPerson }) : [];
2911
+ const pairDirectives = productId && hasPerson ? [
2912
+ "If the attached product is something a person wears, the presenter wears that exact product, with the rest of the outfit styled around it; otherwise the presenter presents or uses the product naturally."
2913
+ ] : [];
2894
2914
  const brandLines = brandRuleDirectives(ctx.brand);
2895
2915
  const allDirectives = [
2896
2916
  ...productDirectives,
2897
2917
  ...personDirectives,
2918
+ ...pairDirectives,
2898
2919
  ...otherDirectives,
2899
2920
  ...cameraDirectives,
2900
2921
  ...brandLines,
@@ -6524,6 +6545,30 @@ function registerImageRoutes(app, deps) {
6524
6545
 
6525
6546
  // src/release/notes.data.ts
6526
6547
  var RELEASES = [
6548
+ {
6549
+ version: "0.3.5",
6550
+ date: "2026-08-21",
6551
+ sections: [
6552
+ {
6553
+ heading: "Fixes",
6554
+ body: "Setting up the Codex engine on Windows no longer reports a successful install as missing, and updates can now find npm there. When npm truly is unreachable, the update command points to the one-line recovery instead of a dead end."
6555
+ }
6556
+ ]
6557
+ },
6558
+ {
6559
+ version: "0.3.4",
6560
+ date: "2026-08-21",
6561
+ sections: [
6562
+ {
6563
+ heading: "Presenters",
6564
+ body: "A presenter's reference photos now define who they are, not what they wear. The neutral studio outfit from their reference set no longer follows them into finished shots; wardrobe comes from the shot, the product and the scene instead, and refining an image keeps the outfit it already has."
6565
+ },
6566
+ {
6567
+ heading: "Fixes",
6568
+ body: "Opening a presenter or scene you built yourself no longer freezes the app."
6569
+ }
6570
+ ]
6571
+ },
6527
6572
  {
6528
6573
  version: "0.3.3",
6529
6574
  date: "2026-08-20",
@@ -6867,7 +6912,7 @@ function registerSystemRoutes(app, deps) {
6867
6912
  }
6868
6913
  core.close();
6869
6914
  for (const name of ["scenri.db", "scenri.db-wal", "scenri.db-shm", "images", "backups"]) {
6870
- rmSync(join(core.home, name), { recursive: true, force: true });
6915
+ rmSync(join(core.home, name), { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
6871
6916
  }
6872
6917
  return { ok: true, scope };
6873
6918
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenri",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",