Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
6c5f3ff
Add /exit slash command (#1)
0xSHKWON May 25, 2026
d2d8615
Add Korean stock research section and CLAUDE.md (#2)
0xSHKWON May 25, 2026
8935f3d
Add Korean stock data pipe (Phase 1) (#3)
0xSHKWON May 29, 2026
650f8dd
Add Korean disclosure tools (Phase 2) (#4)
0xSHKWON May 29, 2026
37c5b75
Add Korea-specific tools — short balance, foreign ownership, NPS hold…
0xSHKWON May 29, 2026
72e2023
Adapt skills for Korean market — DCF market-branch + 물적분할 skill (Phas…
0xSHKWON May 29, 2026
5ed1495
KR financials: normalized summary-first + cross-signal research playb…
0xSHKWON May 30, 2026
8eb246b
KR eval harness: fixed question bank + record/replay + LLM-judge scor…
0xSHKWON May 31, 2026
b18890e
read_filings_kr: DART 사업보고서 narrative extraction (사업의 내용·위험관리·MD&A) (…
0xSHKWON Jun 1, 2026
1478a49
get_market_data_kr: 현재가·시총·PER/PBR·목표주가 컨센서스 (Naver keyless) (#11)
0xSHKWON Jun 2, 2026
2038f68
DCF 순부채 부호 수정: KR 이자부채(totalDebt)·단기금융상품 정규화 + EV→Equity 부호 규칙 (#12)
0xSHKWON Jun 3, 2026
10fe1ba
한국 주식 리서치: 견고성 레이어 + governance 본문 (DART 쿼터·서킷브레이커 / keyless 티어링 / dr…
0xSHKWON Jun 4, 2026
89d6150
한국 주식 리서치 품질개선: 종목 해석·사업부문 데이터·밸류에이션 스킬·eval 정확도 게이트 (#14)
0xSHKWON Jun 20, 2026
c3b7ac3
resolveKrSecurity 분기 단위테스트 (모킹) — 이름→코드 해석 회귀 가드
0xSHKWON Jun 20, 2026
15fbac2
Merge KR equity-research fork (#1–#15) onto upstream main
0xSHKWON Jun 20, 2026
b38b736
Desktop app (Electron + Bun sidecar) + general-user README
0xSHKWON Jun 20, 2026
2ba36f8
Merge remote-tracking branch 'origin/main' into desktop-app-architecture
0xSHKWON Jun 20, 2026
ac3e02d
README/DEVELOPMENT: reflect merged capability — self-expansion, multi…
0xSHKWON Jun 20, 2026
685a9db
fix(desktop): stage real bun binary, not a dangling symlink
0xSHKWON Jun 20, 2026
c4a0828
desktop: forced-update gate + help example prompts + compact redesign
0xSHKWON Jun 20, 2026
1e86712
ci: cross-platform desktop release workflow (mac arm64 + win x64)
0xSHKWON Jun 20, 2026
90f1e7d
desktop: app icon (three.js icosahedron) + full-width top drag region
0xSHKWON Jun 20, 2026
b9ef24f
README: top download buttons (macOS/Windows direct links) + stable ar…
0xSHKWON Jun 20, 2026
96b0b22
release: v1.0.0 — version bump + CI write permission + dynamic versio…
0xSHKWON Jun 21, 2026
6522cda
release: sync desktop package-lock to v1.0.0
0xSHKWON Jun 21, 2026
ef82648
review fixes: sidecar crash/stop fails in-flight runs; drop dead xlsx…
0xSHKWON Jun 21, 2026
f92e295
Desktop app (Electron + Bun sidecar) + 1.0.0 (#16)
0xSHKWON Jun 21, 2026
05a5b29
README: first-run unsigned-app bypass note (macOS right-click→open / …
0xSHKWON Jun 21, 2026
960f1f4
Update README.md
0xSHKWON Jun 21, 2026
e9d3741
fix(desktop): ad-hoc sign macOS app + strip bundle symlinks (fixes 'd…
0xSHKWON Jun 21, 2026
65d7ce5
README: correct first-run bypass for macOS 15/26 (System Settings → O…
0xSHKWON Jun 21, 2026
1090ef0
desktop: Windows in-app auto-update via electron-updater
0xSHKWON Jun 21, 2026
3eb8840
desktop: split Help into two tabs (프롬프트 예시 / API 키 가이드)
0xSHKWON Jun 21, 2026
17ac7ee
release: v1.0.1
0xSHKWON Jun 21, 2026
e623e48
release: update.json latest=1.0.1
0xSHKWON Jun 21, 2026
86fd0db
README: add market-positioning section (3 buckets vs the empty slot w…
0xSHKWON Jun 21, 2026
bddc09f
build(desktop): stage production-only core deps (≈310MB smaller)
0xSHKWON Jun 21, 2026
137fe93
release: v1.0.2 (production-only core bundle ≈310MB smaller)
0xSHKWON Jun 21, 2026
a5db68d
release: update.json latest=1.0.2
0xSHKWON Jun 21, 2026
492b65f
release: v1.0.3 — fix invalid Gemini/xAI model ids (404)
0xSHKWON Jun 21, 2026
fd6d071
release: update.json latest=1.0.3
0xSHKWON Jun 21, 2026
2bbf2ef
release: v1.1.0 — collapsible reasoning timeline in desktop chat
0xSHKWON Jun 21, 2026
6dd1f49
Merge pull request #19 from 0xSHKWON/collapsible-reasoning-toggle
0xSHKWON Jun 21, 2026
8169e62
release: update.json latest=1.1.0
0xSHKWON Jun 21, 2026
f2573f1
release: v1.1.1 — hide menu bar on Windows/Linux
0xSHKWON Jun 24, 2026
264db30
release: update.json latest=1.1.1
0xSHKWON Jun 24, 2026
6de3b24
release: v1.1.2 — ECOS (한국은행) 거시 금리·환율 연동
0xSHKWON Jun 29, 2026
90609ab
release: update.json latest=1.1.2
0xSHKWON Jun 29, 2026
31d3cc3
feat(kr): 실측 β + ERP 4.87% 핀 + β 측정법 질문(CLI·데스크탑) (#22)
0xSHKWON Jul 1, 2026
9e3fbdf
fix(kr): 신뢰성 감사 수정 — P0 4건 + 주관성 P1 3건 + eval CI 승격 (#23)
0xSHKWON Jul 3, 2026
1bf188b
ci: make kr-eval-replay opt-in (run-eval label only), unblock main (#24)
0xSHKWON Jul 3, 2026
f90ea21
docs: README·DEVELOPMENT 최신화 — 신뢰 원칙과 신규 도구를 사용자 언어로
0xSHKWON Jul 3, 2026
bc3fffa
feat(landing): 프로덕트 소개 랜딩 페이지 추가 (#25)
0xSHKWON Jul 7, 2026
897c8ee
ci(pages): landing/ 을 GitHub Pages로 자동 배포 (#26)
0xSHKWON Jul 7, 2026
96f5dff
feat(kr): M4 — 컨센서스 추정치·주가 시계열 도구 + 한국어 유사도·이력 캐싱 + 셀프리뷰 반영 (#27)
0xSHKWON Jul 27, 2026
cdbba47
merge: upstream/main (v1.0.3) — bash 툴+권한 엔진, GPT-5.6, Ollama Cloud, …
0xSHKWON Jul 31, 2026
4192660
fork(model): gpt-5.5 를 선택 가능 상태로 유지 — 직전 세대 보존 정책
0xSHKWON Jul 31, 2026
7ca7851
fix(agent): 승인 핸들러가 없으면 승인 필요 툴을 바인딩하지 않음
0xSHKWON Jul 31, 2026
8ea6f4b
fix(permissions,desktop): 게이트 우회 차단 + 데스크탑 카탈로그 동기화 + 프롬프트 정직성
0xSHKWON Jul 31, 2026
67c9033
Merge pull request #28 from 0xSHKWON/merge/upstream-v1.0.3
0xSHKWON Jul 31, 2026
577608e
fix(desktop): 실행 모델 폴백을 카탈로그에서 도출 — 하드코딩 제거
0xSHKWON Jul 31, 2026
174990f
feat(desktop): 단발 세션 — run 이 이전 대화를 물고 가지 않음
0xSHKWON Jul 31, 2026
064edd5
feat(desktop): 답변 후 새 대화 강제 + 데스크탑 타입체크를 CI 에 추가
0xSHKWON Jul 31, 2026
42d5bc1
feat(desktop): 데스크탑 채널 프로파일 — 터미널용 지시에서 분리
0xSHKWON Jul 31, 2026
44ebee6
fix(desktop): 실행 못 하는 툴 언바인딩 + 복호화 불가 키 정직하게 + DB 실패 시 에러 표시
0xSHKWON Jul 31, 2026
df75c58
fix: KRX 인증을 ID/PW 로 단일화 + 데스크탑 자잘한 정직성·안전 수정
0xSHKWON Jul 31, 2026
840978f
chore: 데스크탑 앱 실행·검증용 프로젝트 스킬 추가
0xSHKWON Aug 1, 2026
bb3a056
fix: 리뷰 지적 2건 — 죽은 쿠키 파서 제거 + 새 질문하기 무반응 수정
0xSHKWON Aug 1, 2026
00d2d85
feat(desktop): API 키 .env 내보내기 + 한국어 조사 앞 볼드 렌더 규칙
0xSHKWON Aug 1, 2026
57bf318
fix(desktop): 한국어 조사 앞 볼드를 렌더 시점에 정규화
0xSHKWON Aug 1, 2026
adc634d
Merge pull request #29 from 0xSHKWON/fix/desktop-model-fallback
0xSHKWON Aug 1, 2026
2ebef97
chore(release): v1.2.0 — 버전 범프 + 업데이트 매니페스트
0xSHKWON Aug 2, 2026
0373684
Merge pull request #30 from 0xSHKWON/release/v1.2.0
0xSHKWON Aug 2, 2026
0200dd8
chore(license): MIT LICENSE 파일 추가 + README 라이선스 절 정비 — 원저작권 표시 보존
0xSHKWON Aug 2, 2026
6f2df8d
Merge pull request #31 from 0xSHKWON/kb-ai-challenge-legal-check
0xSHKWON Aug 2, 2026
f978874
docs(readme): 제품명 제목 명시 + 배지 + LLM 키 요건 명확화 + 랜딩 링크
0xSHKWON Aug 3, 2026
b8a808b
Merge pull request #32 from 0xSHKWON/polish/readme
0xSHKWON Aug 3, 2026
9fda525
docs(readme): 히어로 스크린샷 추가 + 챗봇 비교 섹션을 '뭐가 다른가'로 통합
0xSHKWON Aug 3, 2026
6502d3d
Merge pull request #33 from 0xSHKWON/polish/readme-screenshot
0xSHKWON Aug 3, 2026
d0f8ad9
Update project title for international focus
0xSHKWON Sep 1, 2026
7e58554
merge: upstream/main (v1.0.6) — GPT-6/Claude 5 카탈로그, FD 페이지네이션, 큐 메시지 UI
0xSHKWON Oct 3, 2026
d626219
feat(agents): Claude Code·ChatGPT(Codex) 구독 로그인 + 모델 피커 + 설정 에이전트 탭
0xSHKWON Oct 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions .claude/skills/run-desktop/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
name: run-desktop
description: Build, launch, and drive the Dexter Electron desktop app to verify a change in the real UI. Use when asked to run or start the desktop app, screenshot it, click through it, or confirm a desktop change works outside of tests.
---

The desktop app is the primary product (the CLI has one user). Its UI is React in
an Electron window, and `bun test` never touches it — so a desktop change is
unverified until it has been clicked in a running window.

Drive it through the Playwright REPL at `.claude/skills/run-desktop/driver.mjs`.
macOS only; there is no xvfb involved.

## Setup (once per clone)

```bash
cd desktop
npm install
node node_modules/electron/install.js # see Gotchas — npm install alone may skip this
npm run build # package.json main is ./out/main/index.js
```

Rebuild (`npm run build`) after any change under `desktop/src/` — the driver runs
the built output, not the sources.

## Run

Scripted (the usual path — commands are piped, one per line):

```bash
printf 'launch\nss landing\nquit\n' | node .claude/skills/run-desktop/driver.mjs
```

Interactive:

```bash
node .claude/skills/run-desktop/driver.mjs
```

Screenshots land in `/tmp/dexter-shots/` (override with `SCREENSHOT_DIR`).
**Open them and look.** A blank frame means the app never finished booting.

### Commands

| command | what it does |
|---|---|
| `launch` | launch + wait for `.chat` to mount |
| `ss [name]` | screenshot → `/tmp/dexter-shots/<name>.png` |
| `click <sel>` | click via DOM |
| `click-text <text>` | click the button/nav item with that text (sidebar tabs work) |
| `set-input <sel> <text>` | set a React-controlled input (quote a selector containing spaces) |
| `type <text>` / `press <key>` | keyboard input |
| `wait <sel>` | wait for a selector, 20s |
| `poll-for <sel> [ms]` | 50ms polling, for states too short-lived for `wait` |
| `dialog accept\|dismiss` | arm a one-shot handler for the next `window.confirm` and print its message |
| `eval <js>` / `text [sel]` | evaluate in the page / print innerText |
| `windows` | list windows |
| `quit` | close and clean up the temp profile |

### A full example that exercises the app

```bash
printf 'launch
set-input ".composer textarea" 삼성전자 분석해줘
click-text 전송
wait .composer-note
text .composer
click-text History
dialog dismiss
click .hist-row-del
eval document.querySelectorAll(".hist-row").length
quit
' | node .claude/skills/run-desktop/driver.mjs
```

## Your data is not at risk

`launch` creates a throwaway `--user-data-dir` and deletes it on `quit`, so the
real install's chat history and encrypted API keys are never read or written.
That also means **the app starts empty every time** — no history, model shows
`미설정`. That is the isolated profile, not data loss; the real DB lives at
`~/Library/Application Support/dexter-desktop/dexter-desktop.db`.

Set `DEXTER_REAL_PROFILE=1` only when a run genuinely needs the stored API keys,
and never combine it with a `dialog accept` on a delete.

## What you cannot verify here

There are no API keys in the repo, so a question returns
`[LLM] OPENAI_API_KEY not found in environment variables` instead of an answer.
Useful anyway — that error is a settled assistant turn, so it still exercises the
answered/error UI states. Anything about **answer content** (markdown shape,
Korean company names, the desktop channel profile) needs a keyed run.

## Gotchas

- **`npm install` can leave `node_modules/electron/dist` empty.** The binary is
fetched by electron's postinstall; any earlier `--ignore-scripts` install
poisons it. Fix: `node node_modules/electron/install.js`. `launch` checks for
the binary and prints this.
- **`playwright-core` lives in the repo root**, not in `desktop/`. The driver
resolves it through the root `package.json` — don't "fix" that to a local path.
- **Assigning `.value` on a React input does nothing.** React tracks the previous
value on the node, so `set-input` uses the native setter plus a bubbling
`input` event. Without it the field looks filled but state never updates and
the submit button stays disabled — which reads as "the button is broken".
- **Selectors with spaces**: `set-input` takes the first token as the selector
unless you quote it (`set-input ".composer textarea" …`). `poll-for` only
peels a trailing timeout when the last token is a number.
- **Piped commands need serializing.** readline emits every buffered line at
once and does not await an async handler; the driver chains them on a queue.
If you add a command, keep it in that chain or it will run before `launch`.
- **`DEXTER_CORE_ROOT`** is set by the driver so the sidecar finds the Bun core.
Launching Electron by hand without it makes every run fail to spawn.
- **The sidecar cold start is ~0.4–0.7s**, so transient UI states (a conversion's
취소 button) exist only briefly. Use `poll-for`, not `wait`.
- **No tmux on this machine.** The scripted pipe above is the interactive
substitute.

## Troubleshooting

- **`ERROR: app not built`** → `cd desktop && npm run build`.
- **`WARN: .chat never appeared`** → the renderer threw. `eval document.body.innerHTML.slice(0,500)`.
- **`NOT_AN_INPUT (DIV)`** → the selector had spaces and got split; quote it.
- **`NOT_FOUND` on a row that is visible** → it rendered after the click. Add
`wait`/`poll-for` before it.
242 changes: 242 additions & 0 deletions .claude/skills/run-desktop/driver.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
// REPL driver for the Dexter desktop app (Electron, macOS).
//
// Interactive: node .claude/skills/run-desktop/driver.mjs
// Scripted: printf 'launch\nss landing\nquit\n' | node .claude/skills/run-desktop/driver.mjs
//
// Launches with an isolated --user-data-dir by default so the real install's
// chat history and encrypted API keys are never touched. Set DEXTER_REAL_PROFILE=1
// to attach to the actual profile (needed only when a run must use stored keys).
import { createRequire } from 'node:module';
import * as readline from 'node:readline';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';

const REPO_ROOT = path.resolve(import.meta.dirname, '../../..');
const APP_DIR = path.join(REPO_ROOT, 'desktop');
// playwright-core is a root dependency; desktop/ does not carry its own copy.
const require = createRequire(path.join(REPO_ROOT, 'package.json'));
const { _electron: electron } = require('playwright-core');

const SHOT_DIR = process.env.SCREENSHOT_DIR || '/tmp/dexter-shots';
fs.mkdirSync(SHOT_DIR, { recursive: true });

const ELECTRON_BIN = path.join(APP_DIR, 'node_modules/electron/dist/Electron.app/Contents/MacOS/Electron');

let app = null;
let page = null;
let userDataDir = null;

const COMMANDS = {
async launch() {
if (app) return console.log('already launched');
if (!fs.existsSync(ELECTRON_BIN)) {
return console.log(`ERROR: no Electron binary at ${ELECTRON_BIN}\n → cd desktop && node node_modules/electron/install.js`);
}
if (!fs.existsSync(path.join(APP_DIR, 'out/main/index.js'))) {
return console.log('ERROR: app not built (desktop/out missing)\n → cd desktop && npm run build');
}

const args = ['--no-sandbox'];
if (process.env.DEXTER_REAL_PROFILE !== '1') {
// DEXTER_USER_DATA_DIR points at a profile you prepared yourself — e.g. a
// copy of the real dexter-desktop.db with the chat rows deleted, so a run
// can use the stored API keys without writing into the real history. It is
// yours to clean up; a mkdtemp profile is removed on quit.
userDataDir = process.env.DEXTER_USER_DATA_DIR ?? fs.mkdtempSync(path.join(os.tmpdir(), 'dexter-driver-'));
args.push(`--user-data-dir=${userDataDir}`);
}
args.push(APP_DIR);

app = await electron.launch({
executablePath: ELECTRON_BIN,
args,
// The sidecar resolves the Bun core relative to this.
env: { ...process.env, DEXTER_CORE_ROOT: REPO_ROOT },
timeout: 60_000,
});
page = await app.firstWindow();
await page.waitForLoadState('domcontentloaded').catch(() => {});
// The renderer boots async (settings + secrets over IPC); .chat is the first
// real UI to mount. A blank screenshot means this wait was skipped.
await page.waitForSelector('.chat', { timeout: 30_000 }).catch(() => console.log('WARN: .chat never appeared'));
console.log(`launched${userDataDir ? ` (isolated profile: ${userDataDir})` : ' (REAL profile)'}`);
for (const w of app.windows()) console.log(' ', w.url());
},

async ss(name) {
if (!page) return console.log('ERROR: launch first');
const f = path.join(SHOT_DIR, (name || `ss-${Date.now()}`) + '.png');
await page.screenshot({ path: f });
console.log('screenshot:', f);
},

/** Click via DOM, not coordinates — reliable regardless of layering. */
async click(sel) {
if (!page) return console.log('ERROR: launch first');
console.log('click', sel, '→', await page.evaluate((s) => {
const el = document.querySelector(s);
if (!el) return 'NOT_FOUND';
el.click();
return 'OK';
}, sel));
},

/** Click the first button/nav item whose text matches. Covers the sidebar tabs. */
async 'click-text'(text) {
if (!page) return console.log('ERROR: launch first');
console.log('click-text', JSON.stringify(text), '→', await page.evaluate((t) => {
const els = [...document.querySelectorAll('button, a, [role="button"], [class*="head"], .hist-row, .hist-row-del')];
const el = els.find((e) => e.textContent?.trim() === t) ?? els.find((e) => e.textContent?.includes(t));
if (!el) return 'NOT_FOUND';
el.click();
return `OK: ${el.tagName}.${el.className}`;
}, text));
},

/**
* Set a React-controlled input. Assigning .value directly does nothing — React
* tracks the previous value on the node, so the native setter plus a bubbling
* `input` event is what makes onChange fire.
*
* Usage: set-input <css-sel> <text...>
* set-input "<css-sel with spaces>" <text...>
*/
async 'set-input'(rest) {
if (!page) return console.log('ERROR: launch first');
const quoted = /^(['"])(.*?)\1\s*/.exec(rest);
let sel, value;
if (quoted) {
sel = quoted[2];
value = rest.slice(quoted[0].length);
} else {
const i = rest.indexOf(' ');
[sel, value] = i < 0 ? [rest, ''] : [rest.slice(0, i), rest.slice(i + 1)];
}
console.log('set-input', JSON.stringify(sel), '→', await page.evaluate(([s, v]) => {
const el = document.querySelector(s);
if (!el) return 'NOT_FOUND';
if (el.tagName !== 'TEXTAREA' && el.tagName !== 'INPUT') {
return `NOT_AN_INPUT (${el.tagName}) — quote the selector if it contains spaces`;
}
const proto = el.tagName === 'TEXTAREA' ? window.HTMLTextAreaElement : window.HTMLInputElement;
Object.getOwnPropertyDescriptor(proto.prototype, 'value').set.call(el, v);
el.dispatchEvent(new Event('input', { bubbles: true }));
return `OK (len=${el.value.length})`;
}, [sel, value]));
},

async type(text) { if (page) await page.keyboard.type(text, { delay: 20 }); },
async press(key) { if (page) await page.keyboard.press(key); },

async wait(sel) {
if (!page) return console.log('ERROR: launch first');
try {
await page.waitForSelector(sel, { timeout: 20_000 });
console.log('found:', sel);
} catch {
console.log('TIMEOUT:', sel);
}
},

/**
* Poll until a selector appears. For states too short-lived for `wait`.
* Usage: poll-for <css-sel> [timeout-ms] — the selector may contain spaces,
* so the timeout is taken from the end only when the last token is a number.
*/
async 'poll-for'(rest) {
if (!page) return console.log('ERROR: launch first');
const parts = rest.trim().split(/\s+/);
const tail = parts.length > 1 && /^\d+$/.test(parts[parts.length - 1]) ? parts.pop() : null;
const sel = parts.join(' ');
const budget = Number(tail) || 10_000;
const start = Date.now();
while (Date.now() - start < budget) {
if (await page.evaluate((s) => !!document.querySelector(s), sel)) {
return console.log('appeared:', sel, `after ${Date.now() - start}ms`);
}
await new Promise((r) => setTimeout(r, 50));
}
console.log('NEVER APPEARED:', sel);
},

/** Arm a one-shot handler for the next window.confirm/alert: accept | dismiss. */
async dialog(action) {
if (!page) return console.log('ERROR: launch first');
const accept = action === 'accept';
page.once('dialog', async (d) => {
console.log(`DIALOG(${d.type()}): ${JSON.stringify(d.message())} → ${accept ? 'accepted' : 'dismissed'}`);
await (accept ? d.accept() : d.dismiss());
});
console.log(`armed: next dialog will be ${accept ? 'accepted' : 'dismissed'}`);
},

async eval(expr) {
if (!page) return console.log('ERROR: launch first');
try { console.log(JSON.stringify(await page.evaluate(expr))); }
catch (e) { console.log('ERROR:', e.message); }
},

async text(sel) {
if (!page) return console.log('ERROR: launch first');
console.log(await page.evaluate((s) => (s ? document.querySelector(s) : document.body)?.innerText ?? '(null)', sel || null));
},

async windows() {
if (!app) return console.log('ERROR: launch first');
for (const w of app.windows()) console.log(' ', w.url());
},

async quit() {
if (app) await app.close().catch(() => {});
// Only remove a profile this driver created — never a caller-supplied one.
if (userDataDir && !process.env.DEXTER_USER_DATA_DIR) {
fs.rmSync(userDataDir, { recursive: true, force: true });
}
app = null;
page = null;
userDataDir = null;
},

help() { console.log('commands:', Object.keys(COMMANDS).join(', ')); },
};

// Electron steals the inherited stdin — read the raw fd so the REPL keeps input.
// Works for a TTY and for a pipe, which is how scripted runs feed commands.
const stdin = fs.createReadStream(null, { fd: fs.openSync('/dev/stdin', 'r') });
const rl = readline.createInterface({ input: stdin, output: process.stdout, prompt: 'driver> ' });

// readline emits every buffered line at once when stdin is a pipe, and an async
// handler does not hold the next event back — without this chain a scripted run
// fires all commands before `launch` has resolved.
let queue = Promise.resolve();
let closed = false;
const prompt = () => { if (!closed) rl.prompt(); };

rl.on('line', (line) => {
queue = queue.then(async () => {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) return prompt();
const i = trimmed.indexOf(' ');
const [cmd, rest] = i < 0 ? [trimmed, ''] : [trimmed.slice(0, i), trimmed.slice(i + 1)];
const fn = COMMANDS[cmd];
if (!fn) {
console.log('unknown:', cmd, '— try: help');
return prompt();
}
try { await fn(rest); } catch (e) { console.log('ERROR:', e.message); }
if (cmd === 'quit') { closed = true; rl.close(); process.exit(0); }
prompt();
});
});
// A piped script ends with EOF; let queued commands finish before tearing down.
rl.on('close', () => {
closed = true;
queue = queue.then(async () => {
await COMMANDS.quit();
process.exit(0);
});
});

console.log('dexter desktop driver — "help" for commands, "launch" to start');
rl.prompt();
Loading
Loading