feat(HAI-002): complete React conversion with build fixes and project setup

- Fix Vite alias to resolve @hai/core to types-only module (avoids Node.js deps in browser bundle)
- Add tsconfig.app.json for client-side typecheck
- Fix server.ts return type annotation
- Update imports to use @hai/core (resolved via Vite alias)
- Add base project files needed for workspace
This commit is contained in:
Dustin Byrne
2026-03-25 18:29:50 -04:00
parent e0746fb387
commit 6a754cf199
31 changed files with 1534 additions and 3 deletions

22
packages/cli/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "hai",
"version": "0.1.0",
"type": "module",
"bin": {
"hai": "./dist/bin.js"
},
"scripts": {
"dev": "tsx src/bin.ts",
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@hai/core": "workspace:*",
"@hai/dashboard": "workspace:*",
"@hai/engine": "workspace:*"
},
"devDependencies": {
"tsx": "^4.19.0",
"typescript": "^5.7.0"
}
}

89
packages/cli/src/bin.ts Normal file
View File

@@ -0,0 +1,89 @@
#!/usr/bin/env node
import { runDashboard } from "./commands/dashboard.js";
import { runTaskCreate, runTaskList, runTaskMove } from "./commands/task.js";
const HELP = `
hai — AI-orchestrated task board
Usage:
hai dashboard Start the board web UI
hai task create [title] Create a new task (goes to triage)
hai task list List all tasks
hai task move <id> <col> Move a task to a column
Options:
--port, -p <port> Dashboard port (default: 4040)
--engine Enable AI engine (auto-specify + execute tasks)
--help, -h Show this help
Columns: triage, todo, in-progress, in-review, done
The AI engine uses pi (github.com/badlogic/pi-mono) for agent sessions.
Requires configured API keys — run "pi" first to set up authentication.
`.trim();
async function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
console.log(HELP);
process.exit(0);
}
const command = args[0];
try {
switch (command) {
case "dashboard": {
const portIdx = args.indexOf("--port");
const portIdxShort = args.indexOf("-p");
const pi = portIdx !== -1 ? portIdx : portIdxShort;
const port = pi !== -1 ? parseInt(args[pi + 1], 10) : 4040;
const engine = args.includes("--engine");
await runDashboard(port, { engine });
break;
}
case "task": {
const subcommand = args[1];
switch (subcommand) {
case "create": {
const title = args.slice(2).join(" ");
await runTaskCreate(title || undefined);
break;
}
case "list":
case "ls":
await runTaskList();
break;
case "move": {
const id = args[2];
const column = args[3];
if (!id || !column) {
console.error("Usage: hai task move <id> <column>");
process.exit(1);
}
await runTaskMove(id, column);
break;
}
default:
console.error(`Unknown subcommand: task ${subcommand || ""}`);
console.log("Try: hai task create | list | move");
process.exit(1);
}
break;
}
default:
console.error(`Unknown command: ${command}`);
console.log(HELP);
process.exit(1);
}
} catch (err: any) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
}
main();

View File

@@ -0,0 +1,60 @@
import { TaskStore } from "@hai/core";
import { createServer } from "@hai/dashboard";
import { TriageProcessor, TaskExecutor, Scheduler } from "@hai/engine";
export async function runDashboard(port: number, opts: { engine?: boolean } = {}) {
const cwd = process.cwd();
const store = new TaskStore(cwd);
await store.init();
// Start the web server
const app = createServer(store);
// Optionally start the AI engine
if (opts.engine) {
const triage = new TriageProcessor(store, cwd, {
onSpecifyStart: (t) => console.log(`[engine] Specifying ${t.id}...`),
onSpecifyComplete: (t) => console.log(`[engine] ✓ ${t.id} → todo`),
onSpecifyError: (t, e) => console.log(`[engine] ✗ ${t.id}: ${e.message}`),
});
const executor = new TaskExecutor(store, cwd, {
onStart: (t, p) => console.log(`[engine] Executing ${t.id} in ${p}`),
onComplete: (t) => console.log(`[engine] ✓ ${t.id} → in-review`),
onError: (t, e) => console.log(`[engine] ✗ ${t.id}: ${e.message}`),
});
const scheduler = new Scheduler(store, {
maxConcurrent: 2,
onSchedule: (t) => console.log(`[engine] Scheduled ${t.id}`),
onBlocked: (t, deps) => console.log(`[engine] ${t.id} blocked by ${deps.join(", ")}`),
});
triage.start();
scheduler.start();
process.on("SIGINT", () => {
triage.stop();
scheduler.stop();
process.exit(0);
});
}
app.listen(port, () => {
console.log();
console.log(` hai board`);
console.log(` ────────────────────────`);
console.log(` → http://localhost:${port}`);
console.log();
console.log(` Tasks stored in .hai/tasks/`);
if (opts.engine) {
console.log(` AI engine: ✓ active`);
console.log(` • triage: auto-specifying tasks`);
console.log(` • scheduler: dependency-aware execution`);
} else {
console.log(` AI engine: off (use --engine to enable)`);
}
console.log(` Press Ctrl+C to stop`);
console.log();
});
}

View File

@@ -0,0 +1,78 @@
import { TaskStore, COLUMNS, COLUMN_LABELS, type Column } from "@hai/core";
import { createInterface } from "node:readline/promises";
async function getStore(): Promise<TaskStore> {
const store = new TaskStore(process.cwd());
await store.init();
return store;
}
export async function runTaskCreate(titleArg?: string) {
let title = titleArg;
if (!title) {
const rl = createInterface({ input: process.stdin, output: process.stdout });
title = await rl.question("Task title: ");
rl.close();
}
if (!title?.trim()) {
console.error("Title is required");
process.exit(1);
}
const store = await getStore();
const task = await store.createTask({ title: title.trim() });
console.log();
console.log(` ✓ Created ${task.id}: ${task.title}`);
console.log(` Column: triage`);
console.log(` Path: .hai/tasks/${task.id}/`);
console.log();
}
export async function runTaskList() {
const store = await getStore();
const tasks = await store.listTasks();
if (tasks.length === 0) {
console.log("\n No tasks yet. Create one with: hai task create\n");
return;
}
console.log();
for (const col of COLUMNS) {
const colTasks = tasks.filter((t) => t.column === col);
if (colTasks.length === 0) continue;
const label = COLUMN_LABELS[col];
const dot =
col === "triage" ? "●" :
col === "todo" ? "●" :
col === "in-progress" ? "●" :
col === "in-review" ? "●" : "○";
console.log(` ${dot} ${label} (${colTasks.length})`);
for (const t of colTasks) {
const deps = t.dependencies.length ? ` [deps: ${t.dependencies.join(", ")}]` : "";
console.log(` ${t.id} ${t.title}${deps}`);
}
console.log();
}
}
export async function runTaskMove(id: string, column: string) {
if (!COLUMNS.includes(column as Column)) {
console.error(`Invalid column: ${column}`);
console.error(`Valid columns: ${COLUMNS.join(", ")}`);
process.exit(1);
}
const store = await getStore();
const task = await store.moveTask(id, column as Column);
console.log();
console.log(` ✓ Moved ${task.id}${COLUMN_LABELS[task.column as Column]}`);
console.log();
}

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}