relmio 0.2.8 → 0.2.10
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 +25 -0
- package/README.md +9 -0
- package/docs/troubleshooting.md +9 -4
- package/package.json +1 -1
- package/src/browser.js +60 -0
- package/src/cli.js +3 -20
- package/src/services/oauth.js +51 -7
- package/src/ui/app.js +10 -6
- package/src/ui/oauth-popup.js +18 -0
- package/src/web/server.js +8 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ checks the registry separately after publication.
|
|
|
7
7
|
|
|
8
8
|
## Unreleased
|
|
9
9
|
|
|
10
|
+
## [0.2.10] - 2026-08-03
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Probe installed Windows Node.js runtimes with the literal `node --version`
|
|
15
|
+
output instead of a `node -p` expression, avoiding the PowerShell `[eval]:1`
|
|
16
|
+
quoting failure while still reusing Node.js 22 or newer.
|
|
17
|
+
- Let interactive wizard terminals reopen the local browser page when the user
|
|
18
|
+
presses Enter, while retaining the printed private URL as the fallback for
|
|
19
|
+
noninteractive launches.
|
|
20
|
+
- Navigate the preopened local OAuth tab before severing its opener access,
|
|
21
|
+
show an immediate preparing state, and close an unnavigated waiting tab if
|
|
22
|
+
sign-in setup fails.
|
|
23
|
+
|
|
24
|
+
## [0.2.9] - 2026-08-02
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Launch the local ChatGPT OAuth helper through the current Windows Node.js
|
|
29
|
+
runtime and npm's JavaScript CLI instead of executing `npx.cmd` directly,
|
|
30
|
+
preventing `spawn EINVAL` while preserving the native `npx` path on macOS,
|
|
31
|
+
Linux, WSL, and Git Bash.
|
|
32
|
+
- Explain how to recover when a refreshed wizard page no longer has its private
|
|
33
|
+
session URL.
|
|
34
|
+
|
|
10
35
|
## [0.2.8] - 2026-08-02
|
|
11
36
|
|
|
12
37
|
### Changed
|
package/README.md
CHANGED
|
@@ -124,6 +124,15 @@ 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
|
+
|
|
131
|
+
If the browser does not open automatically, press Enter in an interactive
|
|
132
|
+
terminal to open it again. The printed `Local wizard:` URL remains the fallback
|
|
133
|
+
for a terminal that cannot accept input or when the launcher still cannot open
|
|
134
|
+
a browser.
|
|
135
|
+
|
|
127
136
|
### Requirements
|
|
128
137
|
|
|
129
138
|
- On macOS/Linux/WSL/Git Bash: `curl`, `awk`, `tar`, and either `sha256sum` or
|
package/docs/troubleshooting.md
CHANGED
|
@@ -66,9 +66,10 @@ npm view relmio version
|
|
|
66
66
|
npx --yes --ignore-scripts relmio@latest
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
Keep the terminal open. If the
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
Keep the terminal open. If the running wizard accepts terminal input, press
|
|
70
|
+
Enter to ask it to open the browser again. Otherwise, copy the newest printed
|
|
71
|
+
`http://127.0.0.1:...` URL into the browser. That URL contains a temporary setup
|
|
72
|
+
token: do not post it in an issue or screenshot.
|
|
72
73
|
|
|
73
74
|
You do not need to sign in to npm, configure npm 2FA, or own this package to
|
|
74
75
|
run any public command. npm authentication is required only for the
|
|
@@ -113,9 +114,13 @@ bypassed.
|
|
|
113
114
|
| `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
115
|
| `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
116
|
| 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
|
-
|
|
|
117
|
+
| Windows PowerShell shows `[eval]:1` before the wizard starts | An older bootstrap passed a JavaScript expression through `node -p`; PowerShell native-argument handling can alter that expression. | Update to the latest `relmio@latest` and rerun the same PowerShell or Command Prompt command. The current bootstrap parses the literal `node --version` output and reuses Node.js 22 or newer. |
|
|
118
|
+
| 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. |
|
|
119
|
+
| The browser did not open | The automatic browser launch failed, but the local server may still be running. | Keep the newest terminal open and press Enter to ask an interactive wizard terminal to open it again. If the terminal is noninteractive or the launcher still fails, copy its newest `127.0.0.1` setup URL into the browser. Do not reuse a URL from a closed terminal. |
|
|
120
|
+
| 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
121
|
| 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
122
|
| `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`. |
|
|
123
|
+
| A white `about:blank` tab remains after selecting **Sign in with ChatGPT** | An older local wizard cleared the preopened tab's `opener` before navigating it to the validated sign-in URL. | Update to the latest `relmio@latest`, close the stranded tab, and start one fresh sign-in. The current wizard shows a preparing state, navigates the preopened tab, then clears its opener before the remote page loads. Use **Open fresh ChatGPT sign-in** only when the current wizard shows that validated manual link. |
|
|
119
124
|
| `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. |
|
|
120
125
|
| An **OpenAI OAuth** extension page says the sign-in request expired | A browser extension intercepted the `localhost:1455` callback that belongs to the wizard's fresh login. | Temporarily disable the **Sign in with ChatGPT** or **OpenAI OAuth** extension, then select **Refresh ChatGPT sign-in** in the wizard. Re-enable the extension afterward if you still use it elsewhere. |
|
|
121
126
|
| `ChatGPT sign-in did not finish` appears immediately when refreshing an existing credential | Wizard versions through `0.1.3` attempted to reuse `~/.codex/auth.json`, but the bridge CLI requires an interactive terminal before replacing that file. | Update to `0.1.4` or newer. The wizard signs in through its own new credential file and leaves the Codex app credential untouched. |
|
package/package.json
CHANGED
package/src/browser.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export function browserCommand(url, platform = process.platform) {
|
|
4
|
+
if (platform === "darwin") {
|
|
5
|
+
return { file: "open", args: [url] };
|
|
6
|
+
}
|
|
7
|
+
if (platform === "win32") {
|
|
8
|
+
return { file: "explorer.exe", args: [url] };
|
|
9
|
+
}
|
|
10
|
+
return { file: "xdg-open", args: [url] };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function openBrowser(
|
|
14
|
+
url,
|
|
15
|
+
{ platform = process.platform, spawnProcess = spawn } = {},
|
|
16
|
+
) {
|
|
17
|
+
const command = browserCommand(url, platform);
|
|
18
|
+
let child;
|
|
19
|
+
try {
|
|
20
|
+
child = spawnProcess(command.file, command.args, {
|
|
21
|
+
detached: true,
|
|
22
|
+
stdio: "ignore",
|
|
23
|
+
shell: false,
|
|
24
|
+
});
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
child.once("error", () => {
|
|
29
|
+
// The URL is also printed, so users can open it manually.
|
|
30
|
+
});
|
|
31
|
+
child.unref();
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function attachBrowserReopenOnEnter({
|
|
36
|
+
input = process.stdin,
|
|
37
|
+
url,
|
|
38
|
+
open = openBrowser,
|
|
39
|
+
write = console.log,
|
|
40
|
+
}) {
|
|
41
|
+
if (!input.isTTY) {
|
|
42
|
+
return () => {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const onData = (data) => {
|
|
46
|
+
if (/\r|\n/u.test(String(data))) {
|
|
47
|
+
open(url);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
input.setEncoding?.("utf8");
|
|
52
|
+
input.resume?.();
|
|
53
|
+
input.on("data", onData);
|
|
54
|
+
write("If the wizard did not open automatically, press Enter to open it again.");
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
input.off("data", onData);
|
|
58
|
+
input.pause?.();
|
|
59
|
+
};
|
|
60
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -1,29 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
4
|
|
|
5
|
+
import { attachBrowserReopenOnEnter, openBrowser } from "./browser.js";
|
|
6
6
|
import { startWizardServer } from "./web/server.js";
|
|
7
7
|
|
|
8
|
-
function openBrowser(url) {
|
|
9
|
-
const command =
|
|
10
|
-
process.platform === "darwin"
|
|
11
|
-
? { file: "open", args: [url] }
|
|
12
|
-
: process.platform === "win32"
|
|
13
|
-
? { file: "explorer.exe", args: [url] }
|
|
14
|
-
: { file: "xdg-open", args: [url] };
|
|
15
|
-
|
|
16
|
-
const child = spawn(command.file, command.args, {
|
|
17
|
-
detached: true,
|
|
18
|
-
stdio: "ignore",
|
|
19
|
-
shell: false,
|
|
20
|
-
});
|
|
21
|
-
child.once("error", () => {
|
|
22
|
-
// The URL is also printed, so users can open it manually.
|
|
23
|
-
});
|
|
24
|
-
child.unref();
|
|
25
|
-
}
|
|
26
|
-
|
|
27
8
|
const sessionToken = randomBytes(32).toString("base64url");
|
|
28
9
|
const wizard = await startWizardServer({ sessionToken });
|
|
29
10
|
const url = `${wizard.origin}/?session=${sessionToken}`;
|
|
@@ -39,6 +20,7 @@ console.log("Press Control+C to stop.");
|
|
|
39
20
|
console.log("");
|
|
40
21
|
|
|
41
22
|
openBrowser(url);
|
|
23
|
+
const detachBrowserReopen = attachBrowserReopenOnEnter({ url });
|
|
42
24
|
|
|
43
25
|
let closing = false;
|
|
44
26
|
async function close() {
|
|
@@ -46,6 +28,7 @@ async function close() {
|
|
|
46
28
|
return;
|
|
47
29
|
}
|
|
48
30
|
closing = true;
|
|
31
|
+
detachBrowserReopen();
|
|
49
32
|
await wizard.close();
|
|
50
33
|
}
|
|
51
34
|
|
package/src/services/oauth.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatAuthUpdatedAt } from "./time.js";
|
|
2
|
+
import { prepareOAuthPopup } from "./oauth-popup.js";
|
|
2
3
|
|
|
3
4
|
const token = new URLSearchParams(window.location.search).get("session");
|
|
4
5
|
window.history.replaceState(null, "", window.location.pathname);
|
|
@@ -146,7 +147,9 @@ function showStep(step) {
|
|
|
146
147
|
|
|
147
148
|
async function api(path, { method = "GET", body } = {}) {
|
|
148
149
|
if (!token) {
|
|
149
|
-
throw new Error(
|
|
150
|
+
throw new Error(
|
|
151
|
+
"This wizard link is incomplete. Close this tab and open the full URL printed by the active setup terminal.",
|
|
152
|
+
);
|
|
150
153
|
}
|
|
151
154
|
|
|
152
155
|
let response;
|
|
@@ -307,15 +310,14 @@ element("login-button").addEventListener("click", async (event) => {
|
|
|
307
310
|
const button = event.currentTarget;
|
|
308
311
|
const loginLink = element("login-link");
|
|
309
312
|
const loginWindow = window.open("about:blank", "_blank");
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
+
let loginWindowNavigated = false;
|
|
314
|
+
prepareOAuthPopup(loginWindow);
|
|
313
315
|
clearError();
|
|
314
316
|
loginLink.hidden = true;
|
|
315
317
|
loginLink.removeAttribute("href");
|
|
316
318
|
setBusy(button, true, "Waiting for browser sign-in…");
|
|
317
319
|
setMessage(
|
|
318
|
-
"
|
|
320
|
+
"Preparing a fresh ChatGPT sign-in. The existing local credential will be replaced only after sign-in succeeds.",
|
|
319
321
|
);
|
|
320
322
|
try {
|
|
321
323
|
const result = await api("/api/oauth/login", {
|
|
@@ -327,6 +329,8 @@ element("login-button").addEventListener("click", async (event) => {
|
|
|
327
329
|
loginLink.hidden = false;
|
|
328
330
|
if (loginWindow) {
|
|
329
331
|
loginWindow.location.replace(authorizationUrl);
|
|
332
|
+
loginWindowNavigated = true;
|
|
333
|
+
loginWindow.opener = null;
|
|
330
334
|
}
|
|
331
335
|
setMessage(
|
|
332
336
|
"Complete the newly opened sign-in within five minutes. If no tab opened, use “Open fresh ChatGPT sign-in” below. If an OpenAI OAuth browser extension intercepts the callback, disable it temporarily and start again.",
|
|
@@ -336,7 +340,7 @@ element("login-button").addEventListener("click", async (event) => {
|
|
|
336
340
|
loginLink.removeAttribute("href");
|
|
337
341
|
await refreshAuthStatus({ fresh: true });
|
|
338
342
|
} catch (error) {
|
|
339
|
-
if (loginWindow &&
|
|
343
|
+
if (loginWindow && !loginWindowNavigated) {
|
|
340
344
|
loginWindow.close();
|
|
341
345
|
}
|
|
342
346
|
showError(error);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function prepareOAuthPopup(loginWindow) {
|
|
2
|
+
if (!loginWindow) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
const popupDocument = loginWindow.document;
|
|
8
|
+
const heading = popupDocument.createElement("h1");
|
|
9
|
+
const detail = popupDocument.createElement("p");
|
|
10
|
+
heading.textContent = "Preparing ChatGPT sign-in…";
|
|
11
|
+
detail.textContent = "Relmio is creating a fresh sign-in link.";
|
|
12
|
+
popupDocument.title = "Preparing ChatGPT sign-in";
|
|
13
|
+
popupDocument.body.replaceChildren(heading, detail);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/web/server.js
CHANGED
|
@@ -179,6 +179,7 @@ async function loadDefaultUiFiles() {
|
|
|
179
179
|
const files = await Promise.all([
|
|
180
180
|
readFile(new URL("../ui/index.html", import.meta.url), "utf8"),
|
|
181
181
|
readFile(new URL("../ui/app.js", import.meta.url), "utf8"),
|
|
182
|
+
readFile(new URL("../ui/oauth-popup.js", import.meta.url), "utf8"),
|
|
182
183
|
readFile(new URL("../ui/theme.js", import.meta.url), "utf8"),
|
|
183
184
|
readFile(new URL("../ui/time.js", import.meta.url), "utf8"),
|
|
184
185
|
readFile(new URL("../ui/styles.css", import.meta.url), "utf8"),
|
|
@@ -190,12 +191,13 @@ async function loadDefaultUiFiles() {
|
|
|
190
191
|
return {
|
|
191
192
|
"/": files[0],
|
|
192
193
|
"/app.js": files[1],
|
|
193
|
-
"/
|
|
194
|
-
"/
|
|
195
|
-
"/
|
|
196
|
-
"/
|
|
197
|
-
"/icons/
|
|
198
|
-
"/icons/
|
|
194
|
+
"/oauth-popup.js": files[2],
|
|
195
|
+
"/theme.js": files[3],
|
|
196
|
+
"/time.js": files[4],
|
|
197
|
+
"/styles.css": files[5],
|
|
198
|
+
"/icons/monitor.svg": files[6],
|
|
199
|
+
"/icons/sun.svg": files[7],
|
|
200
|
+
"/icons/moon.svg": files[8],
|
|
199
201
|
};
|
|
200
202
|
}
|
|
201
203
|
|