scenri 0.8.3 → 0.9.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.
@@ -0,0 +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-OJAG3FRX.js';
2
+ //# sourceMappingURL=src-CSR34EBZ.js.map
3
+ //# sourceMappingURL=src-CSR34EBZ.js.map
Binary file
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Scenri desktop bootstrap, v1. A copy of this file lives in
4
+ * ~/.scenri/launcher and is what the Desktop icon runs. It stays small and on
5
+ * node builtins because copies of it live on Desktops indefinitely: it finds
6
+ * the newest valid version under the recorded home and hands off to
7
+ * `node <entry> open`, which holds every decision and is versioned with the
8
+ * app. Change the hand-off and bump the schema in packages/cli/src/desktop;
9
+ * the next start rewrites this copy.
10
+ */
11
+ import { spawn, spawnSync } from 'node:child_process';
12
+ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync } from 'node:fs';
13
+ import { homedir } from 'node:os';
14
+ import { dirname, join } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const SCHEMA = 1;
18
+ const PKG = 'scenri';
19
+ const here = dirname(fileURLToPath(import.meta.url));
20
+
21
+ const record = readJson(join(here, 'launcher.json'));
22
+ const home = typeof record?.home === 'string' && record.home ? record.home : join(homedir(), '.scenri');
23
+ const logPath = join(home, 'logs', 'launcher.log');
24
+
25
+ log(
26
+ `bootstrap v${SCHEMA}: invoked from ${here} by ${process.execPath} (node ${process.version}, ${process.platform}), home ${home}`,
27
+ );
28
+ const version = newestVersion(home);
29
+ if (!version) {
30
+ log(`bootstrap v${SCHEMA}: no valid version under ${join(home, 'app', 'versions')}`);
31
+ dialog("Scenri's app files are missing. Open a terminal and run: npx scenri");
32
+ process.exit(1);
33
+ }
34
+ const entry = join(home, 'app', 'versions', version, 'node_modules', PKG, 'dist', 'index.js');
35
+ const env = { ...process.env, ...(record?.env && typeof record.env === 'object' ? record.env : {}), SCENRI_HOME: home };
36
+ let fd = null;
37
+ try {
38
+ mkdirSync(dirname(logPath), { recursive: true });
39
+ fd = openSync(logPath, 'a');
40
+ } catch {
41
+ /* no log: launch anyway */
42
+ }
43
+ // On macOS the bundle's process is what the Dock bounces, so it stays alive
44
+ // until \`open\` has put something in the browser. On Windows the shortcut's
45
+ // minimised console lives as long as this process, so hand off and leave.
46
+ const wait = process.platform === 'darwin';
47
+ const child = spawn(process.execPath, [entry, 'open'], {
48
+ detached: !wait,
49
+ stdio: ['ignore', fd ?? 'ignore', fd ?? 'ignore'],
50
+ windowsHide: true,
51
+ env,
52
+ });
53
+ if (fd !== null) closeSync(fd);
54
+ // A node that cannot be executed arrives as an event, not an exit, and unhandled
55
+ // it would end this process with nothing on screen.
56
+ child.on('error', (err) => {
57
+ log(`bootstrap v${SCHEMA}: could not start ${entry} (${err.message})`);
58
+ dialog(`Scenri could not start (${err.message}). Open a terminal and run: npx scenri`);
59
+ process.exit(1);
60
+ });
61
+ log(`bootstrap v${SCHEMA}: handing off to ${entry} (pid ${child.pid ?? 'unknown'})`);
62
+ if (wait) {
63
+ child.on('exit', (code) => process.exit(code ?? 1));
64
+ } else {
65
+ child.unref();
66
+ }
67
+
68
+ // ---- helpers, duplicated from the package on purpose: this file stands alone
69
+
70
+ function readJson(path) {
71
+ try {
72
+ return JSON.parse(readFileSync(path, 'utf8'));
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+
78
+ function log(line) {
79
+ try {
80
+ mkdirSync(dirname(logPath), { recursive: true });
81
+ appendFileSync(logPath, `${new Date().toISOString()} ${line}\n`);
82
+ } catch {
83
+ /* a lost line, never a failed launch */
84
+ }
85
+ }
86
+
87
+ /** The same rule as the updater's versionsDir: name matches, entry exists. */
88
+ function newestVersion(dataHome) {
89
+ const versions = join(dataHome, 'app', 'versions');
90
+ let names = [];
91
+ try {
92
+ names = readdirSync(versions);
93
+ } catch {
94
+ return null;
95
+ }
96
+ const valid = names.filter((v) => {
97
+ const root = join(versions, v, 'node_modules', PKG);
98
+ const manifest = readJson(join(root, 'package.json'));
99
+ return manifest?.name === PKG && manifest.version === v && existsSync(join(root, 'dist', 'index.js'));
100
+ });
101
+ valid.sort(compareSemver);
102
+ return valid[valid.length - 1] ?? null;
103
+ }
104
+
105
+ function compareSemver(a, b) {
106
+ const pa = /^(\d+)\.(\d+)\.(\d+)$/.exec(a);
107
+ const pb = /^(\d+)\.(\d+)\.(\d+)$/.exec(b);
108
+ if (!pa && !pb) return 0;
109
+ if (!pa) return -1;
110
+ if (!pb) return 1;
111
+ for (let i = 1; i <= 3; i++) {
112
+ const d = Number(pa[i]) - Number(pb[i]);
113
+ if (d !== 0) return d < 0 ? -1 : 1;
114
+ }
115
+ return 0;
116
+ }
117
+
118
+ /** One native sentence when nothing else can speak; the message is an argument, never syntax. */
119
+ function dialog(message) {
120
+ log(`dialog: ${message}`);
121
+ if (process.env.SCENRI_NO_DIALOG === '1') return;
122
+ try {
123
+ if (process.platform === 'darwin') {
124
+ spawnSync(
125
+ '/usr/bin/osascript',
126
+ [
127
+ '-e',
128
+ 'on run argv',
129
+ '-e',
130
+ 'display dialog (item 1 of argv) with title "Scenri" buttons {"OK"} default button 1 with icon stop',
131
+ '-e',
132
+ 'end run',
133
+ '--',
134
+ message,
135
+ ],
136
+ { stdio: 'ignore' },
137
+ );
138
+ } else if (process.platform === 'win32') {
139
+ // windowsHide keeps the console off screen and the box still shows;
140
+ // probed on a real Windows desktop, where a detached PowerShell shows nothing.
141
+ spawnSync(
142
+ powershell(),
143
+ [
144
+ '-NoProfile',
145
+ '-NonInteractive',
146
+ '-Command',
147
+ "Add-Type -AssemblyName System.Windows.Forms | Out-Null; [System.Windows.Forms.MessageBox]::Show($env:SCENRI_MESSAGE, 'Scenri') | Out-Null",
148
+ ],
149
+ { stdio: 'ignore', windowsHide: true, env: { ...process.env, SCENRI_MESSAGE: message } },
150
+ );
151
+ }
152
+ } catch {
153
+ /* the log line above is the record */
154
+ }
155
+ }
156
+
157
+ /** Windows PowerShell 5.1 by its absolute path: a Desktop click has Explorer's PATH, not the terminal's. */
158
+ function powershell() {
159
+ const root = process.env.SystemRoot || process.env.windir || 'C:\\Windows';
160
+ const absolute = join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
161
+ return existsSync(absolute) ? absolute : 'powershell.exe';
162
+ }
Binary file
@@ -0,0 +1,81 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Starting Scenri</title>
7
+ <meta name="color-scheme" content="light dark">
8
+ <meta name="scenri-studio" content="">
9
+ <style>
10
+ :root { color-scheme: light dark; --bg: #ffffff; --fg: #0a0a0a; --mute: #6b6b6b; --tile: #0d0d0d; --ink: #ffffff; }
11
+ @media (prefers-color-scheme: dark) { :root { --bg: #0d0d0d; --fg: #f5f5f5; --mute: #8a8a8a; --tile: #1a1a1a; } }
12
+ html, body { height: 100%; margin: 0; }
13
+ body { display: grid; place-items: center; background: var(--bg); color: var(--fg); font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, system-ui, sans-serif; }
14
+ main { display: grid; justify-items: center; gap: 18px; text-align: center; padding: 24px; }
15
+ .mark { width: 72px; height: 72px; border-radius: 16px; background: var(--tile); display: grid; place-items: center; }
16
+ .mark svg { width: 40px; height: 40px; fill: var(--ink); }
17
+ h1 { margin: 0; font-size: 17px; font-weight: 600; letter-spacing: -0.01em; }
18
+ p { margin: 0; color: var(--mute); max-width: 34ch; }
19
+ .bar { width: 160px; height: 3px; border-radius: 3px; background: color-mix(in srgb, var(--fg) 12%, transparent); overflow: hidden; }
20
+ .bar i { display: block; width: 40%; height: 100%; border-radius: 3px; background: var(--fg); animation: slide 1.2s ease-in-out infinite; }
21
+ @keyframes slide { 0% { transform: translateX(-100%); } 100% { transform: translateX(400%); } }
22
+ [hidden] { display: none !important; }
23
+ code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 13px; }
24
+ </style>
25
+ </head>
26
+ <body>
27
+ <main>
28
+ <div class="mark" aria-hidden="true"><svg viewBox="0 0 64 64"><title>Scenri</title><path d="M64 18.71V38.8L51.38 51.39V12.62H57.9C61.27 12.62 64 15.35 64 18.72V18.71Z"/>
29
+ <path d="M12.62 12.62V51.39H6.1C2.73 51.39 0 48.66 0 45.29V25.2L12.62 12.61V12.62Z"/>
30
+ <path d="M51.38 51.38L38.79 64H18.7C15.33 64 12.6 61.27 12.6 57.9V51.38H51.37H51.38Z"/>
31
+ <path d="M12.62 12.62L25.2 0H45.29C48.66 0 51.39 2.73 51.39 6.1V12.62H12.62Z"/></svg></div>
32
+ <h1 id="title">Starting Scenri</h1>
33
+ <p id="text">Scenri is starting on this computer. The studio opens here by itself.</p>
34
+ <div class="bar" id="bar" aria-hidden="true"><i></i></div>
35
+ </main>
36
+ <script>
37
+ (() => {
38
+ // The studio URL rides in the fragment, put there by `scenri open` after it
39
+ // verified the port was free or already Scenri's. Only a loopback URL is
40
+ // accepted, so this file can never be made to send someone elsewhere.
41
+ // Written into the meta by scenri open for each launch: a file URL's
42
+ // fragment does not survive open(1) or Start-Process. The fragment is the
43
+ // fallback for a page opened by hand.
44
+ var meta = document.querySelector('meta[name="scenri-studio"]');
45
+ var target = (meta && meta.getAttribute('content')) || location.hash.slice(1);
46
+ var ok = /^http:\/\/(127\.0\.0\.1|localhost):\d{2,5}\/$/.test(target);
47
+ var title = document.getElementById('title');
48
+ var text = document.getElementById('text');
49
+ var bar = document.getElementById('bar');
50
+ var deadline = Date.now() + 90000;
51
+ var done = false;
52
+ var failed = false;
53
+ function go() { if (done) return; done = true; location.replace(target); }
54
+ // The fail text is a verdict on the wait, not on the server: a start that
55
+ // finishes late (a first Defender scan of a fresh install) still hands over.
56
+ function fail(message) {
57
+ if (done || failed) return; failed = true;
58
+ title.textContent = 'Scenri did not start';
59
+ text.innerHTML = message;
60
+ bar.hidden = true;
61
+ }
62
+ if (!ok) { fail('This page needs to be opened by Scenri itself. Open a terminal and run: <code>npx scenri</code>'); return; }
63
+ function probe() {
64
+ if (done) return;
65
+ if (Date.now() > deadline) fail('Open a terminal and run <code>npx scenri</code> to see why.');
66
+ // An image load needs no CORS and works from a file: URL in every browser;
67
+ // the opaque fetch is the second opinion for browsers that block images
68
+ // from a local page.
69
+ var img = new Image();
70
+ img.onload = go;
71
+ img.src = target + 'favicon.ico?' + Date.now();
72
+ if (window.fetch) {
73
+ fetch(target + 'api/version', { mode: 'no-cors', cache: 'no-store' }).then(go, () => {});
74
+ }
75
+ setTimeout(probe, failed ? 2000 : 500);
76
+ }
77
+ probe();
78
+ })();
79
+ </script>
80
+ </body>
81
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scenri",
3
- "version": "0.8.3",
3
+ "version": "0.9.1",
4
4
  "author": "Tony Gorb <hello@scenri.co>",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -53,6 +53,7 @@
53
53
  "!templates/**/.DS_Store",
54
54
  "!templates/previews/*.mjs",
55
55
  "studio-dist",
56
+ "launcher",
56
57
  "README.md",
57
58
  "CHANGELOG.md",
58
59
  "LICENSE",