projectinator 0.3.0 → 0.3.1
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/README.md +25 -7
- package/bin/projectinator.mjs +39 -1
- package/package.json +1 -1
- package/src/cli.ts +263 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
[](https://www.npmjs.com/package/projectinator)
|
|
8
8
|

|
|
9
9
|

|
|
10
|
-

|
|
11
11
|

|
|
12
12
|

|
|
13
13
|
|
|
@@ -30,6 +30,8 @@ npx projectinator # run without installing
|
|
|
30
30
|
npm install -g projectinator # then just: projectinator
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
`projectinator --help` / `--version` work without opening the app; `projectinator doctor` checks your setup.
|
|
34
|
+
|
|
33
35
|
> **It's a CLI, not a library.** `npm install projectinator` (without `-g`) only drops it into a
|
|
34
36
|
> project's `node_modules` — it won't create a runnable command. Use `npx projectinator` or
|
|
35
37
|
> `npm install -g projectinator`. (If `-g` installs but the command isn't found, npm's global
|
|
@@ -120,16 +122,32 @@ Real, unedited output from a full build, kept in [`examples/`](examples/):
|
|
|
120
122
|
|
|
121
123
|
## CLI (same engine, for scripting/CI)
|
|
122
124
|
|
|
125
|
+
Everything below uses the same workspace as the cockpit, so headless builds show up in the app.
|
|
126
|
+
|
|
123
127
|
```bash
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
128
|
+
projectinator doctor # Node, keys, Chromium, Pi catalog, git
|
|
129
|
+
projectinator build "a tip calculator" --dry-run # plan + estimate only (one PM call)
|
|
130
|
+
projectinator build "a tip calculator" --yes # build without the confirmation prompt
|
|
131
|
+
projectinator build "…" --json --budget 2 --provider anthropic # NDJSON events; cap; lock provider
|
|
132
|
+
projectinator build "…" --task-cap 0.5 --task-timeout 5 # per-task limits (USD / minutes)
|
|
133
|
+
projectinator projects # past builds, status, cost
|
|
134
|
+
projectinator models # the roster as it will run, with prices
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Exit codes: `0` ok · `1` environment problem · `2` bad usage · `3` build halted (resume it in the app).
|
|
138
|
+
|
|
139
|
+
<details>
|
|
140
|
+
<summary>Developer scripts (from a clone)</summary>
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
npm start # the cockpit
|
|
144
|
+
npm run build -- --live --mini # fixed 4-task build, cheap end-to-end proof (~$0.10)
|
|
145
|
+
npm run build -- --live --mini --resume # resume a halted run
|
|
129
146
|
npm run bakeoff -- --capability design "Design a pricing page" # model bake-off
|
|
130
|
-
npm test #
|
|
147
|
+
npm test # 187 tests
|
|
131
148
|
npm run typecheck
|
|
132
149
|
```
|
|
150
|
+
</details>
|
|
133
151
|
|
|
134
152
|
## Configuration & data
|
|
135
153
|
|
package/bin/projectinator.mjs
CHANGED
|
@@ -2,14 +2,52 @@
|
|
|
2
2
|
// Launcher for `projectinator` / `npx github:smanookian/projectinator`.
|
|
3
3
|
// Runs the Ink TUI (TypeScript) through tsx — no build step, no compiled dist.
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
7
8
|
|
|
8
9
|
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
9
10
|
const entry = join(root, "src", "tui.tsx");
|
|
10
11
|
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
const { version } = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
14
|
+
|
|
15
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
16
|
+
console.log(version);
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
if (args.includes("--help") || args.includes("-h") || args[0] === "help") {
|
|
20
|
+
console.log(`projectinator ${version} — your AI build team in the terminal
|
|
21
|
+
|
|
22
|
+
Usage
|
|
23
|
+
projectinator open the cockpit (TUI)
|
|
24
|
+
projectinator doctor check Node, API keys, Chromium, Pi catalog, git
|
|
25
|
+
projectinator build "<idea>" [...] plan + build headless (--dry-run, --yes, --json,
|
|
26
|
+
--budget, --provider, --concurrency, --task-cap, --task-timeout)
|
|
27
|
+
projectinator projects list past builds with status and cost
|
|
28
|
+
projectinator models the roster as it will run, with prices
|
|
29
|
+
projectinator --version | --help
|
|
30
|
+
|
|
31
|
+
Setup
|
|
32
|
+
Inside the app: Settings → API keys (Anthropic, OpenAI, Gemini, OpenRouter).
|
|
33
|
+
Keys live in ~/.projectinator/config.json (chmod 0600).
|
|
34
|
+
Optional: npx playwright install chromium → lets the tester run the app headless.
|
|
35
|
+
|
|
36
|
+
Exit codes: 0 ok · 1 environment problem · 2 bad usage · 3 build halted
|
|
37
|
+
Docs: https://github.com/smanookian/projectinator#readme`);
|
|
38
|
+
process.exit(0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const COMMANDS = new Set(["doctor", "build", "projects", "models"]);
|
|
42
|
+
if (args.length && !COMMANDS.has(args[0])) {
|
|
43
|
+
console.error(`projectinator: unknown ${args[0].startsWith("-") ? "option" : "command"} "${args[0]}". Try --help.`);
|
|
44
|
+
process.exit(2);
|
|
45
|
+
}
|
|
46
|
+
|
|
11
47
|
// `node --import tsx <entry>` registers tsx's loader, then runs the TS entry.
|
|
12
|
-
|
|
48
|
+
// No command → the TUI; a command → the headless CLI with the remaining args.
|
|
49
|
+
const target = args.length ? join(root, "src", "cli.ts") : entry;
|
|
50
|
+
const res = spawnSync(process.execPath, ["--import", "tsx", target, ...args], {
|
|
13
51
|
stdio: "inherit",
|
|
14
52
|
cwd: root,
|
|
15
53
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "projectinator",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Your AI build team in the terminal — hand it an app idea, a PM model plans a Scrum backlog, and the best model per role designs, codes, and tests it into working files. Bring your own API key.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// Headless CLI — `projectinator <command>`. Same engine as the cockpit (engine.ts),
|
|
2
|
+
// same workspace root, so builds started here show up in the TUI's project list.
|
|
3
|
+
//
|
|
4
|
+
// doctor check Node, keys, Chromium, Pi catalog, git
|
|
5
|
+
// build "<idea>" [...] plan + build without the TUI
|
|
6
|
+
// projects list past builds
|
|
7
|
+
// models the roster as it will actually run, with prices
|
|
8
|
+
//
|
|
9
|
+
// The launcher (bin/projectinator.mjs) handles --version/--help itself and only
|
|
10
|
+
// spawns this file for a real command, so those stay instant.
|
|
11
|
+
|
|
12
|
+
import { createInterface } from "node:readline";
|
|
13
|
+
import { spawnSync } from "node:child_process";
|
|
14
|
+
import { accessSync, constants, mkdirSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import type { OrchestratorEvent } from "./orchestrator.js";
|
|
18
|
+
import type { Provider } from "./types.js";
|
|
19
|
+
import { MODELS, getModel } from "./models.js";
|
|
20
|
+
import { REGISTRY } from "./registry.js";
|
|
21
|
+
import { piRuntime, resolvePiModel } from "./executor.js";
|
|
22
|
+
import { chromiumAvailable, CHROMIUM_INSTALL_HINT } from "./preview.js";
|
|
23
|
+
import { applyKeysToEnv, getPrefs, loadConfig, ENV_VAR } from "./tui/config.js";
|
|
24
|
+
import {
|
|
25
|
+
availableProviders,
|
|
26
|
+
effectiveRoster,
|
|
27
|
+
listProjects,
|
|
28
|
+
planBuild,
|
|
29
|
+
startBuild,
|
|
30
|
+
PROVIDER_LABEL,
|
|
31
|
+
} from "./tui/engine.js";
|
|
32
|
+
|
|
33
|
+
const NODE_MIN = "22.19.0";
|
|
34
|
+
const money = (n: number) => `$${n.toFixed(2)}`;
|
|
35
|
+
|
|
36
|
+
// ---- tiny argv parser: `--flag value`, `--flag=value`, `--bool`, positionals ----
|
|
37
|
+
|
|
38
|
+
interface Argv {
|
|
39
|
+
positional: string[];
|
|
40
|
+
flags: Record<string, string | true>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function parseArgv(args: string[]): Argv {
|
|
44
|
+
const out: Argv = { positional: [], flags: {} };
|
|
45
|
+
for (let i = 0; i < args.length; i++) {
|
|
46
|
+
const a = args[i]!;
|
|
47
|
+
if (!a.startsWith("--")) { out.positional.push(a); continue; }
|
|
48
|
+
const eq = a.indexOf("=");
|
|
49
|
+
if (eq > 0) { out.flags[a.slice(2, eq)] = a.slice(eq + 1); continue; }
|
|
50
|
+
const next = args[i + 1];
|
|
51
|
+
if (next !== undefined && !next.startsWith("--")) { out.flags[a.slice(2)] = next; i++; }
|
|
52
|
+
else out.flags[a.slice(2)] = true;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function num(v: string | true | undefined, fallback: number): number {
|
|
58
|
+
if (v === undefined || v === true) return fallback;
|
|
59
|
+
const n = parseFloat(v);
|
|
60
|
+
return Number.isFinite(n) ? n : fallback;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function semverGte(a: string, b: string): boolean {
|
|
64
|
+
const pa = a.split(".").map(Number);
|
|
65
|
+
const pb = b.split(".").map(Number);
|
|
66
|
+
for (let i = 0; i < 3; i++) {
|
|
67
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0)) return true;
|
|
68
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0)) return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- doctor ----
|
|
74
|
+
|
|
75
|
+
type Check = { label: string; ok: boolean; detail: string; fatal?: boolean };
|
|
76
|
+
|
|
77
|
+
async function doctor(): Promise<number> {
|
|
78
|
+
const checks: Check[] = [];
|
|
79
|
+
|
|
80
|
+
const node = process.versions.node;
|
|
81
|
+
checks.push({ label: "Node", ok: semverGte(node, NODE_MIN), detail: `v${node} (need ≥ ${NODE_MIN})`, fatal: true });
|
|
82
|
+
|
|
83
|
+
const cfg = loadConfig();
|
|
84
|
+
const providers = availableProviders();
|
|
85
|
+
for (const p of Object.keys(ENV_VAR) as Provider[]) {
|
|
86
|
+
const has = providers.includes(p);
|
|
87
|
+
const src = cfg.keys[p] ? "~/.projectinator/config.json" : has ? "env" : "";
|
|
88
|
+
checks.push({ label: `Key: ${PROVIDER_LABEL[p]}`, ok: has, detail: has ? `set (${src})` : `not set — Settings → API keys, or export ${ENV_VAR[p]}` });
|
|
89
|
+
}
|
|
90
|
+
if (providers.length === 0) checks.push({ label: "Any provider key", ok: false, detail: "no keys at all — nothing can run", fatal: true });
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const runtime = await piRuntime();
|
|
94
|
+
const bad: string[] = [];
|
|
95
|
+
for (const e of REGISTRY) for (const b of ["api", "web"] as const) {
|
|
96
|
+
try { resolvePiModel(runtime, e.byBackend[b].provider, e.byBackend[b].model); } catch { bad.push(`${e.capability}/${e.tier}/${b}`); }
|
|
97
|
+
}
|
|
98
|
+
checks.push({ label: "Pi model catalog", ok: bad.length === 0, detail: bad.length ? `unresolved: ${bad.join(", ")}` : `${Object.keys(MODELS).length} models priced, every registry pick resolves`, fatal: true });
|
|
99
|
+
} catch (e) {
|
|
100
|
+
checks.push({ label: "Pi model catalog", ok: false, detail: e instanceof Error ? e.message : String(e), fatal: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const chromium = await chromiumAvailable();
|
|
104
|
+
checks.push({ label: "Headless Chromium", ok: chromium, detail: chromium ? "installed — tester runs the app" : `missing — tests will be code-reading only (PASS*); ${CHROMIUM_INSTALL_HINT}` });
|
|
105
|
+
|
|
106
|
+
const git = spawnSync("git", ["--version"], { encoding: "utf8" });
|
|
107
|
+
checks.push({ label: "git", ok: git.status === 0, detail: git.status === 0 ? git.stdout.trim() : "not found — builds won't be versioned (undo/history disabled)" });
|
|
108
|
+
|
|
109
|
+
const home = join(homedir(), ".projectinator");
|
|
110
|
+
try { mkdirSync(home, { recursive: true }); accessSync(home, constants.W_OK); checks.push({ label: "Data dir", ok: true, detail: home }); }
|
|
111
|
+
catch { checks.push({ label: "Data dir", ok: false, detail: `${home} not writable`, fatal: true }); }
|
|
112
|
+
|
|
113
|
+
const prefs = getPrefs();
|
|
114
|
+
checks.push({ label: "Prefs", ok: true, detail: `budget cap ${money(prefs.budgetCapUSD)} · ${prefs.concurrency} at once · task limits ${prefs.taskTimeoutMin || "∞"} min / ${prefs.taskCostCapUSD ? money(prefs.taskCostCapUSD) : "∞"}` });
|
|
115
|
+
|
|
116
|
+
console.log("\n projectinator doctor\n");
|
|
117
|
+
for (const c of checks) console.log(` ${c.ok ? "✓" : c.fatal ? "✗" : "!"} ${c.label.padEnd(22)} ${c.detail}`);
|
|
118
|
+
const fatal = checks.filter((c) => !c.ok && c.fatal);
|
|
119
|
+
const warn = checks.filter((c) => !c.ok && !c.fatal);
|
|
120
|
+
console.log(`\n ${fatal.length ? `${fatal.length} blocking problem${fatal.length === 1 ? "" : "s"}` : "Ready"}${warn.length ? ` · ${warn.length} warning${warn.length === 1 ? "" : "s"}` : ""}\n`);
|
|
121
|
+
return fatal.length ? 1 : 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---- projects ----
|
|
125
|
+
|
|
126
|
+
function projects(): number {
|
|
127
|
+
const list = listProjects();
|
|
128
|
+
if (!list.length) { console.log("\n No projects yet. Run `projectinator` or `projectinator build \"an idea\"`.\n"); return 0; }
|
|
129
|
+
console.log("");
|
|
130
|
+
for (const p of list) {
|
|
131
|
+
const done = p.state.outcomes.filter((o) => !o.error).length;
|
|
132
|
+
const mark = p.status === "complete" ? "✓" : p.status === "halted" ? "⚠" : "…";
|
|
133
|
+
console.log(` ${mark} ${p.slug.padEnd(40)} ${p.status.padEnd(9)} ${money(p.totalCost).padStart(8)} ${done}/${p.taskCount} tasks`);
|
|
134
|
+
console.log(` ${p.idea.slice(0, 90)}${p.idea.length > 90 ? "…" : ""}`);
|
|
135
|
+
}
|
|
136
|
+
console.log(`\n ${list.length} project${list.length === 1 ? "" : "s"} · ${money(list.reduce((a, p) => a + p.totalCost, 0))} all time · ${join(homedir(), "…")}\n`);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- models ----
|
|
141
|
+
|
|
142
|
+
function models(): number {
|
|
143
|
+
const providers = availableProviders();
|
|
144
|
+
console.log(`\n Roster as it will run now${providers.length ? ` (keys: ${providers.join(", ")})` : " (no keys — best-of-breed picks shown)"}\n`);
|
|
145
|
+
for (const r of effectiveRoster()) {
|
|
146
|
+
const m = r.model ? getModel(r.model) : undefined;
|
|
147
|
+
const price = m ? `$${m.cost.input}/$${m.cost.output} per 1M` : "";
|
|
148
|
+
console.log(` ${r.label.padEnd(18)} ${(r.model ?? "—").padEnd(28)} ${(r.provider ?? "").padEnd(11)} ${price}`);
|
|
149
|
+
}
|
|
150
|
+
console.log("\n Change in the app: Settings → Models. Prices are from Pi's catalog; actual cost is measured per run.\n");
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---- build ----
|
|
155
|
+
|
|
156
|
+
function ask(question: string): Promise<string> {
|
|
157
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
158
|
+
const { promise, resolve } = Promise.withResolvers<string>();
|
|
159
|
+
rl.question(question, (a) => { rl.close(); resolve(a.trim()); });
|
|
160
|
+
return promise;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function build(argv: Argv): Promise<number> {
|
|
164
|
+
const idea = argv.positional.join(" ").trim();
|
|
165
|
+
const json = argv.flags.json === true;
|
|
166
|
+
const yes = json || argv.flags.yes === true || argv.flags.y === true;
|
|
167
|
+
const dry = argv.flags["dry-run"] === true;
|
|
168
|
+
const emit = (o: Record<string, unknown>) => { if (json) process.stdout.write(JSON.stringify(o) + "\n"); };
|
|
169
|
+
const say = (s: string) => { if (!json) console.log(s); };
|
|
170
|
+
|
|
171
|
+
if (!idea) { console.error(" build: give an idea, e.g. projectinator build \"a tip calculator\""); return 2; }
|
|
172
|
+
|
|
173
|
+
let providers = availableProviders();
|
|
174
|
+
const lock = argv.flags.provider;
|
|
175
|
+
if (typeof lock === "string") {
|
|
176
|
+
if (!(lock in PROVIDER_LABEL)) { console.error(` build: unknown provider "${lock}" (anthropic | openai | google | openrouter)`); return 2; }
|
|
177
|
+
if (!providers.includes(lock as Provider)) { console.error(` build: no key for ${lock} — export ${ENV_VAR[lock as Provider]} or set it in the app`); return 1; }
|
|
178
|
+
providers = [lock as Provider];
|
|
179
|
+
}
|
|
180
|
+
if (!providers.length) { console.error(" build: no API key found. Run `projectinator doctor`."); return 1; }
|
|
181
|
+
|
|
182
|
+
const prefs = getPrefs();
|
|
183
|
+
const budget = num(argv.flags.budget, prefs.budgetCapUSD);
|
|
184
|
+
const concurrency = Math.max(1, Math.floor(num(argv.flags.concurrency, prefs.concurrency)));
|
|
185
|
+
const taskLimits = {
|
|
186
|
+
timeoutMs: num(argv.flags["task-timeout"], prefs.taskTimeoutMin) * 60_000,
|
|
187
|
+
costCapUSD: num(argv.flags["task-cap"], prefs.taskCostCapUSD),
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
say(`\n Planning with the PM (${providers.join("/")})…`);
|
|
191
|
+
const plan = await planBuild(idea, providers);
|
|
192
|
+
emit({ event: "plan", provider: plan.provider, modelId: plan.modelId, estCost: plan.estCost, tasks: plan.tasks });
|
|
193
|
+
say(`\n ${plan.tasks.length} tasks · estimated ${money(plan.estCost)} · cap ${money(budget)}${plan.lock ? ` · locked to ${plan.lock}` : ""}\n`);
|
|
194
|
+
for (const t of plan.tasks) {
|
|
195
|
+
const dep = t.dependsOn?.length ? ` ← ${t.dependsOn.join(", ")}` : "";
|
|
196
|
+
say(` ${t.id.padEnd(6)} ${t.capability.padEnd(7)} ${t.difficulty.padEnd(8)} ${t.title}${dep}`);
|
|
197
|
+
}
|
|
198
|
+
if (plan.estCost > budget) say(`\n ⚠ Estimate exceeds the cap — the build may halt partway.`);
|
|
199
|
+
|
|
200
|
+
if (dry) { say("\n Dry run — nothing built (the PM call above was the only spend).\n"); return 0; }
|
|
201
|
+
if (!yes) {
|
|
202
|
+
const a = await ask(`\n Build it for ~${money(plan.estCost)}? [y/N] `);
|
|
203
|
+
if (!/^y(es)?$/i.test(a)) { say(" Cancelled.\n"); return 0; }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const onEvent = (e: OrchestratorEvent) => {
|
|
207
|
+
emit({ event: e.type, ...e });
|
|
208
|
+
if (json) return;
|
|
209
|
+
if (e.type === "task_start") console.log(` ▶ ${e.task.id} [${e.task.capability}] → ${e.provider}/${e.modelId}${e.round ? ` (round ${e.round})` : ""}`);
|
|
210
|
+
else if (e.type === "task_done") console.log(` ✓ ${e.outcome.taskId} ${money(e.outcome.cost)} running ${money(e.runningTotal)}${e.outcome.verdict ? ` ${e.outcome.verdict.passed ? (e.outcome.verdict.runtimeChecked || e.outcome.capability !== "test" ? "PASS" : "PASS* (app not executed)") : "FAIL"}` : ""}`);
|
|
211
|
+
else if (e.type === "task_failed") console.log(` ⛔ ${e.outcome.taskId} aborted: ${e.outcome.error} — billed ${money(e.outcome.cost)}`);
|
|
212
|
+
else if (e.type === "task_skipped") console.log(` · ${e.taskId} skipped`);
|
|
213
|
+
else if (e.type === "test_failed") console.log(` ✗ ${e.taskId} failed (${e.bugs} bugs) — round ${e.round}`);
|
|
214
|
+
else if (e.type === "retry_dev") console.log(` ↻ re-running ${e.taskId} to fix ${e.forTest}`);
|
|
215
|
+
else if (e.type === "budget_halt") console.log(` ⚠ budget halt at ${money(e.runningTotal)} (cap ${money(e.cap)})`);
|
|
216
|
+
else if (e.type === "cycle_or_error") console.log(` ✗ ${e.message}`);
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
say(`\n Building…\n`);
|
|
220
|
+
const handle = startBuild(idea, plan, { concurrency, budgetCapUSD: budget, taskLimits, onEvent, mode: "auto" });
|
|
221
|
+
const r = await handle.promise;
|
|
222
|
+
emit({ event: "done", halted: r.halted, haltReason: r.haltReason, totalCost: r.totalCost, files: r.files, workspace: handle.workspace });
|
|
223
|
+
say(`\n ${r.halted ? `⚠ Halted (${r.haltReason ?? "?"})` : "✓ Complete"} · ${money(r.totalCost)} · ${r.files.length} file${r.files.length === 1 ? "" : "s"}`);
|
|
224
|
+
say(` ${handle.workspace}\n`);
|
|
225
|
+
return r.halted ? 3 : 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---- main ----
|
|
229
|
+
|
|
230
|
+
const USAGE = `Usage: projectinator <command> [options]
|
|
231
|
+
|
|
232
|
+
doctor check Node, API keys, Chromium, Pi catalog, git
|
|
233
|
+
build "<idea>" [options] plan + build headless (same workspace as the app)
|
|
234
|
+
--dry-run plan and estimate only (spends one PM call)
|
|
235
|
+
--yes, -y don't ask before building
|
|
236
|
+
--json NDJSON events on stdout (implies --yes)
|
|
237
|
+
--budget <usd> cap for this build (default: your prefs)
|
|
238
|
+
--provider <name> anthropic|openai|google|openrouter (default: prefs / keys)
|
|
239
|
+
--concurrency <n> tasks at once (default: your prefs)
|
|
240
|
+
--task-cap <usd> per-task cost ceiling (default: your prefs; 0 = off)
|
|
241
|
+
--task-timeout <min> per-task timeout (default: your prefs; 0 = off)
|
|
242
|
+
projects list past builds with status and cost
|
|
243
|
+
models the roster as it will run, with prices
|
|
244
|
+
|
|
245
|
+
Exit codes: 0 ok · 1 environment problem · 2 bad usage · 3 build halted`;
|
|
246
|
+
|
|
247
|
+
export async function main(args: string[]): Promise<number> {
|
|
248
|
+
applyKeysToEnv();
|
|
249
|
+
const argv = parseArgv(args);
|
|
250
|
+
const cmd = argv.positional.shift();
|
|
251
|
+
switch (cmd) {
|
|
252
|
+
case "doctor": return doctor();
|
|
253
|
+
case "projects": return projects();
|
|
254
|
+
case "models": return models();
|
|
255
|
+
case "build": return build(argv);
|
|
256
|
+
case undefined: case "help": console.log(USAGE); return cmd ? 0 : 2;
|
|
257
|
+
default: console.error(`projectinator: unknown command "${cmd}".\n\n${USAGE}`); return 2;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (process.argv[1] && import.meta.url === new URL(process.argv[1], "file://").href) {
|
|
262
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
263
|
+
}
|