jeo-code 0.9.12 → 0.9.14
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 +30 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +1 -1
- package/src/auth/browser-tab.ts +182 -0
- package/src/auth/callback-server.ts +68 -20
- package/src/commands/launch.ts +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,36 @@ The README mirrors the latest 5 entries — regenerate with `bun run changelog:s
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.9.14] - 2026-08-04
|
|
12
|
+
_A `$a $b … [intent]` skill-chain invocation of a bundled workflow skill (deep-interview/ralplan/team/ultragoal) hung forever with no error and no prompt._
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **`$a $b …` skill-chain workflow invocations hung silently** (`src/commands/launch.ts`) — the chain path (`runOneSkillShot`) wired `io.output` to a bundled workflow engine but omitted `io.input`. `runDeepInterviewEngine` (and the other workflow engines) only spin up their own `readline.Interface` on `process.stdin` when `opts.io?.input` is absent; the interactive REPL already owns a `readline.Interface` on the same stdin via `promptInput`, so the two interfaces silently fought over raw mode — no prompt was ever drawn and no input was ever delivered, so the workflow just hung forever. The sibling single-invocation path (`runSkillInvocation`) already wired `promptInput` through `io.input`; the chain path now does the same (including the preview-arm/disarm dance around the prompt).
|
|
16
|
+
|
|
17
|
+
### Verified
|
|
18
|
+
- `bun run typecheck` clean.
|
|
19
|
+
- `test/deep-interview-noninteractive.test.ts`, `test/launch-flags.test.ts` pass.
|
|
20
|
+
- `test/launch-repl-eof.test.ts`'s 2 failures are pre-existing (reproduce identically on the unmodified branch).
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## [0.9.13] - 2026-08-03
|
|
24
|
+
_The provider-login browser tab now actually goes away — by itself on macOS, by a working button everywhere else._
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- **The OAuth login tab stayed open after "Login complete"** (`src/auth/callback-server.ts`, new `src/auth/browser-tab.ts`) — two separate defects:
|
|
28
|
+
- The page decided whether the close worked from `window.close()`'s return value, which is `undefined` on EVERY browser. The failure branch therefore always ran and hid the "Close" button on the first countdown tick, so the tab was left open with its only manual control gone. Success is now inferred from the document actually going away (`pagehide` / `document.hidden`); the button stays visible and re-attempts on every press, and the countdown hint is replaced by an explicit "press ⌘W / Ctrl+W" instruction only when the browser refuses.
|
|
29
|
+
- `window.close()` cannot close a tab the page's own script did not open, and jeo hands the auth URL to the OS (`open` / `xdg-open` / `start`), so the request was always ignored. jeo now closes the tab from the OUTSIDE on macOS: after the callback page is served, it lists running processes (no Automation permission needed), builds an AppleScript naming ONLY the browsers that are actually running, and closes just the tabs whose URL contains this login's `localhost:<port>/callback`. `window.open("","_self")` is also tried first for the engines that still honor the legacy self-adopt, and the countdown is 3s instead of 5s.
|
|
30
|
+
- **A single missing browser silently disabled the whole close script** — AppleScript resolves app terminology at COMPILE time, which `try` cannot guard, so one static `tell application "Arc"` block on a machine without Arc made the entire script fail to compile and close nothing (`osacompile`: "Expected class name but found property"). The script is now generated only for browsers found in the live process list.
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
- `JEO_AUTH_TAB_CLOSE=0` opts out of the OS-level tab close entirely; the attempt is hard-capped at 2s, never surfaces an error, and is skipped when the login finished through a manual code paste (no tab of ours was ever rendered).
|
|
34
|
+
|
|
35
|
+
### Verified
|
|
36
|
+
- Real Chromium (Playwright) against the actual served page: the scripted close is refused exactly as it is for a real OAuth tab, and the page survives intact — URL unchanged (`window.open("","_self")` does not blank it), "Login complete ✓" still rendered, countdown hint swapped for the manual instruction, Close button still visible; `window.close()` is invoked once by the countdown and again on every button press (the old build stopped after the first).
|
|
37
|
+
- `osacompile` on the generated AppleScript: fails on the full browser list (reproducing the compile bug) and compiles cleanly for the running-process-derived list.
|
|
38
|
+
- `closeAuthTab` against a live loopback callback URL returns in ~2s without hanging when macOS withholds Automation permission, so a login can never stall behind it.
|
|
39
|
+
- Suites: oauth, browser-tab, openai-oauth, antigravity-login, kimi-oauth, oauth-lock, auth-matrix, doctor — 53 pass / 0 fail. `bun run typecheck` clean, `bun run pack:check` 283 files.
|
|
40
|
+
|
|
11
41
|
## [0.9.12] - 2026-08-02
|
|
12
42
|
_Fixes the boxed prompt's stolen keystrokes, drifting caret, and paste/attachment spacing under `jeo --tmux`._
|
|
13
43
|
|
package/README.ja.md
CHANGED
|
@@ -287,10 +287,10 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
287
287
|
|
|
288
288
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
289
289
|
- **[Unreleased]**
|
|
290
|
+
- **[0.9.14]** (2026-08-04) — A `$a $b … [intent]` skill-chain invocation of a bundled workflow skill (deep-interview/ralplan/team/ultragoal) hung forever with no error and no prompt.
|
|
291
|
+
- **[0.9.13]** (2026-08-03) — The provider-login browser tab now actually goes away — by itself on macOS, by a working button everywhere else.
|
|
290
292
|
- **[0.9.12]** (2026-08-02) — Fixes the boxed prompt's stolen keystrokes, drifting caret, and paste/attachment spacing under `jeo --tmux`.
|
|
291
293
|
- **[0.9.11]** (2026-07-30) — Imports GJC v5 sessions safely, exposes bounded slash controls, and gates the release artifact before publication.
|
|
292
|
-
- **[0.9.10]** (2026-07-27) — `jeo update` now verifies the binary you actually run, and recovers from bun's stale registry cache.
|
|
293
|
-
- **[0.9.9]** (2026-07-27) — Root-causes the frozen `jeo --tmux` TUI: the window was pinned to its launch size, so resizing the terminal only cut the view.
|
|
294
294
|
|
|
295
295
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
296
296
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -288,10 +288,10 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
288
288
|
|
|
289
289
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
290
290
|
- **[Unreleased]**
|
|
291
|
+
- **[0.9.14]** (2026-08-04) — A `$a $b … [intent]` skill-chain invocation of a bundled workflow skill (deep-interview/ralplan/team/ultragoal) hung forever with no error and no prompt.
|
|
292
|
+
- **[0.9.13]** (2026-08-03) — The provider-login browser tab now actually goes away — by itself on macOS, by a working button everywhere else.
|
|
291
293
|
- **[0.9.12]** (2026-08-02) — Fixes the boxed prompt's stolen keystrokes, drifting caret, and paste/attachment spacing under `jeo --tmux`.
|
|
292
294
|
- **[0.9.11]** (2026-07-30) — Imports GJC v5 sessions safely, exposes bounded slash controls, and gates the release artifact before publication.
|
|
293
|
-
- **[0.9.10]** (2026-07-27) — `jeo update` now verifies the binary you actually run, and recovers from bun's stale registry cache.
|
|
294
|
-
- **[0.9.9]** (2026-07-27) — Root-causes the frozen `jeo --tmux` TUI: the window was pinned to its launch size, so resizing the terminal only cut the view.
|
|
295
295
|
|
|
296
296
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
297
297
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -295,10 +295,10 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
295
295
|
|
|
296
296
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
297
297
|
- **[Unreleased]**
|
|
298
|
+
- **[0.9.14]** (2026-08-04) — A `$a $b … [intent]` skill-chain invocation of a bundled workflow skill (deep-interview/ralplan/team/ultragoal) hung forever with no error and no prompt.
|
|
299
|
+
- **[0.9.13]** (2026-08-03) — The provider-login browser tab now actually goes away — by itself on macOS, by a working button everywhere else.
|
|
298
300
|
- **[0.9.12]** (2026-08-02) — Fixes the boxed prompt's stolen keystrokes, drifting caret, and paste/attachment spacing under `jeo --tmux`.
|
|
299
301
|
- **[0.9.11]** (2026-07-30) — Imports GJC v5 sessions safely, exposes bounded slash controls, and gates the release artifact before publication.
|
|
300
|
-
- **[0.9.10]** (2026-07-27) — `jeo update` now verifies the binary you actually run, and recovers from bun's stale registry cache.
|
|
301
|
-
- **[0.9.9]** (2026-07-27) — Root-causes the frozen `jeo --tmux` TUI: the window was pinned to its launch size, so resizing the terminal only cut the view.
|
|
302
302
|
|
|
303
303
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
304
304
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -287,10 +287,10 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
287
287
|
|
|
288
288
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
289
289
|
- **[Unreleased]**
|
|
290
|
+
- **[0.9.14]** (2026-08-04) — A `$a $b … [intent]` skill-chain invocation of a bundled workflow skill (deep-interview/ralplan/team/ultragoal) hung forever with no error and no prompt.
|
|
291
|
+
- **[0.9.13]** (2026-08-03) — The provider-login browser tab now actually goes away — by itself on macOS, by a working button everywhere else.
|
|
290
292
|
- **[0.9.12]** (2026-08-02) — Fixes the boxed prompt's stolen keystrokes, drifting caret, and paste/attachment spacing under `jeo --tmux`.
|
|
291
293
|
- **[0.9.11]** (2026-07-30) — Imports GJC v5 sessions safely, exposes bounded slash controls, and gates the release artifact before publication.
|
|
292
|
-
- **[0.9.10]** (2026-07-27) — `jeo update` now verifies the binary you actually run, and recovers from bun's stale registry cache.
|
|
293
|
-
- **[0.9.9]** (2026-07-27) — Root-causes the frozen `jeo --tmux` TUI: the window was pinned to its launch size, so resizing the terminal only cut the view.
|
|
294
294
|
|
|
295
295
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
296
296
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort closing of the OAuth callback TAB the login opened.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: the callback page already asks the browser to close itself
|
|
5
|
+
* (`window.close()` after a short countdown), but every current browser REFUSES
|
|
6
|
+
* that for a tab the page's own script did not open. jeo hands the auth URL to
|
|
7
|
+
* the OS (`open` / `xdg-open` / `start`), so the tab is user-opened by that rule
|
|
8
|
+
* and the request is silently ignored — the login succeeds in the terminal while
|
|
9
|
+
* a dead "Login complete" tab lingers in the browser (the reported bug).
|
|
10
|
+
*
|
|
11
|
+
* The only reliable way to close such a tab is to drive the browser from the
|
|
12
|
+
* outside. macOS exposes exactly that through AppleScript, so this module builds
|
|
13
|
+
* a targeted script that closes ONLY tabs whose URL contains the loopback
|
|
14
|
+
* callback (e.g. `localhost:1455/callback`) — never any other tab — in whichever
|
|
15
|
+
* of the common browsers is ALREADY running (`is running` never launches one).
|
|
16
|
+
*
|
|
17
|
+
* Everything here is best effort and failure-silent: no automation permission, a
|
|
18
|
+
* browser without an AppleScript dictionary (Firefox), Linux/Windows, or a user
|
|
19
|
+
* who already closed the tab all end as a no-op. `JEO_AUTH_TAB_CLOSE=0` opts out.
|
|
20
|
+
*/
|
|
21
|
+
import { jeoEnv } from "../util/env";
|
|
22
|
+
|
|
23
|
+
/** Chromium-family browsers whose AppleScript dictionary exposes `windows`/`tabs`
|
|
24
|
+
* with a `URL` property and a `close` command. Safari uses the same shape and is
|
|
25
|
+
* appended by {@link closeAuthTabScript}. Firefox has no tab-level dictionary at
|
|
26
|
+
* all, so it cannot be driven this way — its tab keeps the page's own fallback. */
|
|
27
|
+
export const APPLESCRIPT_BROWSERS: readonly string[] = [
|
|
28
|
+
"Google Chrome",
|
|
29
|
+
"Google Chrome Beta",
|
|
30
|
+
"Google Chrome Canary",
|
|
31
|
+
"Chromium",
|
|
32
|
+
"Brave Browser",
|
|
33
|
+
"Microsoft Edge",
|
|
34
|
+
"Vivaldi",
|
|
35
|
+
"Arc",
|
|
36
|
+
"Safari",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** Escape a string for embedding inside an AppleScript double-quoted literal. */
|
|
40
|
+
function escapeAppleScript(value: string): string {
|
|
41
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The AppleScript that closes every tab whose URL contains `fragment`.
|
|
46
|
+
*
|
|
47
|
+
* One STATIC `tell` block per browser (never a dynamic `tell application appName`):
|
|
48
|
+
* app-specific terms like `URL of t` only compile inside a literal tell block, so a
|
|
49
|
+
* variable-driven loop would fail to compile and close nothing.
|
|
50
|
+
*
|
|
51
|
+
* Pass ONLY browsers that are actually running (see {@link runningBrowserApps}). A
|
|
52
|
+
* block naming an app that is not INSTALLED cannot resolve its terminology and
|
|
53
|
+
* fails at COMPILE time — which `try` cannot catch, since `try` guards runtime
|
|
54
|
+
* errors only — so one missing browser would sink the whole script and close
|
|
55
|
+
* nothing (verified with `osacompile`: "Expected class name but found property").
|
|
56
|
+
* Each block keeps its own `try` + `is running` guard for the runtime races: a
|
|
57
|
+
* browser quitting mid-script, or a window that refuses the query.
|
|
58
|
+
*/
|
|
59
|
+
export function closeAuthTabScript(fragment: string, browsers: readonly string[] = APPLESCRIPT_BROWSERS): string {
|
|
60
|
+
const frag = escapeAppleScript(fragment);
|
|
61
|
+
const blocks = browsers.map(
|
|
62
|
+
app => `try
|
|
63
|
+
if application "${escapeAppleScript(app)}" is running then
|
|
64
|
+
tell application "${escapeAppleScript(app)}"
|
|
65
|
+
repeat with w in (every window)
|
|
66
|
+
repeat with t in (every tab of w)
|
|
67
|
+
try
|
|
68
|
+
if (URL of t) contains "${frag}" then close t
|
|
69
|
+
end try
|
|
70
|
+
end repeat
|
|
71
|
+
end repeat
|
|
72
|
+
end tell
|
|
73
|
+
end if
|
|
74
|
+
end try`,
|
|
75
|
+
);
|
|
76
|
+
return blocks.join("\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The subset of {@link APPLESCRIPT_BROWSERS} currently running, derived from
|
|
80
|
+
* `ps -Ax -o comm=` output (each line is an executable path such as
|
|
81
|
+
* `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`). Reading the
|
|
82
|
+
* process list needs no Automation permission, so this narrows the generated
|
|
83
|
+
* script — and with it the compile surface and the number of permission prompts —
|
|
84
|
+
* to the browsers that could actually be holding the callback tab. */
|
|
85
|
+
export function runningBrowserApps(psOutput: string, browsers: readonly string[] = APPLESCRIPT_BROWSERS): string[] {
|
|
86
|
+
const lines = psOutput.split("\n");
|
|
87
|
+
return browsers.filter(app => lines.some(line => line.includes(`/${app}.app/`)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The command to run for `platform`, or null when the platform has no reliable
|
|
91
|
+
* way to close a tab it did not open (Linux/Windows: no scriptable, browser-
|
|
92
|
+
* agnostic tab API — the callback page's own message is the fallback there). */
|
|
93
|
+
export function browserTabCloseCommand(
|
|
94
|
+
platform: NodeJS.Platform,
|
|
95
|
+
fragment: string,
|
|
96
|
+
browsers: readonly string[] = APPLESCRIPT_BROWSERS,
|
|
97
|
+
): string[] | null {
|
|
98
|
+
if (platform !== "darwin") return null;
|
|
99
|
+
if (!fragment) return null;
|
|
100
|
+
return ["osascript", "-e", closeAuthTabScript(fragment, browsers)];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The `host:port/path` slice of a redirect URI — the narrowest fragment that still
|
|
104
|
+
* identifies THIS login's callback tab (the port is unique per flow/run), so no
|
|
105
|
+
* unrelated tab can ever match. Returns "" when the URI cannot be parsed. */
|
|
106
|
+
export function callbackUrlFragment(redirectUri: string): string {
|
|
107
|
+
try {
|
|
108
|
+
const url = new URL(redirectUri);
|
|
109
|
+
return `${url.host}${url.pathname}`;
|
|
110
|
+
} catch {
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface CloseAuthTabOptions {
|
|
116
|
+
platform?: NodeJS.Platform;
|
|
117
|
+
env?: Record<string, string | undefined>;
|
|
118
|
+
/** Hard cap so a hung/permission-prompting osascript can never stall the login.
|
|
119
|
+
* The FIRST close on a machine raises macOS's "control Google Chrome" Automation
|
|
120
|
+
* prompt; if the user does not answer in time this kills the helper and the page's
|
|
121
|
+
* own fallback message takes over. Answering it once makes later logins instant. */
|
|
122
|
+
timeoutMs?: number;
|
|
123
|
+
/** Injected for tests; defaults to the real spawn. */
|
|
124
|
+
spawn?: (cmd: string[]) => { exited: Promise<number>; kill: () => void };
|
|
125
|
+
/** Running-process listing (`ps -Ax -o comm=`); injected for tests. Returning ""
|
|
126
|
+
* means "cannot tell", which is treated as "no known browser running". */
|
|
127
|
+
listProcesses?: () => string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Close the browser tab still showing this login's callback page. Resolves `true`
|
|
132
|
+
* only when the OS accepted the request; every other outcome (unsupported
|
|
133
|
+
* platform, opt-out, spawn failure, timeout, non-zero exit) resolves `false` and
|
|
134
|
+
* is silent — the terminal already reported the login result, so this must never
|
|
135
|
+
* surface an error of its own.
|
|
136
|
+
*/
|
|
137
|
+
export async function closeAuthTab(redirectUri: string, opts: CloseAuthTabOptions = {}): Promise<boolean> {
|
|
138
|
+
const env = opts.env ?? process.env;
|
|
139
|
+
if (jeoEnv("AUTH_TAB_CLOSE", env) === "0") return false;
|
|
140
|
+
const platform = opts.platform ?? process.platform;
|
|
141
|
+
if (platform !== "darwin") return false;
|
|
142
|
+
const fragment = callbackUrlFragment(redirectUri);
|
|
143
|
+
if (!fragment) return false;
|
|
144
|
+
const listProcesses =
|
|
145
|
+
opts.listProcesses ??
|
|
146
|
+
(() => {
|
|
147
|
+
try {
|
|
148
|
+
return Bun.spawnSync(["ps", "-Ax", "-o", "comm="], { stdout: "pipe", stderr: "ignore" }).stdout.toString();
|
|
149
|
+
} catch {
|
|
150
|
+
return "";
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
// Never script a browser that is not running: an uninstalled app breaks the whole
|
|
154
|
+
// script at compile time, and a running-only list keeps the Automation prompt to
|
|
155
|
+
// the browser the user actually has open.
|
|
156
|
+
const browsers = runningBrowserApps(listProcesses());
|
|
157
|
+
if (browsers.length === 0) return false;
|
|
158
|
+
const cmd = browserTabCloseCommand(platform, fragment, browsers);
|
|
159
|
+
if (!cmd) return false;
|
|
160
|
+
const timeoutMs = opts.timeoutMs ?? 2000;
|
|
161
|
+
const spawn =
|
|
162
|
+
opts.spawn ??
|
|
163
|
+
((c: string[]) => {
|
|
164
|
+
const proc = Bun.spawn(c, { stdout: "ignore", stderr: "ignore", stdin: "ignore" });
|
|
165
|
+
return { exited: proc.exited, kill: () => proc.kill() };
|
|
166
|
+
});
|
|
167
|
+
try {
|
|
168
|
+
const proc = spawn(cmd);
|
|
169
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
170
|
+
const timeout = new Promise<number>(resolve => {
|
|
171
|
+
timer = setTimeout(() => {
|
|
172
|
+
try { proc.kill(); } catch { /* already gone */ }
|
|
173
|
+
resolve(-1);
|
|
174
|
+
}, timeoutMs);
|
|
175
|
+
});
|
|
176
|
+
const code = await Promise.race([proc.exited, timeout]);
|
|
177
|
+
if (timer) clearTimeout(timer);
|
|
178
|
+
return code === 0;
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { OAuthController, OAuthCredentials } from "./types";
|
|
11
11
|
import { generateState } from "./pkce";
|
|
12
|
+
import { closeAuthTab } from "./browser-tab";
|
|
12
13
|
|
|
13
14
|
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
14
15
|
// Loopback callback host. `localhost` (not the 127.0.0.1 IP literal) is the intentional
|
|
@@ -64,34 +65,58 @@ button:hover{background:#1b1c27}
|
|
|
64
65
|
<img class="wordmark" alt="jeo" src="${JEO_WORDMARK_DATA_URI}">
|
|
65
66
|
<h1 class="__STATUS__">__TITLE__</h1>
|
|
66
67
|
<p>__MSG__</p>
|
|
67
|
-
<button id="jeo-close" type="button">Close</button>
|
|
68
|
-
<p class="hint">Closing automatically in <span id="jeo-countdown">__SECONDS__</span>s…</p>
|
|
69
|
-
<p class="hint closed" id="jeo-closed-msg">
|
|
68
|
+
<button id="jeo-close" type="button">Close this tab</button>
|
|
69
|
+
<p class="hint" id="jeo-countdown-msg">Closing automatically in <span id="jeo-countdown">__SECONDS__</span>s…</p>
|
|
70
|
+
<p class="hint closed" id="jeo-closed-msg">This browser refuses to close a tab it opened itself — press \u2318W (Ctrl+W) to close it.</p>
|
|
70
71
|
</div>
|
|
71
72
|
<script>__SCRIPT__</script>
|
|
72
73
|
</body></html>`;
|
|
73
74
|
|
|
74
|
-
// Seconds before the tab
|
|
75
|
-
// failure page
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
const AUTO_CLOSE_SECONDS = 5;
|
|
75
|
+
// Seconds before the tab tries to close itself — applies to BOTH the success and
|
|
76
|
+
// the failure page (each is a final page that should not linger). Pressing
|
|
77
|
+
// "Close this tab" runs the same attempt immediately, without waiting out the
|
|
78
|
+
// countdown, and stays clickable afterwards.
|
|
79
|
+
const AUTO_CLOSE_SECONDS = 3;
|
|
80
80
|
|
|
81
81
|
function renderHtml(ok: boolean, msg: string): string {
|
|
82
|
+
// Closing contract, in order of what actually works:
|
|
83
|
+
// 1. `window.open("","_self")` before `window.close()` — the legacy self-adopt
|
|
84
|
+
// trick that still lets Firefox (and older engines) close an OS-opened tab.
|
|
85
|
+
// 2. `window.close()` — succeeds only where the tab counts as script-opened.
|
|
86
|
+
// 3. If the tab is still here a moment later, the browser refused: tell the
|
|
87
|
+
// user how to close it and KEEP the button working.
|
|
88
|
+
//
|
|
89
|
+
// The old code branched on `window.close()`'s return value — that call returns
|
|
90
|
+
// `undefined` ALWAYS, so the failure branch fired unconditionally and hid the
|
|
91
|
+
// Close button on the first tick. That left the tab open with its only manual
|
|
92
|
+
// control gone (the "인증 브라우저가 종료되지 않는다" report). Success is now
|
|
93
|
+
// detected by observing that the document is actually going away
|
|
94
|
+
// (`document.hidden` / the `pagehide` unload path), never by a return value.
|
|
95
|
+
//
|
|
96
|
+
// The CLI side additionally closes the tab through the OS on macOS once the
|
|
97
|
+
// callback lands (`closeAuthTab` in ./browser-tab.ts), which is the only path
|
|
98
|
+
// that works when the browser refuses the scripted close outright.
|
|
82
99
|
const script = `(function(){
|
|
83
|
-
var n=${AUTO_CLOSE_SECONDS},
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
var n=${AUTO_CLOSE_SECONDS},
|
|
101
|
+
el=document.getElementById("jeo-countdown"),
|
|
102
|
+
countdownMsg=document.getElementById("jeo-countdown-msg"),
|
|
103
|
+
btn=document.getElementById("jeo-close"),
|
|
104
|
+
closedMsg=document.getElementById("jeo-closed-msg"),
|
|
105
|
+
t,gone=false;
|
|
106
|
+
window.addEventListener("pagehide",function(){gone=true;});
|
|
107
|
+
function showManual(){
|
|
108
|
+
if(gone||document.hidden)return;
|
|
109
|
+
if(countdownMsg)countdownMsg.style.display="none";
|
|
110
|
+
if(closedMsg)closedMsg.classList.remove("closed");
|
|
111
|
+
}
|
|
112
|
+
function attemptClose(){
|
|
113
|
+
if(t){clearInterval(t);t=null;}
|
|
114
|
+
try{window.open("","_self");}catch(e){}
|
|
115
|
+
try{window.close();}catch(e){}
|
|
116
|
+
setTimeout(showManual,250);
|
|
92
117
|
}
|
|
93
|
-
t=setInterval(function(){n-=1;if(el)el.textContent=String(Math.max(n,0));if(n<=0)
|
|
94
|
-
if(btn)btn.addEventListener("click",
|
|
118
|
+
t=setInterval(function(){n-=1;if(el)el.textContent=String(Math.max(n,0));if(n<=0)attemptClose();},1000);
|
|
119
|
+
if(btn)btn.addEventListener("click",attemptClose);
|
|
95
120
|
})();`;
|
|
96
121
|
return PAGE_HTML
|
|
97
122
|
.replace("__STATUS__", ok ? "ok" : "fail")
|
|
@@ -110,6 +135,9 @@ export abstract class OAuthCallbackFlow {
|
|
|
110
135
|
protected fixedRedirectUri?: string;
|
|
111
136
|
#resolve?: (r: CallbackResult) => void;
|
|
112
137
|
#reject?: (e: Error) => void;
|
|
138
|
+
/** Set for the duration of one `login()`; fired the moment the callback PAGE is
|
|
139
|
+
* served, which is the only proof a browser tab of ours exists to close. */
|
|
140
|
+
#onPageServed?: () => void;
|
|
113
141
|
|
|
114
142
|
constructor(ctrl: OAuthController, opts: number | OAuthCallbackFlowOptions, callbackPath = DEFAULT_CALLBACK_PATH) {
|
|
115
143
|
this.ctrl = ctrl;
|
|
@@ -131,6 +159,11 @@ export abstract class OAuthCallbackFlow {
|
|
|
131
159
|
async login(): Promise<OAuthCredentials> {
|
|
132
160
|
const state = generateState();
|
|
133
161
|
const { server, redirectUri } = this.#startServer(state);
|
|
162
|
+
// Whether the browser actually reached the callback page. A manual paste (or a
|
|
163
|
+
// cancelled/timed-out flow) never rendered a tab of ours, so there is nothing to
|
|
164
|
+
// close and no reason to pay for an osascript spawn.
|
|
165
|
+
let servedCallbackPage = false;
|
|
166
|
+
this.#onPageServed = () => { servedCallbackPage = true; };
|
|
134
167
|
try {
|
|
135
168
|
const { url, instructions } = await this.generateAuthUrl(state, redirectUri);
|
|
136
169
|
this.ctrl.onAuth?.({ url, instructions });
|
|
@@ -140,9 +173,23 @@ export abstract class OAuthCallbackFlow {
|
|
|
140
173
|
return await this.exchangeToken(code, returnedState || state, redirectUri);
|
|
141
174
|
} finally {
|
|
142
175
|
server.stop();
|
|
176
|
+
this.#onPageServed = undefined;
|
|
177
|
+
// Close the leftover callback tab from the OUTSIDE (macOS only; see
|
|
178
|
+
// ./browser-tab.ts). Awaited — a one-shot `jeo auth login` exits as soon as
|
|
179
|
+
// this returns, so a fire-and-forget spawn would be killed before it ran —
|
|
180
|
+
// but hard-capped, silent, and never able to fail the login it follows.
|
|
181
|
+
if (servedCallbackPage) {
|
|
182
|
+
try { await this.closeBrowserTab(redirectUri); } catch { /* best effort only */ }
|
|
183
|
+
}
|
|
143
184
|
}
|
|
144
185
|
}
|
|
145
186
|
|
|
187
|
+
/** OS-level close of the callback tab (see ./browser-tab.ts). Overridable so tests
|
|
188
|
+
* can observe the attempt without driving a real browser. */
|
|
189
|
+
protected closeBrowserTab(redirectUri: string): Promise<boolean> {
|
|
190
|
+
return closeAuthTab(redirectUri);
|
|
191
|
+
}
|
|
192
|
+
|
|
146
193
|
#startServer(expectedState: string): { server: Bun.Server<unknown>; redirectUri: string } {
|
|
147
194
|
const serve = (port: number) =>
|
|
148
195
|
Bun.serve({
|
|
@@ -186,6 +233,7 @@ export abstract class OAuthCallbackFlow {
|
|
|
186
233
|
message = "Authentication succeeded. Return to the terminal — jeo is ready.";
|
|
187
234
|
}
|
|
188
235
|
|
|
236
|
+
this.#onPageServed?.(); // a real browser tab now holds this page — it can be closed
|
|
189
237
|
const resolve = this.#resolve;
|
|
190
238
|
const reject = this.#reject;
|
|
191
239
|
queueMicrotask(() => {
|
package/src/commands/launch.ts
CHANGED
|
@@ -2093,8 +2093,18 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2093
2093
|
io: {
|
|
2094
2094
|
output: (line: string) => {
|
|
2095
2095
|
console.log(line);
|
|
2096
|
-
}
|
|
2096
|
+
},
|
|
2097
|
+
// Reuse the SAME readline interface the interactive REPL already owns
|
|
2098
|
+
// (via promptInput) instead of letting the workflow engine spin up a
|
|
2099
|
+
// second `readline.Interface` on process.stdin — deep-interview.ts only
|
|
2100
|
+
// creates its own internal rl when `opts.io?.input` is absent, and two
|
|
2101
|
+
// concurrent readline interfaces on one stdin silently fight over raw
|
|
2102
|
+
// mode: no error, no visible prompt, the workflow just hangs forever.
|
|
2103
|
+
// The sibling `runSkillInvocation` path already wires `promptInput`
|
|
2104
|
+
// here; this `$a $b …` chain path must match it.
|
|
2105
|
+
input: () => promptInput("")
|
|
2097
2106
|
},
|
|
2107
|
+
|
|
2098
2108
|
args: inv.skill.name === "deep-interview" ? (inv.intent ? inv.intent.split(/\s+/) : []) : undefined
|
|
2099
2109
|
};
|
|
2100
2110
|
|