Skip to content

Commit e6ed512

Browse files
Make CodeRouter Studio a self-contained, distributable desktop app
- Bundle the daemon (CLI + ink/react/ws/ripgrep + node-pty prebuilds) into the packaged app via a stage-daemon step + electron-builder extraResources, and run it under Electron's own Node so the app needs no global Node install. - resolveCli now prefers the bundled daemon (process.resourcesPath/cli). - Disable electron-builder npmRebuild (we ship prebuilt node-pty) and add a generated .icns/.png app icon from the logo. - Add Masterplan to the app chat mode selector (color-coded like the rest). - Document CodeRouter Studio (download + build-from-source) in the README and fix stale repo URLs to the Code-Router org. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 358a55e commit e6ed512

11 files changed

Lines changed: 164 additions & 19 deletions

File tree

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,37 @@ One coding agent that picks the right model for every task — fast, cheap model
88

99
Works with your existing **Claude Code** or **Codex** CLI, or any API key — OpenAI, Anthropic, OpenRouter, DeepSeek, Groq, or a local Ollama model.
1010

11-
## Install
11+
Use it two ways: the **CLI** (`coderouter`) for the terminal, or **CodeRouter Studio**, a desktop app for everything else.
12+
13+
## CodeRouter Studio (desktop app)
14+
15+
CodeRouter Studio is a native desktop app that wraps the same router in a full UI — your projects, chats, loops, usage, and plugins in one place. It runs a persistent local daemon so background work (like loops) keeps going after you close the window.
16+
17+
**Download:** grab the latest `CodeRouter-Studio-*.dmg` (macOS) from the [Releases page](https://github.com/Code-Router/CodeRouter/releases), drag it to Applications, and open it.
18+
19+
> The app isn't notarized yet, so on first launch macOS may warn that it's from an unidentified developer. Right‑click the app → **Open**, then confirm. The app is fully self‑contained — no separate Node or CLI install required.
20+
21+
What's inside:
22+
23+
- **Chat** with all five modes — Agent, Plan, Masterplan, Debug, Review — picked from a color‑coded selector. Voice‑to‑text, a project picker, and inline code diffs with **Review / Undo**.
24+
- **Projects & Chats** — register any folder, browse your work, and open the chats that belong to each project. CLI sessions and app chats share the same store, so they show up identically in both places.
25+
- **Loops** — describe an outcome in plain English; CodeRouter generates a verifiable loop (goal, verifier, stop condition), you approve it, and it runs and self‑corrects until the check passes.
26+
- **Usage & Spending** — cost/usage across *all* CodeRouter work on your machine, an activity heatmap, and a spending limit (default **$50/mo**) that's actually enforced by the daemon, with a progress bar on the Overview.
27+
- **Plugins** — install plugins, rules, skills, and subagents, or browse the marketplace.
28+
- **Terminal** — a real, responsive shell in the bottom/side panel.
29+
- **Light & dark themes.**
30+
31+
### Build Studio from source
32+
33+
```bash
34+
pnpm i
35+
pnpm --filter @coderouter/app dev # run in development
36+
pnpm --filter @coderouter/app package # build a distributable (.dmg / AppImage / nsis)
37+
```
38+
39+
`package` builds the renderer + Electron main, bundles the daemon into the app (so it's self‑contained), and emits an installer under `packages/app/release/`.
40+
41+
## Install (CLI)
1242

1343
Requires **Node 24+**.
1444

packages/app/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
dist/
22
dist-electron/
33
release/
4+
resources/

packages/app/build/icon.icns

1.28 MB
Binary file not shown.

packages/app/build/icon.png

221 KB
Loading

packages/app/electron-builder.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
{
22
"appId": "dev.coderouter.studio",
33
"productName": "CodeRouter Studio",
4+
"npmRebuild": false,
45
"files": ["dist/**/*", "dist-electron/**/*"],
5-
"directories": { "output": "release" },
6-
"mac": { "category": "public.app-category.developer-tools", "target": ["dmg"] },
6+
"extraResources": [{ "from": "resources/cli", "to": "cli" }],
7+
"directories": { "output": "release", "buildResources": "build" },
8+
"mac": {
9+
"category": "public.app-category.developer-tools",
10+
"target": ["dmg"],
11+
"icon": "build/icon.icns"
12+
},
713
"linux": { "target": ["AppImage"], "category": "Development" },
814
"win": { "target": ["nsis"] }
915
}

packages/app/electron/main.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,36 @@ function findNode(): string | null {
6666

6767
/** Resolve the command used to spawn the daemon. */
6868
function resolveCli(): { cmd: string; args: string[]; electronNode: boolean } {
69-
const cliPath = process.env.CODEROUTER_CLI || (() => {
70-
const local = join(app.getAppPath(), '..', 'cli', 'dist', 'cli.js');
71-
return existsSync(local) ? local : null;
72-
})();
69+
// 1. Explicit override (dev / power users). Prefer standalone Node if one
70+
// exists so a system node-pty matches; otherwise Electron-as-Node.
71+
const override = process.env.CODEROUTER_CLI;
72+
if (override && existsSync(override)) {
73+
const node = findNode();
74+
return node
75+
? { cmd: node, args: [override], electronNode: false }
76+
: { cmd: process.execPath, args: [override], electronNode: true };
77+
}
7378

74-
if (cliPath) {
79+
// 2. Daemon bundled inside the packaged app (staged by scripts/stage-daemon
80+
// into Resources/cli). Run it under Electron's own Node
81+
// (ELECTRON_RUN_AS_NODE), which is guaranteed new enough for node:sqlite
82+
// and loads node-pty's NAPI prebuild — so the app is fully
83+
// self-contained and needs no global Node install.
84+
const bundled = join(process.resourcesPath, 'cli', 'dist', 'cli.js');
85+
if (existsSync(bundled)) {
86+
return { cmd: process.execPath, args: [bundled], electronNode: true };
87+
}
88+
89+
// 3. Dev: the sibling workspace build.
90+
const local = join(app.getAppPath(), '..', 'cli', 'dist', 'cli.js');
91+
if (existsSync(local)) {
7592
const node = findNode();
76-
// Prefer standalone Node so node-pty loads; fall back to Electron-as-Node
77-
// (the daemon still works, only the terminal backend would be unavailable).
78-
if (node) return { cmd: node, args: [cliPath], electronNode: false };
79-
return { cmd: process.execPath, args: [cliPath], electronNode: true };
93+
return node
94+
? { cmd: node, args: [local], electronNode: false }
95+
: { cmd: process.execPath, args: [local], electronNode: true };
8096
}
97+
98+
// 4. Last resort: a globally installed `coderouter` on PATH.
8199
return { cmd: 'coderouter', args: [], electronNode: false };
82100
}
83101

packages/app/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"version": "0.1.0",
44
"private": true,
55
"description": "CodeRouter Studio — Electron desktop app (Loops, Projects, Chats, Usage)",
6+
"author": "Efe Acar",
67
"type": "module",
78
"main": "dist-electron/main.cjs",
89
"scripts": {
@@ -13,7 +14,9 @@
1314
"build": "npm run build:main && npm run build:renderer",
1415
"start": "electron .",
1516
"typecheck": "tsc --noEmit",
16-
"package": "npm run build && electron-builder"
17+
"build:cli": "pnpm --filter @coderouter/core build && pnpm --filter coderouter-cli build",
18+
"stage:daemon": "node scripts/stage-daemon.mjs",
19+
"package": "npm run build && npm run build:cli && npm run stage:daemon && electron-builder"
1720
},
1821
"dependencies": {
1922
"@coderouter/core": "workspace:*",
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/**
2+
* Stage the CodeRouter daemon into `resources/cli` so electron-builder can
3+
* ship it inside the packaged app. This makes CodeRouter Studio fully
4+
* self-contained: it spawns `<resources>/cli/dist/cli.js daemon` under
5+
* Electron's bundled Node (which includes node:sqlite), so the app works on
6+
* a machine with no global Node or `coderouter` install.
7+
*
8+
* The bundled cli.js (built by tsup) keeps a handful of dependencies external
9+
* and imports them statically, so they must exist in an adjacent
10+
* node_modules:
11+
* - ink + react: the REPL renderer. Never rendered by the daemon, but ESM
12+
* resolves the static imports at load, so they must be present.
13+
* - ws: WebSocket server for the terminal PTY relay.
14+
* - @vscode/ripgrep: per-platform `rg` binary for fast context scanning.
15+
* - node-pty: native terminal backend (NAPI prebuilds).
16+
*
17+
* Pure-JS deps (ink/react/ws/ripgrep) are materialised with a real `npm
18+
* install` so their full transitive closure is correct and flat. node-pty is
19+
* copied from the workspace instead — it already carries working NAPI
20+
* prebuilds, and reinstalling it risks a native recompile.
21+
*/
22+
import { execFileSync } from 'node:child_process';
23+
import { cpSync, existsSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
24+
import { dirname, join } from 'node:path';
25+
import { fileURLToPath } from 'node:url';
26+
27+
const appDir = join(dirname(fileURLToPath(import.meta.url)), '..');
28+
const repoRoot = join(appDir, '..', '..');
29+
const cliDir = join(repoRoot, 'packages', 'cli');
30+
const out = join(appDir, 'resources', 'cli');
31+
32+
const cliJs = join(cliDir, 'dist', 'cli.js');
33+
if (!existsSync(cliJs)) {
34+
console.error('[stage-daemon] packages/cli/dist/cli.js not found — build the CLI first (pnpm --filter coderouter-cli build).');
35+
process.exit(1);
36+
}
37+
38+
// Pin to the versions declared by the CLI so the bundle matches what we test.
39+
const cliPkg = JSON.parse(
40+
(await import('node:fs')).readFileSync(join(cliDir, 'package.json'), 'utf8'),
41+
);
42+
const want = (name) => cliPkg.dependencies?.[name] ?? cliPkg.devDependencies?.[name] ?? 'latest';
43+
44+
rmSync(out, { recursive: true, force: true });
45+
mkdirSync(join(out, 'dist'), { recursive: true });
46+
cpSync(cliJs, join(out, 'dist', 'cli.js'));
47+
48+
// 1. package.json with the pure-JS externals; `npm install` resolves the
49+
// full (flat) transitive closure, including ripgrep's per-platform binary
50+
// package (an optionalDependency).
51+
writeFileSync(
52+
join(out, 'package.json'),
53+
`${JSON.stringify(
54+
{
55+
name: 'coderouter-daemon',
56+
private: true,
57+
type: 'module',
58+
dependencies: {
59+
'@vscode/ripgrep': want('@vscode/ripgrep'),
60+
ink: want('ink'),
61+
react: want('react'),
62+
ws: want('ws'),
63+
},
64+
},
65+
null,
66+
2,
67+
)}\n`,
68+
);
69+
70+
console.log('[stage-daemon] installing daemon runtime deps (ink, react, ws, ripgrep)…');
71+
execFileSync('npm', ['install', '--omit=dev', '--no-audit', '--no-fund', '--no-package-lock'], {
72+
cwd: out,
73+
stdio: 'inherit',
74+
});
75+
76+
// 2. node-pty: copy the already-built package (with NAPI prebuilds) from the
77+
// workspace to avoid a native recompile during install.
78+
const ptySrc = [join(cliDir, 'node_modules', 'node-pty'), join(repoRoot, 'node_modules', 'node-pty')].find(existsSync);
79+
if (ptySrc) {
80+
cpSync(realpathSync(ptySrc), join(out, 'node_modules', 'node-pty'), { recursive: true, dereference: true });
81+
console.log('[stage-daemon] staged node-pty (terminal backend)');
82+
} else {
83+
console.warn('[stage-daemon] node-pty not found in workspace — terminal will be unavailable');
84+
}
85+
86+
console.log(`[stage-daemon] daemon staged at ${out}`);

packages/app/src/pages/Chat.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,13 @@ type Msg = {
1919
};
2020

2121
const EFFORTS = ['low', 'medium', 'high', 'max'] as const;
22-
const MODES = ['agent', 'plan', 'debug', 'review'] as const;
22+
const MODES = ['agent', 'plan', 'masterplan', 'debug', 'review'] as const;
2323

2424
/** Per-mode accent colors so the selector reads like Cursor's mode picker. */
2525
const MODE_META: Record<string, { label: string; dot: string; text: string; chip: string }> = {
2626
agent: { label: 'Agent', dot: 'bg-emerald-500', text: 'text-emerald-500', chip: 'border-emerald-500/40 bg-emerald-500/10' },
2727
plan: { label: 'Plan', dot: 'bg-sky-500', text: 'text-sky-500', chip: 'border-sky-500/40 bg-sky-500/10' },
28+
masterplan: { label: 'Masterplan', dot: 'bg-indigo-500', text: 'text-indigo-500', chip: 'border-indigo-500/40 bg-indigo-500/10' },
2829
debug: { label: 'Debug', dot: 'bg-amber-500', text: 'text-amber-500', chip: 'border-amber-500/40 bg-amber-500/10' },
2930
review: { label: 'Review', dot: 'bg-violet-500', text: 'text-violet-500', chip: 'border-violet-500/40 bg-violet-500/10' },
3031
};

packages/cli/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ In the REPL, type `/` for commands and `@` to reference files. Slash commands in
5252

5353
## Links
5454

55-
- Repository & full docs: <https://github.com/EfeAcar6431/CodeRouter>
56-
- Issues: <https://github.com/EfeAcar6431/CodeRouter/issues>
55+
- Repository & full docs: <https://github.com/Code-Router/CodeRouter>
56+
- Issues: <https://github.com/Code-Router/CodeRouter/issues>
5757

5858
## License
5959

0 commit comments

Comments
 (0)