relmio 0.2.7 → 0.2.9

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
@@ -7,6 +7,27 @@ checks the registry separately after publication.
7
7
 
8
8
  ## Unreleased
9
9
 
10
+ ## [0.2.9] - 2026-08-02
11
+
12
+ ### Fixed
13
+
14
+ - Launch the local ChatGPT OAuth helper through the current Windows Node.js
15
+ runtime and npm's JavaScript CLI instead of executing `npx.cmd` directly,
16
+ preventing `spawn EINVAL` while preserving the native `npx` path on macOS,
17
+ Linux, WSL, and Git Bash.
18
+ - Explain how to recover when a refreshed wizard page no longer has its private
19
+ session URL.
20
+
21
+ ## [0.2.8] - 2026-08-02
22
+
23
+ ### Changed
24
+
25
+ - Replace the local browser wizard's text appearance selector with compact,
26
+ accessible System/Light/Dark icons, while keeping the original horizontal
27
+ Signal Spine flow and touch-friendly behavior.
28
+ - Serve the bundled Lucide SVG assets from the wizard and include them in the
29
+ npm package so offline and Node-free browser launches render consistently.
30
+
10
31
  ## [0.2.7] - 2026-08-02
11
32
 
12
33
  ### Changed
package/README.md CHANGED
@@ -124,6 +124,10 @@ Users who already have Node.js 22 or newer can run the npm package directly:
124
124
  npx --yes --ignore-scripts relmio@latest
125
125
  ```
126
126
 
127
+ Keep the active setup terminal open while using the wizard. If the page says
128
+ the wizard link is incomplete, close that tab and open the full `Local wizard:`
129
+ URL printed by the current terminal instead of refreshing the stripped page.
130
+
127
131
  ### Requirements
128
132
 
129
133
  - On macOS/Linux/WSL/Git Bash: `curl`, `awk`, `tar`, and either `sha256sum` or
@@ -113,7 +113,9 @@ bypassed.
113
113
  | `node: command not found`, `node is not recognized`, or Node is older than 22 | The NPX fallback cannot use the local runtime. | Use the macOS/Linux curl command or the native Windows PowerShell/Command Prompt command above. Either can run with a verified temporary runtime. Do not install Node.js on the VPS for the wizard. |
114
114
  | `curl` or `sh` is not recognized on Windows | The macOS/Linux command was pasted into a native Windows terminal. | Use the PowerShell command in PowerShell, or the longer `powershell -NoProfile ...` command in Command Prompt. Git Bash is not required. |
115
115
  | A bootstrap reports a checksum mismatch | The Node.js download did not match the official SHA-256 manifest, so it was not executed. | Retry on a trusted connection. Do not bypass the check. If it repeats, use an existing Node.js 22+ installation and report the sanitized error. |
116
+ | Windows reports `spawn EINVAL` when starting ChatGPT sign-in | An older wizard tried to execute `npx.cmd` directly; Windows requires the current Node runtime to launch npm's JavaScript CLI. | Update to the latest `relmio@latest` release and restart the setup command. The current wizard keeps the macOS/Linux/WSL/Git Bash `npx` path unchanged. |
116
117
  | The browser did not open | The automatic browser launch failed, but the local server may still be running. | Keep the newest terminal open and copy its newest `127.0.0.1` setup URL into the browser. Do not reuse a URL from a closed terminal. |
118
+ | The wizard says `This wizard link is incomplete` | The page was refreshed or opened without the private `?session=...` token; the token is removed from the address bar after startup. | Close the tab and open the complete `Local wizard:` URL printed by the active setup terminal. Do not reuse a URL from a closed terminal. |
117
119
  | An old wizard page reports an invalid or expired setup session | The local server was closed or a newer wizard run created a different one-time session token. | Close the old page and use only the URL printed by the currently running terminal. |
118
120
  | `npx` appears to run an older wizard | An old terminal or tab is still active, or the package was run without an explicit tag. | Close old runs, check `npm view relmio version`, then run `npx --yes --ignore-scripts relmio@latest`. |
119
121
  | `This sign-in request expired` | The OAuth tab is old or the five-minute callback window ended. | Close the old tab and select **Refresh ChatGPT sign-in** from the newest active wizard. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relmio",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Turn a supported ChatGPT/Codex OAuth sign-in into a private OpenAI-compatible endpoint, starting with self-hosted n8n.",
5
5
  "keywords": [
6
6
  "relmio",
@@ -16,6 +16,37 @@ const OPENAI_AUTH_ORIGIN = "https://auth.openai.com";
16
16
  const wait = (milliseconds) =>
17
17
  new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
18
18
 
19
+ function resolveWindowsNpxCli({ env, execPath }) {
20
+ const npmExecPathKey = Object.keys(env ?? {}).find(
21
+ (key) => key.toLowerCase() === "npm_execpath",
22
+ );
23
+ const npmExecPath =
24
+ npmExecPathKey === undefined ? undefined : env[npmExecPathKey];
25
+
26
+ if (typeof npmExecPath === "string") {
27
+ if (/(?:^|[\\/])npx-cli\.js$/iu.test(npmExecPath)) {
28
+ return resolve(npmExecPath);
29
+ }
30
+ if (/(?:^|[\\/])npm-cli\.js$/iu.test(npmExecPath)) {
31
+ return resolve(dirname(npmExecPath), "npx-cli.js");
32
+ }
33
+ }
34
+
35
+ return resolve(dirname(execPath), "node_modules", "npm", "bin", "npx-cli.js");
36
+ }
37
+
38
+ function createNpxInvocation({ platform, env, execPath }) {
39
+ if (platform !== "win32") {
40
+ return { command: "npx", prefixArgs: [] };
41
+ }
42
+
43
+ return {
44
+ command: execPath,
45
+ // Windows cannot execute npx.cmd directly with shell:false.
46
+ prefixArgs: [resolveWindowsNpxCli({ env, execPath })],
47
+ };
48
+ }
49
+
19
50
  export function resolveAuthPath({
20
51
  env = process.env,
21
52
  homeDirectory = homedir(),
@@ -132,11 +163,12 @@ export async function startOAuthLogin({
132
163
  env = process.env,
133
164
  homeDirectory = homedir(),
134
165
  platform = process.platform,
166
+ execPath = process.execPath,
135
167
  spawnProcess = spawn,
136
168
  createPendingId = randomUUID,
137
169
  waitForCredentialPoll = wait,
138
170
  } = {}) {
139
- const command = platform === "win32" ? "npx.cmd" : "npx";
171
+ const npxInvocation = createNpxInvocation({ platform, env, execPath });
140
172
  const authPath = resolveAuthPath({ env, homeDirectory });
141
173
  const authDirectory = dirname(authPath);
142
174
  const pendingAuthPath = `${authPath}.pending-${createPendingId()}`;
@@ -155,12 +187,24 @@ export async function startOAuthLogin({
155
187
  await fileSystem.mkdir(authDirectory, { recursive: true, mode: 0o700 });
156
188
  await fileSystem.chmod(authDirectory, 0o700);
157
189
 
158
- const child = spawnProcess(command, args, {
159
- env,
160
- shell: false,
161
- stdio: ["ignore", "pipe", "pipe"],
162
- windowsHide: true,
163
- });
190
+ let child;
191
+ try {
192
+ child = spawnProcess(
193
+ npxInvocation.command,
194
+ [...npxInvocation.prefixArgs, ...args],
195
+ {
196
+ env,
197
+ shell: false,
198
+ stdio: ["ignore", "pipe", "pipe"],
199
+ windowsHide: true,
200
+ },
201
+ );
202
+ } catch (error) {
203
+ throw new Error(
204
+ "The local sign-in command could not start. Update Relmio and retry with Node.js 22 or newer.",
205
+ { cause: error },
206
+ );
207
+ }
164
208
  child.stderr?.resume();
165
209
 
166
210
  let loginOutput = "";
package/src/ui/app.js CHANGED
@@ -146,7 +146,9 @@ function showStep(step) {
146
146
 
147
147
  async function api(path, { method = "GET", body } = {}) {
148
148
  if (!token) {
149
- throw new Error("This wizard link is incomplete. Restart the setup command.");
149
+ throw new Error(
150
+ "This wizard link is incomplete. Close this tab and open the full URL printed by the active setup terminal.",
151
+ );
150
152
  }
151
153
 
152
154
  let response;
@@ -0,0 +1,6 @@
1
+ <!-- Lucide monitor icon, ISC licensed: https://lucide.dev/license -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3
+ <rect width="20" height="14" x="2" y="3" rx="2" />
4
+ <line x1="8" x2="16" y1="21" y2="21" />
5
+ <line x1="12" x2="12" y1="17" y2="21" />
6
+ </svg>
@@ -0,0 +1,4 @@
1
+ <!-- Lucide moon icon, ISC licensed: https://lucide.dev/license -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3
+ <path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" />
4
+ </svg>
@@ -0,0 +1,12 @@
1
+ <!-- Lucide sun icon, ISC licensed: https://lucide.dev/license -->
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
3
+ <circle cx="12" cy="12" r="4" />
4
+ <path d="M12 2v2" />
5
+ <path d="M12 20v2" />
6
+ <path d="m4.93 4.93 1.41 1.41" />
7
+ <path d="m17.66 17.66 1.41 1.41" />
8
+ <path d="M2 12h2" />
9
+ <path d="M20 12h2" />
10
+ <path d="m6.34 17.66-1.41 1.41" />
11
+ <path d="m19.07 4.93-1.41 1.41" />
12
+ </svg>
package/src/ui/index.html CHANGED
@@ -35,17 +35,20 @@
35
35
  <div class="header-actions">
36
36
  <fieldset class="theme-picker">
37
37
  <legend class="visually-hidden">Color theme</legend>
38
- <label>
38
+ <label title="Use system appearance">
39
39
  <input type="radio" name="color-theme" value="system" checked />
40
- <span>System</span>
40
+ <span class="theme-icon theme-icon-system" aria-hidden="true"></span>
41
+ <span class="visually-hidden">System</span>
41
42
  </label>
42
- <label>
43
+ <label title="Use light appearance">
43
44
  <input type="radio" name="color-theme" value="light" />
44
- <span>Light</span>
45
+ <span class="theme-icon theme-icon-light" aria-hidden="true"></span>
46
+ <span class="visually-hidden">Light</span>
45
47
  </label>
46
- <label>
48
+ <label title="Use dark appearance">
47
49
  <input type="radio" name="color-theme" value="dark" />
48
- <span>Dark</span>
50
+ <span class="theme-icon theme-icon-dark" aria-hidden="true"></span>
51
+ <span class="visually-hidden">Dark</span>
49
52
  </label>
50
53
  </fieldset>
51
54
  <span class="local-badge">Runs only on this computer</span>
package/src/ui/styles.css CHANGED
@@ -201,9 +201,9 @@ a:focus-visible {
201
201
  min-width: 0;
202
202
  margin: 0;
203
203
  padding: 0.1875rem;
204
- border: 1px solid var(--border);
205
- border-radius: 0.625rem;
206
- background: var(--surface-muted);
204
+ border: 0;
205
+ border-radius: 999px;
206
+ background: color-mix(in srgb, var(--text) 88%, var(--border));
207
207
  }
208
208
 
209
209
  .theme-picker label {
@@ -217,29 +217,50 @@ a:focus-visible {
217
217
  pointer-events: none;
218
218
  }
219
219
 
220
- .theme-picker span {
221
- min-height: 2rem;
220
+ .theme-picker .theme-icon {
221
+ width: 1.75rem;
222
+ min-height: 1.75rem;
222
223
  display: inline-flex;
223
224
  align-items: center;
224
225
  justify-content: center;
225
- padding: 0 0.625rem;
226
- border-radius: 0.4375rem;
227
- color: var(--text-muted);
228
- font-size: 0.6875rem;
229
- font-weight: 700;
226
+ padding: 0;
227
+ border-radius: 999px;
228
+ color: rgb(255 255 255 / 72%);
230
229
  transition:
231
230
  color 160ms var(--ease),
232
231
  background 160ms var(--ease),
233
232
  box-shadow 160ms var(--ease);
234
233
  }
235
234
 
236
- .theme-picker input:checked + span {
235
+ .theme-picker .theme-icon::before {
236
+ width: 1rem;
237
+ height: 1rem;
238
+ content: "";
239
+ display: block;
240
+ background: currentColor;
241
+ mask: center / contain no-repeat var(--theme-icon);
242
+ -webkit-mask: center / contain no-repeat var(--theme-icon);
243
+ }
244
+
245
+ .theme-icon-system {
246
+ --theme-icon: url("/icons/monitor.svg");
247
+ }
248
+
249
+ .theme-icon-light {
250
+ --theme-icon: url("/icons/sun.svg");
251
+ }
252
+
253
+ .theme-icon-dark {
254
+ --theme-icon: url("/icons/moon.svg");
255
+ }
256
+
257
+ .theme-picker input:checked + .theme-icon {
237
258
  color: var(--text);
238
259
  background: var(--surface);
239
260
  box-shadow: var(--shadow-sm);
240
261
  }
241
262
 
242
- .theme-picker input:focus-visible + span {
263
+ .theme-picker input:focus-visible + .theme-icon {
243
264
  outline: 3px solid #f2a94a;
244
265
  outline-offset: 2px;
245
266
  }
@@ -1050,10 +1071,6 @@ footer p {
1050
1071
  gap: 0.375rem;
1051
1072
  }
1052
1073
 
1053
- .theme-picker span {
1054
- padding-inline: 0.5rem;
1055
- }
1056
-
1057
1074
  .shell {
1058
1075
  width: min(100% - 1rem, 54rem);
1059
1076
  padding-top: 2rem;
package/src/web/server.js CHANGED
@@ -182,6 +182,9 @@ async function loadDefaultUiFiles() {
182
182
  readFile(new URL("../ui/theme.js", import.meta.url), "utf8"),
183
183
  readFile(new URL("../ui/time.js", import.meta.url), "utf8"),
184
184
  readFile(new URL("../ui/styles.css", import.meta.url), "utf8"),
185
+ readFile(new URL("../ui/icons/monitor.svg", import.meta.url), "utf8"),
186
+ readFile(new URL("../ui/icons/sun.svg", import.meta.url), "utf8"),
187
+ readFile(new URL("../ui/icons/moon.svg", import.meta.url), "utf8"),
185
188
  ]);
186
189
 
187
190
  return {
@@ -190,6 +193,9 @@ async function loadDefaultUiFiles() {
190
193
  "/theme.js": files[2],
191
194
  "/time.js": files[3],
192
195
  "/styles.css": files[4],
196
+ "/icons/monitor.svg": files[5],
197
+ "/icons/sun.svg": files[6],
198
+ "/icons/moon.svg": files[7],
193
199
  };
194
200
  }
195
201
 
@@ -421,9 +427,11 @@ function createRequestHandler(state) {
421
427
  const contentType =
422
428
  path.endsWith(".js")
423
429
  ? "text/javascript; charset=utf-8"
424
- : path === "/styles.css"
425
- ? "text/css; charset=utf-8"
426
- : "text/html; charset=utf-8";
430
+ : path.endsWith(".svg")
431
+ ? "image/svg+xml; charset=utf-8"
432
+ : path === "/styles.css"
433
+ ? "text/css; charset=utf-8"
434
+ : "text/html; charset=utf-8";
427
435
  const contents = state.uiFiles[path];
428
436
  response.writeHead(200, {
429
437
  "Content-Type": contentType,