scenri 0.8.2 → 0.9.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,112 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.0](https://github.com/tonygorb/Scenri/compare/v0.8.3...v0.9.0) (2026-09-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * **cli:** a desktop icon that starts Scenri without a terminal ([35e8160](https://github.com/tonygorb/Scenri/commit/35e816086b3d2e10b32e454b4f40e93f4c95082a))
9
+ * **cli:** desktop status, install and quit routes for Settings ([c8478e1](https://github.com/tonygorb/Scenri/commit/c8478e1b2059ba40c85779469e97251c5669152b))
10
+ * **cli:** install, repair, remove and report the desktop launcher on macOS and Windows ([ee03601](https://github.com/tonygorb/Scenri/commit/ee036017dd5be738aff40b1adf2b04b7c45548fc))
11
+ * **cli:** launcher support paths, rotating logs, and adopting the running install into app/versions ([abde8cc](https://github.com/tonygorb/Scenri/commit/abde8cc23a0dd7eccb1e55d4aada1603108b68da))
12
+ * **cli:** offer the desktop icon once at a terminal, and keep an installed one current on every start ([5f56bb9](https://github.com/tonygorb/Scenri/commit/5f56bb9a45432df605c295b8893638cd75c0a2a0))
13
+ * **cli:** scenri desktop and the hidden open command, with a console-less supervisor ([fbe3fcf](https://github.com/tonygorb/Scenri/commit/fbe3fcf7ef16573448c22cd222c0cd6a37ca909c))
14
+ * **cli:** scenri open reuses a running Scenri or starts the supervisor detached and waits for it ([713e4f8](https://github.com/tonygorb/Scenri/commit/713e4f8f12014ceeff36bcea23718b2aac19ccec))
15
+ * **cli:** ship the desktop launcher's icons, an icns and an ico cut from the brand mark ([ef104e2](https://github.com/tonygorb/Scenri/commit/ef104e28654c31350dd103850a22dfa34f54b26e))
16
+ * **cli:** the frozen desktop bootstrap and the starting page ([151b2b9](https://github.com/tonygorb/Scenri/commit/151b2b92445a5f6b7602bedb4cc07c7131a85d61))
17
+ * **studio:** after Shut down the tab closes instead of showing a card ([42bdf89](https://github.com/tonygorb/Scenri/commit/42bdf896d9e6ed7a4cfc98e21b201372097e3c8c))
18
+ * **studio:** Desktop shortcut and Quit rows in About, and a verify hop before a build is adopted ([1587567](https://github.com/tonygorb/Scenri/commit/15875670bb5c3810437f19393440ecbffe51e68d))
19
+ * **studio:** Reuse setup is back under the record, beside Try again ([abe6b03](https://github.com/tonygorb/Scenri/commit/abe6b034490733701b6c990eb6688f0147dd81bc))
20
+ * **studio:** Reuse setup is back, and the shot sidebar and colour chips line up ([13345f2](https://github.com/tonygorb/Scenri/commit/13345f2a24d46cf3ae3f7c7d47ecd6c8a24a6665))
21
+ * **studio:** Shut down Scenri lives at the bottom of the brand menu, not in a Settings pane ([d21914b](https://github.com/tonygorb/Scenri/commit/d21914b9dd1b4e712120e1d1c42208e48f8833fa))
22
+
23
+
24
+ ### Bug Fixes
25
+
26
+ * **cli:** a browser that cannot be opened gets the address in a dialog instead of silence ([ef3d6a1](https://github.com/tonygorb/Scenri/commit/ef3d6a11a963611aa73ff025630bf55fc1a92fa5))
27
+ * **cli:** a double-click answers at once, and every click stops paying for node probing ([25685e7](https://github.com/tonygorb/Scenri/commit/25685e7b8087bb1f794b1b5d30cd1252b4ea9b32))
28
+ * **cli:** scenri open logs the sentence it shows, so support reads the log instead of the screen ([b52350c](https://github.com/tonygorb/Scenri/commit/b52350ce16cd67dc8a0f8931d3f7db71975ece07))
29
+ * **cli:** the boot refresh heals node-path and node-major against the record ([4d09985](https://github.com/tonygorb/Scenri/commit/4d09985accf05b9eacd238249a2c61c697710b07))
30
+ * **cli:** the rendered starting page is not a support file, so the boot refresh leaves it alone ([d3b1396](https://github.com/tonygorb/Scenri/commit/d3b13966b778c4c51a49922984f394ac9d0e146d))
31
+ * **cli:** the starting page carries the studio URL in a meta, because open(1) drops a file URL's fragment ([4fa6c68](https://github.com/tonygorb/Scenri/commit/4fa6c682d09c34de76ef00256fb1c0bdd8f84133))
32
+ * **cli:** when the recorded node is gone the icon prefers a node of the same major, and a native-module death says so ([aa1dba0](https://github.com/tonygorb/Scenri/commit/aa1dba06d0d48c2a6ff3c4adcc47409f31f69311))
33
+ * **studio:** a colour chip wears its colour as the thumbnail's circle, and its menu hugs the chip ([e5970c1](https://github.com/tonygorb/Scenri/commit/e5970c14116f9721e5596167b3e821bcd45908b3))
34
+ * **studio:** the lifecycle overlays cover a Settings dialog instead of hiding behind it ([1e89433](https://github.com/tonygorb/Scenri/commit/1e894337948f1feb4816a67584098e4658d63244))
35
+ * **studio:** the shot record clamps by rows, its release is a verb, and its colour chip is the thumbnail's circle ([20da7a1](https://github.com/tonygorb/Scenri/commit/20da7a154272004db5cd22e0185bc08452e0a132))
36
+ * **studio:** the shot sidebar's composer, hairlines and copy control share one inset ([d5c4a99](https://github.com/tonygorb/Scenri/commit/d5c4a99e9bcc1cc43fd8673547cfdb73923b0d8b))
37
+
38
+ ## [0.8.3](https://github.com/tonygorb/Scenri/compare/v0.8.2...v0.8.3) (2026-09-06)
39
+
40
+
41
+ ### Features
42
+
43
+ * **composer:** rebuild the attach picker as an add-to-shot surface ([35cf31b](https://github.com/tonygorb/Scenri/commit/35cf31bc43766f8b9144eedac1531c4c63415ce0))
44
+ * **core:** carry the root's whole history on a lineage answer ([e5eab8b](https://github.com/tonygorb/Scenri/commit/e5eab8b4536e67dedc9d1c70cf67555f3f726412))
45
+ * **studio:** a Keeping chip switches off and back on, and the preview follows ([372db20](https://github.com/tonygorb/Scenri/commit/372db20aa7b27448de134d3211d8cc756bfa5ba0))
46
+ * **studio:** a rail of the feed's roots beside the shot, and a strip that is the root's whole history ([c4a8c6d](https://github.com/tonygorb/Scenri/commit/c4a8c6d7b5aec772d2791ff36197c4ae2fafa5a7))
47
+ * **studio:** Carried over is what rode, and you decide what rides ([1ba1b5f](https://github.com/tonygorb/Scenri/commit/1ba1b5fc0514854e906e1d96891bcdafeb3508c6))
48
+ * **studio:** no zoom on the stage ([de7a965](https://github.com/tonygorb/Scenri/commit/de7a965d8de740bc800030116754809b9be0f0f0))
49
+ * **studio:** one-click download, no Compare, and a stage that never collapses between versions ([73c82ef](https://github.com/tonygorb/Scenri/commit/73c82ef54fd30eb6af78896e5862cfd7bfc1ac5d))
50
+ * **studio:** read a shot's history as a trail, the original and each refinement by number ([039fd8e](https://github.com/tonygorb/Scenri/commit/039fd8e4e506a6b76d6e64fc53602a2f9dcf5630))
51
+ * **studio:** review a shot with a feed rail, in-place zoom and true actions ([02544f9](https://github.com/tonygorb/Scenri/commit/02544f99b06cbaa5e30f2b4f11d579309facb100))
52
+ * **studio:** square trail tiles, and on a phone the trail is a scrubber ([0d674c7](https://github.com/tonygorb/Scenri/commit/0d674c7d7cf0e5317d877539a02883259028e8a0))
53
+ * **studio:** the picture answers a right click, the wheel zooms, the ring is the feed's ([1cde7a2](https://github.com/tonygorb/Scenri/commit/1cde7a21c21214b2f088ffa30bdba37564b4e25a))
54
+ * **studio:** the picture is a loupe ([074c37b](https://github.com/tonygorb/Scenri/commit/074c37b8dd8929e5bb728c2d5b4b555f2dd7f322))
55
+ * **studio:** the rail is a fisheye, tiles and gaps shrinking together toward the ends ([e78ed46](https://github.com/tonygorb/Scenri/commit/e78ed46bba1ecbb1a0aad1fb2774b20f1b6c025f))
56
+ * **studio:** the rail is flat, its ends dissolving through a mask, the ringed tile centred ([90d3e05](https://github.com/tonygorb/Scenri/commit/90d3e05f9f533c0f314244e307d8e73b164b4745))
57
+ * **studio:** the rail swells toward the middle and sits back at its ends, the shelf's own depth ([d2c70c4](https://github.com/tonygorb/Scenri/commit/d2c70c4d2a84927ddf12a08bfd02ca94b9af81a2))
58
+ * **studio:** the rail's swell is one curve, transparent at the ends and magnified at the centred shot ([4ab3bee](https://github.com/tonygorb/Scenri/commit/4ab3beeed137461644d27dbc86f5ef7d71ce6ce3))
59
+ * **studio:** the refine field carries no band; the record says what was carried ([f9d23bd](https://github.com/tonygorb/Scenri/commit/f9d23bdc463dd55eeb4ea9efd8db9e934bca6603))
60
+ * **studio:** the refine field says which picture it is about ([f58cfde](https://github.com/tonygorb/Scenri/commit/f58cfdedb3e1e783421cd776f05f12f216a030b1))
61
+ * **studio:** the trail is one line and the picture's own tiles, not a row of numerals ([809221e](https://github.com/tonygorb/Scenri/commit/809221e1721bb0143983640ece20f492ca9a8eee))
62
+ * **studio:** tooltips, true states and a zoom viewer for the shot's actions ([f07133c](https://github.com/tonygorb/Scenri/commit/f07133c321305b6a9c1d74f9f16801f2047ad4ce))
63
+ * **studio:** zoom the picture where it is, wheel through the feed, rail every shot ([552caaa](https://github.com/tonygorb/Scenri/commit/552caaa5f13fc63fa250e25d16551c9033230b1c))
64
+
65
+
66
+ ### Bug Fixes
67
+
68
+ * **brief:** a scene with nobody attached renders its set alone ([9c2c9cc](https://github.com/tonygorb/Scenri/commit/9c2c9cc6c1c263cd29c1db726440cb4f273e020c))
69
+ * **brief:** the empty role is decided by the compiler, and guarded after the camera note ([606a9d7](https://github.com/tonygorb/Scenri/commit/606a9d76888af92c840c27a1e105d26dd0834cb8))
70
+ * **composer:** a colour tile ticks and toggles like every other tile ([65dfdbe](https://github.com/tonygorb/Scenri/commit/65dfdbeca2e116c50a553b45e9c9928083b38ab0))
71
+ * **composer:** a pick in the picker no longer flashes, and a held tile can always be taken out ([a4c5846](https://github.com/tonygorb/Scenri/commit/a4c5846b6baf99b8e8cb4495066158de695d87c9))
72
+ * **composer:** Add logo is reachable from the keyboard ([2db04a7](https://github.com/tonygorb/Scenri/commit/2db04a762bd9ddfe168c2e3676ac4192bb27da81))
73
+ * **composer:** one composer height, one 510px frame, every tile state, and nothing re-deals on a pick ([2c254b0](https://github.com/tonygorb/Scenri/commit/2c254b006cc3912be7467cc2eca7629ff16fb51e))
74
+ * **composer:** one grid, one frame, tiles that toggle, paste to upload, and a phone that keeps the composer in view ([34ad69a](https://github.com/tonygorb/Scenri/commit/34ad69a9975b19fd69b7f069364cc21538e2349e))
75
+ * **composer:** size picker tiles for the face, not for the panel width ([9915d33](https://github.com/tonygorb/Scenri/commit/9915d338b99a6de6ddc36f98feb05b69b6fda015))
76
+ * **composer:** the Add color trigger is a ghost button, not a swatch ([ccee0d9](https://github.com/tonygorb/Scenri/commit/ccee0d915bb2c18ca91bf282fd1fc859de895349))
77
+ * **composer:** the make-one action sits in the group's own row, beside its name ([65f5b95](https://github.com/tonygorb/Scenri/commit/65f5b956386b32776d807f4f46583454541e855c))
78
+ * **composer:** the make-one verbs wear the quiet look Show all wears, so no row shifts between tabs ([3d1b3f2](https://github.com/tonygorb/Scenri/commit/3d1b3f223a9350b7a0f67953b5ec7c05d2949fe3))
79
+ * **composer:** the picker pages by scroll, not by a button ([b238f1c](https://github.com/tonygorb/Scenri/commit/b238f1cc1452b48023307ec94b162776ea29dda6))
80
+ * **composer:** the picker's head on the Figma row, and a phone head of its own ([2c74888](https://github.com/tonygorb/Scenri/commit/2c748882cbaeec8ff08fe207108717b92455a0fc))
81
+ * **composer:** the picker's Shots tab reaches every finished shot, a page at a time ([0b887e3](https://github.com/tonygorb/Scenri/commit/0b887e31d14cc4719d53fe369c55124771f36caa))
82
+ * **composer:** the picker's tiles take the rail's states ([dabfafe](https://github.com/tonygorb/Scenri/commit/dabfafe189adc9424669535bc4da67e381088d5f))
83
+ * **composer:** the search field can be left, and every tab can make one of its own ([4dca6a4](https://github.com/tonygorb/Scenri/commit/4dca6a47ca8d242106fa8089eb668baedd454892))
84
+ * **create:** the rail's Recent shots count and page the whole feed ([94aac66](https://github.com/tonygorb/Scenri/commit/94aac66cebecc3ddd9e976ef88b415e13f5abe96))
85
+ * **studio:** a drag close up pans, a step never flickers, a held key never skips ([df63c70](https://github.com/tonygorb/Scenri/commit/df63c70308eebec53b35d541050a53b2d3b109aa))
86
+ * **studio:** a Keeping chip peeks its picture on hover, the way every ingredient chip does ([137b8f0](https://github.com/tonygorb/Scenri/commit/137b8f075033a5169ff0893fc7216f2dacd52003))
87
+ * **studio:** a refinement's record is its ask, never the references it sent along ([349c6fc](https://github.com/tonygorb/Scenri/commit/349c6fc3d985b67c02491f45b81f2dec4f3477d4))
88
+ * **studio:** every preview card wears a tail, a breath away from its anchor ([ce30a3d](https://github.com/tonygorb/Scenri/commit/ce30a3d98ba381a96e2d9658aef01aaa3b00fbdb))
89
+ * **studio:** no tooltip on the zoom reading, and the review spec seeds within budget ([096dc8c](https://github.com/tonygorb/Scenri/commit/096dc8c224b626fc9ef403552f71d2c5e98be7e4))
90
+ * **studio:** the band says Keeping, once; the record is the ask alone ([bdd1fe3](https://github.com/tonygorb/Scenri/commit/bdd1fe3911bf5678e56dc0dc9a42c1a6dd5f89b4))
91
+ * **studio:** the close and the arrows stand beside the rail, not over it ([a805340](https://github.com/tonygorb/Scenri/commit/a805340e9cd6984ddd019e66485a21100bc8f813))
92
+ * **studio:** the held row under the picture measures exactly like the strip ([eb325e7](https://github.com/tonygorb/Scenri/commit/eb325e780dc0d9b8d4687d15c114f9a35b13d062))
93
+ * **studio:** the rail fades only the side that has more, so the first and last shot stay whole ([33f8964](https://github.com/tonygorb/Scenri/commit/33f89640dca9284e7f40206c8a33bba4461d5d93))
94
+ * **studio:** the rail is centred and faded before its first paint ([0328d86](https://github.com/tonygorb/Scenri/commit/0328d8656c319283cf5351e68c87315a3a982fd3))
95
+ * **studio:** the rail's fade is whole within a step, not a hundred pixels later ([0c81256](https://github.com/tonygorb/Scenri/commit/0c8125654f7141e34f93536c50271033aef2720d))
96
+ * **studio:** the rail's fades grow in with the scroll instead of popping at the first pixel ([9f29b96](https://github.com/tonygorb/Scenri/commit/9f29b96c272ac68cc37b0974c9f5944f98ced7e4))
97
+ * **studio:** the rail's own gutter is the gap to the close ([89455f0](https://github.com/tonygorb/Scenri/commit/89455f08c4c75b8b74faca516b29e1f7fd3e84b0))
98
+ * **studio:** the rail's peek card opens beside the tile ([449e3d2](https://github.com/tonygorb/Scenri/commit/449e3d251ba088a7ed76b9008702bc2080071a1a))
99
+ * **studio:** the rail's peek card wears a tail ([aa42969](https://github.com/tonygorb/Scenri/commit/aa429698b6db7551a938ede5ce86c1891c8a4c2c))
100
+ * **studio:** the record is the prompt, copied like the header verbs ([5bddad4](https://github.com/tonygorb/Scenri/commit/5bddad421e7f4c36b9d34154dc9f36ce873cfbe6))
101
+ * **studio:** the shot overlay takes focus itself on open ([aeb3c28](https://github.com/tonygorb/Scenri/commit/aeb3c287c6ebb16582bb0d9c3eeb2073b7c5a84e))
102
+ * **studio:** the stage never animates geometry ([9d5e3dc](https://github.com/tonygorb/Scenri/commit/9d5e3dc545e1475fd90b4f96647a507b9faca5b3))
103
+ * **studio:** trail tiles always cover their square ([93fcce1](https://github.com/tonygorb/Scenri/commit/93fcce145871f8f5d4fc91877101bb8bf9dcfb9e))
104
+
105
+
106
+ ### Miscellaneous Chores
107
+
108
+ * **release:** pin the next version to 0.8.3 ([0d6449b](https://github.com/tonygorb/Scenri/commit/0d6449b767fb3cc04e31c058fe9277dead5fce4e))
109
+
3
110
  ## [0.8.2](https://github.com/tonygorb/Scenri/compare/v0.8.1...v0.8.2) (2026-09-03)
4
111
 
5
112
 
package/README.md CHANGED
@@ -65,6 +65,8 @@ npx scenri
65
65
 
66
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.
67
67
 
68
+ On macOS and Windows, Scenri then asks once whether to put a **Scenri icon on your desktop**. Say yes and from then on a double-click starts Scenri and opens it in your browser, no terminal needed; the icon works offline and keeps working across updates. Said no? Settings, then About, then **Add to desktop**, or `npx scenri desktop` in a terminal. Scenri stays what it is, a local server and your browser: the icon is a launcher, not an app.
69
+
68
70
  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).
69
71
 
70
72
  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](https://github.com/tonygorb/scenri/blob/main/docs/INSTALL.md#troubleshooting).
@@ -0,0 +1,55 @@
1
+ import { readFileSync, mkdirSync, writeFileSync, renameSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ // src/desktop/paths.ts
5
+ var LAUNCHER_SCHEMA = 1;
6
+ var launcherDir = (homedir) => join(homedir, ".scenri", "launcher");
7
+ var logsDir = (home) => join(home, "logs");
8
+ var launcherRecordPath = (homedir) => join(launcherDir(homedir), "launcher.json");
9
+ function readLauncherRecord(homedir) {
10
+ try {
11
+ const raw = JSON.parse(readFileSync(launcherRecordPath(homedir), "utf8"));
12
+ if (typeof raw?.schema !== "number" || typeof raw.home !== "string" || typeof raw.nodePath !== "string" || typeof raw.artifact?.path !== "string") {
13
+ return null;
14
+ }
15
+ return raw;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ function writeLauncherRecord(homedir, record) {
21
+ const path = launcherRecordPath(homedir);
22
+ mkdirSync(launcherDir(homedir), { recursive: true });
23
+ const tmp = `${path}.tmp`;
24
+ writeFileSync(tmp, `${JSON.stringify(record, null, 2)}
25
+ `);
26
+ renameSync(tmp, path);
27
+ }
28
+ var REPLAYED = ["SCENRI_PORT", "SCENRI_HOST"];
29
+ function recordedEnv(env) {
30
+ const out = {};
31
+ for (const key of REPLAYED) if (env[key]) out[key] = env[key];
32
+ return out;
33
+ }
34
+ var KNOWN_FOLDER_DESKTOP = "[Environment]::GetFolderPath('Desktop')";
35
+ async function desktopDir(deps) {
36
+ if (deps.env.SCENRI_DESKTOP_DIR) return deps.env.SCENRI_DESKTOP_DIR;
37
+ if (deps.platform === "darwin") return join(deps.homedir, "Desktop");
38
+ if (deps.platform === "win32") {
39
+ const out = await deps.runImpl("powershell.exe", [
40
+ "-NoProfile",
41
+ "-NonInteractive",
42
+ "-Command",
43
+ KNOWN_FOLDER_DESKTOP
44
+ ]);
45
+ const dir = out.trim();
46
+ return dir || null;
47
+ }
48
+ return null;
49
+ }
50
+ var isSupportedPlatform = (platform) => platform === "darwin" || platform === "win32";
51
+ var launcherInstalled = (homedir) => existsSync(launcherRecordPath(homedir));
52
+
53
+ export { LAUNCHER_SCHEMA, desktopDir, isSupportedPlatform, launcherDir, launcherInstalled, launcherRecordPath, logsDir, readLauncherRecord, recordedEnv, writeLauncherRecord };
54
+ //# sourceMappingURL=chunk-4NQKRVO2.js.map
55
+ //# sourceMappingURL=chunk-4NQKRVO2.js.map
@@ -0,0 +1,246 @@
1
+ import { isSupportedPlatform, desktopDir, launcherDir, LAUNCHER_SCHEMA, writeLauncherRecord, recordedEnv, readLauncherRecord } from './chunk-4NQKRVO2.js';
2
+ import { existsSync, rmSync, mkdirSync, writeFileSync, chmodSync, copyFileSync, renameSync, utimesSync } from 'fs';
3
+ import { join, dirname } from 'path';
4
+
5
+ var MAC_BUNDLE_ID = "co.scenri.desktop-launcher";
6
+ var MAC_MARKER = "scenri-launcher.json";
7
+ var MAC_SCRIPT = `#!/bin/sh
8
+ # Scenri desktop launcher. Written by Scenri; deleting it removes only this icon.
9
+ L="$HOME/.scenri/launcher"
10
+ say() {
11
+ if [ "$SCENRI_NO_DIALOG" = "1" ]; then return 0; fi
12
+ /usr/bin/osascript -e 'on run argv' \\
13
+ -e 'display dialog (item 1 of argv) with title "Scenri" buttons {"OK"} default button 1 with icon stop' \\
14
+ -e 'end run' -- "$1"
15
+ }
16
+ if [ ! -f "$L/launch.mjs" ]; then
17
+ say "Scenri's launcher files are missing. Open Terminal and run: npx scenri desktop"
18
+ exit 1
19
+ fi
20
+ # The node that installed the icon, while it still exists: no probing, no delay.
21
+ NODE=""
22
+ if [ -r "$L/node-path" ]; then IFS= read -r NODE < "$L/node-path" || NODE=""; fi
23
+ if [ -z "$NODE" ] || [ ! -x "$NODE" ]; then
24
+ MAJOR=""
25
+ if [ -r "$L/node-major" ]; then IFS= read -r MAJOR < "$L/node-major" || MAJOR=""; fi
26
+ # Native modules were built for that node's major and another major refuses
27
+ # them: every candidate once demanding the same major, then once taking any.
28
+ candidates() {
29
+ command -v node 2>/dev/null || true
30
+ echo /opt/homebrew/bin/node
31
+ echo /usr/local/bin/node
32
+ echo "$HOME/.volta/bin/node"
33
+ echo "$HOME/.local/share/fnm/aliases/default/bin/node"
34
+ echo "$HOME/Library/Application Support/fnm/aliases/default/bin/node"
35
+ for c in "$HOME"/.nvm/versions/node/*/bin/node; do echo "$c"; done
36
+ }
37
+ pick() {
38
+ want="$1"
39
+ candidates | while IFS= read -r c; do
40
+ [ -n "$c" ] && [ -x "$c" ] || continue
41
+ if [ -n "$want" ]; then
42
+ v="$("$c" -p 'process.versions.node.split(".")[0]' 2>/dev/null)" || continue
43
+ [ "$v" = "$want" ] || continue
44
+ fi
45
+ echo "$c"
46
+ done | tail -n 1
47
+ }
48
+ NODE=""
49
+ if [ -n "$MAJOR" ]; then NODE="$(pick "$MAJOR")"; fi
50
+ if [ -z "$NODE" ]; then NODE="$(pick "")"; fi
51
+ fi
52
+ if [ -z "$NODE" ] || [ ! -x "$NODE" ]; then
53
+ say "Scenri needs Node.js, and none was found on this Mac. Install it from nodejs.org, then open Terminal and run: npx scenri"
54
+ exit 1
55
+ fi
56
+ exec "$NODE" "$L/launch.mjs"
57
+ `;
58
+ function macPlist(opts) {
59
+ return `<?xml version="1.0" encoding="UTF-8"?>
60
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
61
+ <plist version="1.0">
62
+ <dict>
63
+ <key>CFBundleDevelopmentRegion</key>
64
+ <string>en</string>
65
+ <key>CFBundleDisplayName</key>
66
+ <string>Scenri</string>
67
+ <key>CFBundleExecutable</key>
68
+ <string>Scenri</string>
69
+ <key>CFBundleIconFile</key>
70
+ <string>Scenri.icns</string>
71
+ <key>CFBundleIdentifier</key>
72
+ <string>${MAC_BUNDLE_ID}</string>
73
+ <key>CFBundleInfoDictionaryVersion</key>
74
+ <string>6.0</string>
75
+ <key>CFBundleName</key>
76
+ <string>Scenri</string>
77
+ <key>CFBundlePackageType</key>
78
+ <string>APPL</string>
79
+ <key>CFBundleShortVersionString</key>
80
+ <string>${opts.version}</string>
81
+ <key>CFBundleVersion</key>
82
+ <string>${opts.schema}</string>
83
+ <key>LSMinimumSystemVersion</key>
84
+ <string>12.0</string>
85
+ <key>NSHighResolutionCapable</key>
86
+ <true/>
87
+ </dict>
88
+ </plist>
89
+ `;
90
+ }
91
+ var isOurMacBundle = (bundle) => existsSync(join(bundle, "Contents", "Resources", MAC_MARKER));
92
+ function writeMacBundle(opts) {
93
+ const tmp = `${opts.path}.tmp-${process.pid}`;
94
+ rmSync(tmp, { recursive: true, force: true });
95
+ const contents = join(tmp, "Contents");
96
+ mkdirSync(join(contents, "MacOS"), { recursive: true });
97
+ mkdirSync(join(contents, "Resources"), { recursive: true });
98
+ writeFileSync(join(contents, "Info.plist"), macPlist(opts));
99
+ writeFileSync(join(contents, "PkgInfo"), "APPL????");
100
+ const script = join(contents, "MacOS", "Scenri");
101
+ writeFileSync(script, MAC_SCRIPT);
102
+ chmodSync(script, 493);
103
+ copyFileSync(opts.icns, join(contents, "Resources", "Scenri.icns"));
104
+ writeFileSync(
105
+ join(contents, "Resources", MAC_MARKER),
106
+ `${JSON.stringify({ scenri: "desktop-launcher", schema: opts.schema, version: opts.version }, null, 2)}
107
+ `
108
+ );
109
+ rmSync(opts.path, { recursive: true, force: true });
110
+ mkdirSync(dirname(opts.path), { recursive: true });
111
+ renameSync(tmp, opts.path);
112
+ const now = /* @__PURE__ */ new Date();
113
+ utimesSync(opts.path, now, now);
114
+ }
115
+
116
+ // src/desktop/windows.ts
117
+ var POWERSHELL = "powershell.exe";
118
+ var POWERSHELL_ARGS = ["-NoProfile", "-NonInteractive", "-Command"];
119
+ var CREATE_LNK_COMMAND = "$s = New-Object -ComObject WScript.Shell; $l = $s.CreateShortcut($env:SCENRI_LNK); $l.TargetPath = $env:SCENRI_TARGET; $l.Arguments = $env:SCENRI_ARGS; $l.WorkingDirectory = $env:SCENRI_WORKDIR; $l.IconLocation = $env:SCENRI_ICON; $l.Description = 'Open Scenri'; $l.WindowStyle = 7; $l.Save()";
120
+ var READ_LNK_COMMAND = "$s = New-Object -ComObject WScript.Shell; $l = $s.CreateShortcut($env:SCENRI_LNK); Write-Output $l.TargetPath; Write-Output $l.Arguments";
121
+ async function readLnk(runImpl, path) {
122
+ const out = await runImpl(POWERSHELL, [...POWERSHELL_ARGS, READ_LNK_COMMAND], { env: { SCENRI_LNK: path } });
123
+ const [target = "", args = ""] = out.split(/\r?\n/);
124
+ return { target: target.trim(), args: args.trim() };
125
+ }
126
+ async function isOurLnk(runImpl, path, bootstrap) {
127
+ try {
128
+ const { args } = await readLnk(runImpl, path);
129
+ return args.toLowerCase().includes(bootstrap.toLowerCase());
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+ async function writeLnk(runImpl, opts) {
135
+ await runImpl(POWERSHELL, [...POWERSHELL_ARGS, CREATE_LNK_COMMAND], {
136
+ env: {
137
+ SCENRI_LNK: opts.path,
138
+ SCENRI_TARGET: opts.target,
139
+ SCENRI_ARGS: opts.args,
140
+ SCENRI_WORKDIR: opts.workdir,
141
+ SCENRI_ICON: opts.icon
142
+ }
143
+ });
144
+ }
145
+
146
+ // src/desktop/install.ts
147
+ var UNSUPPORTED = "Desktop shortcuts are not available on this system yet.";
148
+ var NO_DESKTOP = "Your Desktop folder could not be found.";
149
+ var COLLISION = "Something else named Scenri is already on your desktop. Move or rename it, then try again.";
150
+ var DENIED = "macOS did not allow writing to your Desktop. Allow it under System Settings > Privacy & Security > Files and Folders, then try again.";
151
+ var SUPPORT_FILES = ["launch.mjs", "Scenri.icns", "scenri.ico"];
152
+ async function installDesktop(deps) {
153
+ if (!isSupportedPlatform(deps.platform)) return { ok: false, reason: "unsupported", message: UNSUPPORTED };
154
+ const desktop = await desktopDir(deps);
155
+ if (!desktop) return { ok: false, reason: "no-desktop", message: NO_DESKTOP };
156
+ const darwin = deps.platform === "darwin";
157
+ const support = launcherDir(deps.homedir);
158
+ const bootstrap = join(support, "launch.mjs");
159
+ const kind = darwin ? "macos-app" : "windows-lnk";
160
+ const artifact = join(desktop, darwin ? "Scenri.app" : "Scenri.lnk");
161
+ if (existsSync(artifact)) {
162
+ const ours = darwin ? isOurMacBundle(artifact) : await isOurLnk(deps.runImpl, artifact, bootstrap);
163
+ if (!ours) return { ok: false, reason: "collision", message: COLLISION };
164
+ }
165
+ const nodeMajor = runningNodeMajor();
166
+ writeSupportFiles({ ...deps, nodeMajor }, support);
167
+ try {
168
+ if (darwin) {
169
+ writeMacBundle({
170
+ path: artifact,
171
+ icns: join(support, "Scenri.icns"),
172
+ version: deps.version,
173
+ schema: LAUNCHER_SCHEMA
174
+ });
175
+ } else {
176
+ await writeLnk(deps.runImpl, {
177
+ path: artifact,
178
+ target: deps.execPath,
179
+ args: `"${bootstrap}"`,
180
+ workdir: support,
181
+ icon: `${join(support, "scenri.ico")},0`
182
+ });
183
+ }
184
+ } catch (err) {
185
+ const code = err.code;
186
+ if (darwin && (code === "EPERM" || code === "EACCES"))
187
+ return { ok: false, reason: "desktop-denied", message: DENIED };
188
+ const detail = err instanceof Error ? err.message.split("\n")[0] : String(err);
189
+ return { ok: false, reason: "failed", message: `Scenri could not write ${artifact} (${detail}).` };
190
+ }
191
+ writeLauncherRecord(deps.homedir, {
192
+ schema: LAUNCHER_SCHEMA,
193
+ createdBy: deps.version,
194
+ home: deps.home,
195
+ nodePath: deps.execPath,
196
+ nodeMajor,
197
+ env: recordedEnv(deps.env),
198
+ artifact: { kind, path: artifact }
199
+ });
200
+ return { ok: true, kind, path: artifact };
201
+ }
202
+ var runningNodeMajor = () => Number(process.versions.node.split(".")[0]);
203
+ function writeSupportFiles(deps, support) {
204
+ mkdirSync(support, { recursive: true });
205
+ for (const f of SUPPORT_FILES) copyFileSync(join(deps.assetsDir, f), join(support, f));
206
+ if (deps.platform === "darwin") {
207
+ writeFileSync(join(support, "node-path"), `${deps.execPath}
208
+ `);
209
+ writeFileSync(join(support, "node-major"), `${deps.nodeMajor}
210
+ `);
211
+ }
212
+ }
213
+ async function removeDesktop(deps) {
214
+ const record = readLauncherRecord(deps.homedir);
215
+ const support = launcherDir(deps.homedir);
216
+ let removed = false;
217
+ let message;
218
+ const path = record?.artifact.path;
219
+ if (path && existsSync(path)) {
220
+ const ours = record.artifact.kind === "macos-app" ? isOurMacBundle(path) : await isOurLnk(deps.runImpl, path, join(support, "launch.mjs"));
221
+ if (ours) {
222
+ rmSync(path, { recursive: true, force: true });
223
+ removed = true;
224
+ } else {
225
+ message = `${path} is not the launcher Scenri made, so it was left alone.`;
226
+ }
227
+ }
228
+ rmSync(support, { recursive: true, force: true });
229
+ return { removed, path, message };
230
+ }
231
+ async function desktopStatus(deps) {
232
+ const supported = isSupportedPlatform(deps.platform);
233
+ const record = readLauncherRecord(deps.homedir);
234
+ let installed = false;
235
+ let path = null;
236
+ if (supported && record && existsSync(record.artifact.path)) {
237
+ installed = record.artifact.kind === "macos-app" ? isOurMacBundle(record.artifact.path) : true;
238
+ path = installed ? record.artifact.path : null;
239
+ }
240
+ const current = installed && record !== null && record.schema === LAUNCHER_SCHEMA && (existsSync(record.nodePath) || record.nodePath === deps.execPath);
241
+ return { supported, platform: deps.platform, installed, path, current, record };
242
+ }
243
+
244
+ export { MAC_SCRIPT, POWERSHELL, POWERSHELL_ARGS, UNSUPPORTED, desktopStatus, installDesktop, isOurMacBundle, macPlist, removeDesktop, runningNodeMajor, writeLnk, writeMacBundle, writeSupportFiles };
245
+ //# sourceMappingURL=chunk-HLMR33DT.js.map
246
+ //# sourceMappingURL=chunk-HLMR33DT.js.map