Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# prepack stash (restored by postpack; transient)
.publish-stash/

# npm pack output
omp-deck-*.tgz

node_modules
dist
.DS_Store
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ omp-deck is the cockpit that holds all of that. The chat surface stays at parity

## Quickstart

### Global install (recommended)

Requires [Bun](https://bun.sh) ≥ 1.3.14.

```sh
npm install -g omp-deck
omp-deck
```

Boots on <http://127.0.0.1:8787>. Data lives in `~/.omp-deck/` (override with `OMP_DECK_DATA_DIR`). Your existing `~/.omp/agent` is picked up automatically — no re-auth.

Other knobs: `OMP_DECK_PORT`, `OMP_DECK_HOST`, `OMP_DECK_DB_PATH`, `OMP_DECK_UPLOADS_ROOT`, `OMP_DECK_WEB_DIST` — see [docs/configuration.md](./docs/configuration.md). Or run `bunx omp-deck` if you'd rather not install globally.

### From source

If `omp` already works in a terminal on this machine:

```sh
Expand All @@ -69,7 +84,7 @@ bun install
bun run dev
```

Open <http://127.0.0.1:5173>. Your existing `~/.omp/agent` is picked up automatically — no re-auth.
Open <http://127.0.0.1:5173>.

On **Windows**, you can also double-click `Start-OMP-Deck.cmd` from the repo root — it boots the server on `:8787`, starts the Vite app on `:5173`, opens the deck in your browser, and writes logs under `.logs/`. On **macOS / Linux**, the sibling is `bash Start-OMP-Deck.sh start` (`stop` / `status` subcommands too); bare invocation runs foreground, same as `bun run dev`.

Expand Down
107 changes: 107 additions & 0 deletions bin/omp-deck.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env node
// omp-deck CLI entrypoint.
//
// This is a tiny Node-runnable shim. It checks for Bun on PATH (the deck is a
// Bun-native server) and spawns the bundled server, inheriting stdio + signals
// + exit code. Default data directory is ~/.omp-deck; overridable via
// OMP_DECK_DATA_DIR or the existing OMP_DECK_DB_PATH / OMP_DECK_UPLOADS_ROOT
// env vars. Default web dist is the bundled `apps/web/dist/` shipped in the
// package; overridable via OMP_DECK_WEB_DIST.
//
// Why Node, not Bun: the user may not have Bun yet — we want to print an
// actionable install message instead of an ENOENT.

import { spawn, spawnSync } from "node:child_process";
import { existsSync, mkdirSync } from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { fileURLToPath } from "node:url";

const HERE = path.dirname(fileURLToPath(import.meta.url));
// Package root is the directory containing `bin/`.
const PKG_ROOT = path.resolve(HERE, "..");
const SERVER_ENTRY = path.join(PKG_ROOT, "apps", "server", "src", "index.ts");
const WEB_DIST = path.join(PKG_ROOT, "apps", "web", "dist");
const STARTER_SKILLS = path.join(PKG_ROOT, "starter-skills");
const STARTER_EXTENSIONS = path.join(PKG_ROOT, "starter-extensions");

function fail(msg) {
console.error(`omp-deck: ${msg}`);
process.exit(1);
}

function ensureBun() {
const probe = spawnSync(process.platform === "win32" ? "where" : "which", ["bun"], {
stdio: ["ignore", "pipe", "ignore"],
});
if (probe.status === 0 && probe.stdout.toString().trim().length > 0) return;
console.error("omp-deck requires Bun (https://bun.sh) — not found on PATH.");
console.error("");
console.error("Install:");
console.error(" curl -fsSL https://bun.sh/install | bash (macOS / Linux)");
console.error(" powershell -c \"irm bun.sh/install.ps1 | iex\" (Windows)");
console.error("");
console.error("Then re-run: omp-deck");
process.exit(127);
}

function resolveDataDir() {
const explicit = process.env.OMP_DECK_DATA_DIR?.trim();
if (explicit) return path.resolve(explicit);
return path.join(os.homedir(), ".omp-deck");
}

function main() {
if (!existsSync(SERVER_ENTRY)) {
fail(`server entry missing at ${SERVER_ENTRY} — broken install?`);
}
ensureBun();

const dataDir = resolveDataDir();
mkdirSync(dataDir, { recursive: true });

const env = { ...process.env };
// Only set defaults — let user overrides win.
env.OMP_DECK_DB_PATH ??= path.join(dataDir, "deck.db");
env.OMP_DECK_UPLOADS_ROOT ??= path.join(dataDir, "uploads");
env.OMP_DECK_WEB_DIST ??= WEB_DIST;
env.OMP_DECK_STARTER_SKILLS_DIR ??= STARTER_SKILLS;
env.OMP_DECK_STARTER_EXTENSIONS_DIR ??= STARTER_EXTENSIONS;
// Default cwd: the data dir, not wherever the user happened to invoke from.
// The agent's own session cwd is independent and still defaults to $HOME.
env.OMP_DECK_DEFAULT_CWD ??= os.homedir();

const args = process.argv.slice(2);
const child = spawn("bun", [SERVER_ENTRY, ...args], {
stdio: "inherit",
env,
// Bun resolves relative imports against the script path; cwd here only
// influences where Bun looks for bunfig.toml — keep it at package root
// so workspace settings (if any) apply.
cwd: PKG_ROOT,
});

function forward(sig) {
try {
child.kill(sig);
} catch {
/* child already exited */
}
}
process.on("SIGINT", () => forward("SIGINT"));
process.on("SIGTERM", () => forward("SIGTERM"));

child.on("exit", (code, signal) => {
if (signal) {
// Re-raise the signal in this process so the parent shell sees it.
process.kill(process.pid, signal);
} else {
process.exit(code ?? 0);
}
});
child.on("error", (err) => {
fail(`failed to spawn bun: ${err.message}`);
});
}

main();
37 changes: 34 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "omp-deck",
"version": "0.5.0",
"private": true,
"private": false,
"description": "Cockpit web UI for the omp (oh-my-pi) coding agent — multi-session chat, kanban, cron routines, inbox, messaging bridges, and a marketplace browser. Designed to be served loopback-only behind a Tailscale gate or SSH tunnel.",
"keywords": [
"omp",
Expand Down Expand Up @@ -40,11 +40,42 @@
"build": "bun run --filter '@omp-deck/web' build && bun run --filter '@omp-deck/server' build",
"start": "bun run --filter '@omp-deck/server' start",
"typecheck": "bun run --filter='@omp-deck/*' typecheck",
"clean": "rm -rf apps/*/dist apps/*/node_modules apps/bridges/*/node_modules packages/*/node_modules node_modules"
"clean": "rm -rf apps/*/dist apps/*/node_modules apps/bridges/*/node_modules packages/*/node_modules node_modules",
"prepack": "node scripts/prepack.mjs",
"postpack": "node scripts/postpack.mjs"
},
"bin": {
"omp-deck": "./bin/omp-deck.mjs"
},
"files": [
"bin",
"apps/server/src",
"apps/server/package.json",
"apps/web/dist",
"starter-skills",
"starter-extensions",
"README.md",
"LICENSE",
"CHANGELOG.md"
],
"engines": {
"bun": ">=1.3.14"
"bun": ">=1.3.14",
"node": ">=18"
},
"dependencies": {
"@oh-my-pi/pi-ai": "15.1.7",
"@oh-my-pi/pi-coding-agent": "15.1.7",
"@omp-deck/protocol": "0.5.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"croner": "^10.0.1",
"hono": "^4.6.14",
"quickjs-emscripten": "^0.31.0",
"yaml": "^2.9.0"
},
"bundledDependencies": [
"@omp-deck/protocol"
],
"devDependencies": {
"typescript": "^5.6.3"
}
Expand Down
53 changes: 53 additions & 0 deletions scripts/postpack.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env node
// Post-pack cleanup. Reverses the prepack changes so the dev workflow keeps
// working after a local `npm pack` / `npm publish`. Safe to run twice.
//
// Two jobs (mirror prepack):
// 1. Restore stashed files (paper-trading templates, tests, source maps)
// from `.publish-stash/` back to their original paths, using the
// manifest written by prepack.
// 2. Drop the materialized `node_modules/@omp-deck/protocol` and let bun
// restore the workspace symlink via `bun install`.

import { existsSync, mkdirSync, readFileSync, renameSync, rmSync } from "node:fs";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
import { spawnSync } from "node:child_process";

const HERE = path.dirname(fileURLToPath(import.meta.url));
const ROOT = path.resolve(HERE, "..");
const PROTOCOL_DEST = path.join(ROOT, "node_modules", "@omp-deck", "protocol");
const STASH = path.join(ROOT, ".publish-stash");
const STASH_MANIFEST = path.join(STASH, "manifest.json");

// 1. Restore stashed files.
if (existsSync(STASH_MANIFEST)) {
const manifest = JSON.parse(readFileSync(STASH_MANIFEST, "utf8"));
let restored = 0;
for (const rel of manifest.stashed ?? []) {
const from = path.join(STASH, rel);
const to = path.join(ROOT, rel);
if (!existsSync(from)) continue;
mkdirSync(path.dirname(to), { recursive: true });
renameSync(from, to);
restored += 1;
}
rmSync(STASH, { recursive: true, force: true });
process.stdout.write(`postpack: restored ${restored} stashed file(s)\n`);
}

// 2. Drop materialized protocol so the symlink can come back.
if (existsSync(PROTOCOL_DEST)) {
rmSync(PROTOCOL_DEST, { recursive: true, force: true });
process.stdout.write("postpack: removed materialized @omp-deck/protocol\n");
}

const r = spawnSync("bun", ["install", "--frozen-lockfile"], {
cwd: ROOT,
stdio: "inherit",
});
if (r.status !== 0) {
process.stderr.write(
"postpack: bun install failed; restore the workspace manually with `bun install`.\n",
);
}
Loading
Loading