projectinator 0.3.0 → 0.4.0

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.
Files changed (170) hide show
  1. package/README.md +30 -11
  2. package/bin/projectinator.mjs +56 -11
  3. package/dist/bakeoff.js +163 -0
  4. package/dist/bakeoff.js.map +1 -0
  5. package/dist/build-state.js +26 -0
  6. package/dist/build-state.js.map +1 -0
  7. package/dist/burndown.js +19 -0
  8. package/dist/burndown.js.map +1 -0
  9. package/dist/calibration.js +86 -0
  10. package/dist/calibration.js.map +1 -0
  11. package/dist/cli.js +289 -0
  12. package/dist/cli.js.map +1 -0
  13. package/dist/cost.js +35 -0
  14. package/dist/cost.js.map +1 -0
  15. package/dist/council.js +147 -0
  16. package/dist/council.js.map +1 -0
  17. package/dist/demo.js +93 -0
  18. package/dist/demo.js.map +1 -0
  19. package/dist/estimate.js +98 -0
  20. package/dist/estimate.js.map +1 -0
  21. package/dist/executor.js +118 -0
  22. package/dist/executor.js.map +1 -0
  23. package/dist/git.js +82 -0
  24. package/dist/git.js.map +1 -0
  25. package/dist/intake.js +98 -0
  26. package/dist/intake.js.map +1 -0
  27. package/dist/models.js +171 -0
  28. package/dist/models.js.map +1 -0
  29. package/dist/narrate.js +76 -0
  30. package/dist/narrate.js.map +1 -0
  31. package/dist/openrouter.js +112 -0
  32. package/dist/openrouter.js.map +1 -0
  33. package/dist/orchestrator.js +273 -0
  34. package/dist/orchestrator.js.map +1 -0
  35. package/dist/pm.js +245 -0
  36. package/dist/pm.js.map +1 -0
  37. package/dist/preview.js +194 -0
  38. package/dist/preview.js.map +1 -0
  39. package/dist/registry-store.js +38 -0
  40. package/dist/registry-store.js.map +1 -0
  41. package/dist/registry.js +115 -0
  42. package/dist/registry.js.map +1 -0
  43. package/dist/research.js +98 -0
  44. package/dist/research.js.map +1 -0
  45. package/dist/retro.js +83 -0
  46. package/dist/retro.js.map +1 -0
  47. package/dist/roles.js +357 -0
  48. package/dist/roles.js.map +1 -0
  49. package/dist/router.js +95 -0
  50. package/dist/router.js.map +1 -0
  51. package/dist/run-bakeoff.js +68 -0
  52. package/dist/run-bakeoff.js.map +1 -0
  53. package/dist/run-build.js +193 -0
  54. package/dist/run-build.js.map +1 -0
  55. package/{src/run-dev.ts → dist/run-dev.js} +23 -39
  56. package/dist/run-dev.js.map +1 -0
  57. package/{src/run-pm.ts → dist/run-pm.js} +34 -46
  58. package/dist/run-pm.js.map +1 -0
  59. package/{src/run-research.ts → dist/run-research.js} +19 -30
  60. package/dist/run-research.js.map +1 -0
  61. package/{src/run-scout.ts → dist/run-scout.js} +17 -27
  62. package/dist/run-scout.js.map +1 -0
  63. package/dist/run-web.js +93 -0
  64. package/dist/run-web.js.map +1 -0
  65. package/dist/scout.js +81 -0
  66. package/dist/scout.js.map +1 -0
  67. package/dist/session-cost.js +15 -0
  68. package/dist/session-cost.js.map +1 -0
  69. package/dist/stack.js +34 -0
  70. package/dist/stack.js.map +1 -0
  71. package/dist/stuck.js +21 -0
  72. package/dist/stuck.js.map +1 -0
  73. package/dist/tui/App.js +1269 -0
  74. package/dist/tui/App.js.map +1 -0
  75. package/dist/tui/BakeOff.js +95 -0
  76. package/dist/tui/BakeOff.js.map +1 -0
  77. package/dist/tui/BoardEditor.js +183 -0
  78. package/dist/tui/BoardEditor.js.map +1 -0
  79. package/dist/tui/EditableBoard.js +140 -0
  80. package/dist/tui/EditableBoard.js.map +1 -0
  81. package/dist/tui/Frame.js +78 -0
  82. package/dist/tui/Frame.js.map +1 -0
  83. package/dist/tui/Intake.js +59 -0
  84. package/dist/tui/Intake.js.map +1 -0
  85. package/dist/tui/Kanban.js +75 -0
  86. package/dist/tui/Kanban.js.map +1 -0
  87. package/dist/tui/Settings.js +299 -0
  88. package/dist/tui/Settings.js.map +1 -0
  89. package/dist/tui/StackPick.js +44 -0
  90. package/dist/tui/StackPick.js.map +1 -0
  91. package/dist/tui/WebAccounts.js +131 -0
  92. package/dist/tui/WebAccounts.js.map +1 -0
  93. package/dist/tui/components.js +178 -0
  94. package/dist/tui/components.js.map +1 -0
  95. package/dist/tui/config.js +129 -0
  96. package/dist/tui/config.js.map +1 -0
  97. package/dist/tui/deploy.js +114 -0
  98. package/dist/tui/deploy.js.map +1 -0
  99. package/dist/tui/engine.js +710 -0
  100. package/dist/tui/engine.js.map +1 -0
  101. package/dist/tui/notify.js +47 -0
  102. package/dist/tui/notify.js.map +1 -0
  103. package/dist/tui/panels.js +42 -0
  104. package/dist/tui/panels.js.map +1 -0
  105. package/dist/tui/templates.js +106 -0
  106. package/dist/tui/templates.js.map +1 -0
  107. package/dist/tui/theme.js +42 -0
  108. package/dist/tui/theme.js.map +1 -0
  109. package/dist/tui/validate.js +51 -0
  110. package/dist/tui/validate.js.map +1 -0
  111. package/{src/tui.tsx → dist/tui.js} +23 -33
  112. package/dist/tui.js.map +1 -0
  113. package/dist/types.js +19 -0
  114. package/dist/types.js.map +1 -0
  115. package/dist/web/oauth-anthropic.js +179 -0
  116. package/dist/web/oauth-anthropic.js.map +1 -0
  117. package/dist/web/session.js +274 -0
  118. package/dist/web/session.js.map +1 -0
  119. package/package.json +5 -4
  120. package/src/bakeoff.ts +0 -214
  121. package/src/build-state.ts +0 -47
  122. package/src/burndown.ts +0 -35
  123. package/src/calibration.ts +0 -88
  124. package/src/cost.ts +0 -43
  125. package/src/council.ts +0 -175
  126. package/src/demo.ts +0 -106
  127. package/src/estimate.ts +0 -111
  128. package/src/executor.ts +0 -169
  129. package/src/git.ts +0 -72
  130. package/src/intake.ts +0 -125
  131. package/src/models.ts +0 -175
  132. package/src/narrate.ts +0 -87
  133. package/src/openrouter.ts +0 -119
  134. package/src/orchestrator.ts +0 -310
  135. package/src/pm.ts +0 -302
  136. package/src/preview.ts +0 -206
  137. package/src/registry-store.ts +0 -41
  138. package/src/registry.ts +0 -132
  139. package/src/research.ts +0 -123
  140. package/src/retro.ts +0 -99
  141. package/src/roles.ts +0 -401
  142. package/src/router.ts +0 -120
  143. package/src/run-bakeoff.ts +0 -77
  144. package/src/run-build.ts +0 -203
  145. package/src/run-web.ts +0 -87
  146. package/src/scout.ts +0 -121
  147. package/src/session-cost.ts +0 -17
  148. package/src/stack.ts +0 -46
  149. package/src/tui/App.tsx +0 -1760
  150. package/src/tui/BakeOff.tsx +0 -190
  151. package/src/tui/BoardEditor.tsx +0 -260
  152. package/src/tui/EditableBoard.tsx +0 -179
  153. package/src/tui/Frame.tsx +0 -142
  154. package/src/tui/Intake.tsx +0 -111
  155. package/src/tui/Kanban.tsx +0 -158
  156. package/src/tui/Settings.tsx +0 -513
  157. package/src/tui/StackPick.tsx +0 -79
  158. package/src/tui/WebAccounts.tsx +0 -197
  159. package/src/tui/components.tsx +0 -340
  160. package/src/tui/config.ts +0 -150
  161. package/src/tui/deploy.ts +0 -137
  162. package/src/tui/engine.ts +0 -749
  163. package/src/tui/notify.ts +0 -21
  164. package/src/tui/panels.tsx +0 -89
  165. package/src/tui/templates.ts +0 -119
  166. package/src/tui/theme.ts +0 -44
  167. package/src/tui/validate.ts +0 -53
  168. package/src/types.ts +0 -208
  169. package/src/web/oauth-anthropic.ts +0 -206
  170. package/src/web/session.ts +0 -299
@@ -1,513 +0,0 @@
1
- // Settings — API keys, model-per-role assignments, preferences, web-login (soon).
2
- // Self-contained: manages its own sub-navigation; calls onExit when done.
3
-
4
- import React, { useState } from "react";
5
- import { Box, Text } from "ink";
6
- import { Spinner, StatusMessage } from "@inkjs/ui";
7
- import type { Capability, Provider, Tier } from "../types.js";
8
- import { C, Panel, Menu as SelectInput, GroupedMenu, KeyHint, TextField as TextInput, Password, type MenuGroup } from "./components.js";
9
- import { WebAccounts } from "./WebAccounts.js";
10
- import { connectedProviders } from "../web/session.js";
11
- import { estimateAccuracy } from "../estimate.js";
12
- import { availableProviders, effectiveRoster, allModels, setRoleModel, PROVIDER_LABEL } from "./engine.js";
13
- import { setKey, getPrefs, setPrefs, loadConfig, setPreferredProvider, getDefaultMode, setDefaultMode, getNotify, setNotify, getPreferredStack, setPreferredStack, ENV_VAR, type Prefs } from "./config.js";
14
- import { validateKey } from "./validate.js";
15
- import { openRouterModels, refreshOpenRouterModels } from "../openrouter.js";
16
-
17
- type Sub = "menu" | "keys" | "keyEntry" | "models" | "modelPick" | "orBrowse" | "orPick" | "prefs" | "provider" | "workflow" | "weblogin" | "accuracy" | "stack";
18
-
19
- export function Settings({ onExit }: { onExit: () => void }): React.ReactElement {
20
- const [sub, setSub] = useState<Sub>("menu");
21
- const [keyProvider, setKeyProvider] = useState<Provider>("anthropic");
22
- const [keyDraft, setKeyDraft] = useState("");
23
- const [checking, setChecking] = useState(false);
24
- const [keyError, setKeyError] = useState("");
25
- const [role, setRole] = useState<{ capability: Capability; tier: Tier; label: string } | null>(null);
26
- const [notice, setNotice] = useState("");
27
- const [orQuery, setOrQuery] = useState(""); // OpenRouter model-browser filter
28
- const [, force] = useState(0);
29
- const refresh = () => force((n) => n + 1);
30
-
31
- // ---------- menu ----------
32
- if (sub === "menu") {
33
- const groups: MenuGroup[] = [
34
- { title: "Models & providers", items: [
35
- { label: "API keys", value: "keys" },
36
- { label: "Preferred provider", value: "provider" },
37
- { label: "Model assignments", value: "models" },
38
- { label: "Estimate accuracy", value: "accuracy" },
39
- ] },
40
- { title: "Build defaults", items: [
41
- { label: "Default workflow", value: "workflow" },
42
- { label: `Default stack: ${getPreferredStack()}`, value: "stack" },
43
- { label: "Budget, speed & alerts", value: "prefs" },
44
- { label: `Notify on done: ${getNotify() ? "On" : "Off"}`, value: "notify" },
45
- ] },
46
- // Web-login (browser automation / OAuth) is parked — vendors closed
47
- // third-party subscription auth in 2026. Hidden unless PROJECTINATOR_WEB=1.
48
- ...(process.env.PROJECTINATOR_WEB === "1"
49
- ? [{ title: "Experimental", items: [{ label: `Connect accounts${connectedProviders().length ? ` (${connectedProviders().length})` : ""}`, value: "weblogin" }] }]
50
- : []),
51
- { title: "", items: [{ label: "Back", value: "back" }] },
52
- ];
53
- return (
54
- <Box flexDirection="column">
55
- {notice ? <Box marginBottom={1}><StatusMessage variant="success">{notice}</StatusMessage></Box> : null}
56
- <Panel title="Settings">
57
- <GroupedMenu
58
- groups={groups}
59
- onSelect={(i) => {
60
- setNotice("");
61
- if (i.value === "back") onExit();
62
- else if (i.value === "notify") {
63
- const next = !getNotify();
64
- setNotify(next);
65
- setNotice(`Notifications ${next ? "on" : "off"}.`);
66
- } else setSub(i.value as Sub);
67
- }}
68
- />
69
- </Panel>
70
- </Box>
71
- );
72
- }
73
-
74
- // ---------- API keys ----------
75
- if (sub === "keys") {
76
- const have = new Set(availableProviders());
77
- const providers: Provider[] = ["anthropic", "openai", "google", "openrouter"];
78
- return (
79
- <Box flexDirection="column">
80
- <Panel title="API keys">
81
- <Text color={C.textMuted}>Select a provider to add or replace its key. Saved to ~/.projectinator (0600).</Text>
82
- <Box marginTop={1}>
83
- <SelectInput
84
- items={[
85
- ...providers.map((p) => ({
86
- label: `${have.has(p) ? "✓" : "·"} ${PROVIDER_LABEL[p]} ${have.has(p) ? "(set)" : "(not set)"}`,
87
- value: p,
88
- })),
89
- { label: "Back", value: "__back" },
90
- ]}
91
- onSelect={(i) => {
92
- if (i.value === "__back") setSub("menu");
93
- else {
94
- setKeyProvider(i.value as Provider);
95
- setKeyDraft("");
96
- setKeyError("");
97
- setChecking(false);
98
- setSub("keyEntry");
99
- }
100
- }}
101
- />
102
- </Box>
103
- </Panel>
104
- </Box>
105
- );
106
- }
107
-
108
- if (sub === "keyEntry") {
109
- if (checking) {
110
- return (
111
- <Box flexDirection="column">
112
- <Panel title={`Enter key for ${PROVIDER_LABEL[keyProvider]}`}>
113
- <Spinner label={`Verifying key with ${PROVIDER_LABEL[keyProvider]}…`} />
114
- </Panel>
115
- </Box>
116
- );
117
- }
118
- return (
119
- <Box flexDirection="column">
120
- <Panel title={`Enter key for ${PROVIDER_LABEL[keyProvider]}`}>
121
- <Text color={C.textMuted}>Sets {ENV_VAR[keyProvider]}. Paste and press Enter — it's verified before saving. (hidden)</Text>
122
- {keyError ? <Box marginTop={1}><StatusMessage variant="error">{keyError}</StatusMessage></Box> : null}
123
- <Box marginTop={1}>
124
- <Password
125
- placeholder="paste your key…"
126
- onSubmit={(k) => {
127
- const trimmed = k.trim();
128
- if (!trimmed) {
129
- setSub("menu");
130
- return;
131
- }
132
- setKeyError("");
133
- setChecking(true);
134
- void validateKey(keyProvider, trimmed).then((res) => {
135
- setChecking(false);
136
- if (res.ok) {
137
- setKey(keyProvider, trimmed);
138
- setNotice(`Saved ${PROVIDER_LABEL[keyProvider]} key (verified ✓).`);
139
- setSub("menu");
140
- } else {
141
- setKeyError(res.error ?? "Key rejected.");
142
- }
143
- });
144
- }}
145
- />
146
- </Box>
147
- </Panel>
148
- </Box>
149
- );
150
- }
151
-
152
- // ---------- model assignments ----------
153
- if (sub === "models") {
154
- const rows = effectiveRoster();
155
- const lock = loadConfig().preferredProvider;
156
- return (
157
- <Box flexDirection="column">
158
- <Panel title="Model assignments">
159
- <Text color={C.textMuted}>Which model plays each role. {lock ? `Pinned to ${PROVIDER_LABEL[lock]} — pick from its models.` : "Saved as overrides."}</Text>
160
- <Box marginTop={1}>
161
- <SelectInput
162
- items={[
163
- ...rows.map((r) => ({
164
- label: `${r.label.padEnd(16)} ${r.model ?? "—"}`,
165
- value: `${r.capability}:${r.tier}`,
166
- })),
167
- { label: "Back", value: "__back" },
168
- ]}
169
- onSelect={(i) => {
170
- if (i.value === "__back") setSub("menu");
171
- else {
172
- const [capability, tier] = i.value.split(":") as [Capability, Tier];
173
- const r = rows.find((x) => x.capability === capability && x.tier === tier)!;
174
- setRole({ capability, tier, label: r.label });
175
- if (lock === "openrouter") {
176
- setOrQuery("");
177
- void refreshOpenRouterModels().then(() => refresh()); // freshen catalog in the background
178
- setSub("orBrowse");
179
- } else setSub("modelPick");
180
- }
181
- }}
182
- />
183
- </Box>
184
- </Panel>
185
- </Box>
186
- );
187
- }
188
-
189
- if (sub === "modelPick" && role) {
190
- const lock = loadConfig().preferredProvider;
191
- const models = lock ? allModels().filter((m) => m.provider === lock) : allModels();
192
- return (
193
- <Box flexDirection="column">
194
- <Panel title={`Pick a model for ${role.label}`}>
195
- {lock ? <Text color={C.textMuted}>Showing {PROVIDER_LABEL[lock]} models (you've pinned this provider).</Text> : null}
196
- <Box marginTop={lock ? 1 : 0}>
197
- <SelectInput
198
- limit={10}
199
- items={[
200
- ...models.map((m) => ({ label: `${m.name} (${m.provider})`, value: m.id })),
201
- { label: "Back", value: "__back" },
202
- ]}
203
- onSelect={(i) => {
204
- if (i.value !== "__back") {
205
- setRoleModel(role.capability, role.tier, i.value);
206
- setNotice(`${role.label} → ${i.value}`);
207
- refresh();
208
- }
209
- setSub("models");
210
- }}
211
- />
212
- </Box>
213
- </Panel>
214
- </Box>
215
- );
216
- }
217
-
218
- // ---------- OpenRouter model browser: filter the whole catalog by name ----------
219
- if ((sub === "orBrowse" || sub === "orPick") && role) {
220
- const all = openRouterModels();
221
- const q = orQuery.trim().toLowerCase();
222
- const matches = q ? all.filter((m) => m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q)) : all;
223
- const price = (m: (typeof all)[number]) => `$${m.cost.input}/$${m.cost.output}`;
224
-
225
- if (sub === "orBrowse") {
226
- return (
227
- <Box flexDirection="column">
228
- <Panel title={`OpenRouter model for ${role.label}`}>
229
- <Text color={C.textMuted}>Type to filter {all.length} models by name or slug — e.g. “kimi”, “deepseek”, “coder”.</Text>
230
- <Box marginTop={1}>
231
- <Text color={C.accent}>{"filter › "}</Text>
232
- <TextInput
233
- value={orQuery}
234
- onChange={setOrQuery}
235
- placeholder="kimi"
236
- onSubmit={() => {
237
- if (!orQuery.trim()) { setSub("models"); return; }
238
- if (matches.length) setSub("orPick");
239
- }}
240
- />
241
- </Box>
242
- <Box marginTop={1} flexDirection="column">
243
- <Text color={C.textSubtle}>{matches.length} match{matches.length === 1 ? "" : "es"}{matches.length ? " — Enter to choose:" : ""}</Text>
244
- {matches.slice(0, 6).map((m) => (
245
- <Text key={m.id} wrap="truncate-end"><Text color={C.textMuted}>{m.name}</Text> <Text color={C.textSubtle}>{m.id} {price(m)}</Text></Text>
246
- ))}
247
- {matches.length > 6 ? <Text color={C.textSubtle}> …refine to narrow</Text> : null}
248
- </Box>
249
- <Box marginTop={1}><KeyHint hints={[{ keys: "Enter", label: "choose" }, { keys: "empty Enter", label: "back" }]} /></Box>
250
- </Panel>
251
- </Box>
252
- );
253
- }
254
-
255
- // orPick — select from the filtered matches
256
- return (
257
- <Box flexDirection="column">
258
- <Panel title={`Pick a model · “${orQuery}” (${matches.length})`}>
259
- <SelectInput
260
- limit={12}
261
- items={[
262
- ...matches.slice(0, 50).map((m) => ({ label: `${m.name} ${m.id} ${price(m)}`, value: m.id })),
263
- { label: "← Refine filter", value: "__refine" },
264
- { label: "Back", value: "__back" },
265
- ]}
266
- onSelect={(i) => {
267
- if (i.value === "__refine") { setSub("orBrowse"); return; }
268
- if (i.value === "__back") { setSub("models"); return; }
269
- setRoleModel(role.capability, role.tier, i.value);
270
- setNotice(`${role.label} → ${i.value}`);
271
- refresh();
272
- setSub("models");
273
- }}
274
- />
275
- </Panel>
276
- </Box>
277
- );
278
- }
279
-
280
- // ---------- preferred provider ----------
281
- if (sub === "provider") {
282
- const have = new Set(availableProviders());
283
- const current = loadConfig().preferredProvider;
284
- const providers: Provider[] = ["anthropic", "openai", "google", "openrouter"];
285
- return (
286
- <Box flexDirection="column">
287
- <Panel title="Preferred provider">
288
- <Text color={C.textMuted}>Pin one provider for every role, or Auto to use the best available. Current: {current ?? "Auto"}.</Text>
289
- <Box marginTop={1}>
290
- <SelectInput
291
- items={[
292
- { label: `Auto (best available)${!current ? " ✓" : ""}`, value: "__auto" },
293
- ...providers.map((p) => ({
294
- label: `${PROVIDER_LABEL[p]}${have.has(p) ? "" : " (no key)"}${current === p ? " ✓" : ""}`,
295
- value: p,
296
- })),
297
- { label: "Back", value: "__back" },
298
- ]}
299
- onSelect={(i) => {
300
- if (i.value === "__back") {
301
- setSub("menu");
302
- return;
303
- }
304
- const choice = i.value === "__auto" ? undefined : (i.value as Provider);
305
- setPreferredProvider(choice);
306
- setNotice(`Preferred provider: ${choice ?? "Auto"}.`);
307
- setSub("menu");
308
- }}
309
- />
310
- </Box>
311
- </Panel>
312
- </Box>
313
- );
314
- }
315
-
316
- // ---------- default workflow ----------
317
- if (sub === "workflow") {
318
- const current = getDefaultMode();
319
- return (
320
- <Box flexDirection="column">
321
- <Panel title="Default workflow for new builds">
322
- <Text color={C.textMuted}>Current: {current === "approval" ? "Approval-gated" : "Auto-run"}.</Text>
323
- <Box marginTop={1}>
324
- <SelectInput
325
- items={[
326
- { label: `Auto-run — confirm cost, then build${current === "auto" ? " ✓" : ""}`, value: "auto" },
327
- { label: `Approval-gated — you approve the backlog first${current === "approval" ? " ✓" : ""}`, value: "approval" },
328
- { label: "Back", value: "__back" },
329
- ]}
330
- onSelect={(i) => {
331
- if (i.value !== "__back") {
332
- setDefaultMode(i.value as "auto" | "approval");
333
- setNotice(`Default workflow: ${i.value === "approval" ? "Approval-gated" : "Auto-run"}.`);
334
- }
335
- setSub("menu");
336
- }}
337
- />
338
- </Box>
339
- </Panel>
340
- </Box>
341
- );
342
- }
343
-
344
- // ---------- preferences ----------
345
- if (sub === "prefs") {
346
- const prefs = getPrefs();
347
- return <PrefsEditor initial={prefs} onDone={(p) => { setPrefs(p); setNotice("Preferences saved."); setSub("menu"); }} onCancel={() => setSub("menu")} />;
348
- }
349
-
350
- // ---------- estimate accuracy (calibration vs baseline) ----------
351
- if (sub === "accuracy") {
352
- const rows = estimateAccuracy();
353
- return (
354
- <Box flexDirection="column">
355
- <Panel title="Estimate accuracy">
356
- <Text color={C.textMuted}>Measured output tokens vs the static baseline, per role/difficulty. Self-calibration</Text>
357
- <Text color={C.textMuted}>replaces the baseline once a bucket has ≥2 samples (✓ active).</Text>
358
- <Box marginTop={1} flexDirection="column">
359
- {rows.length === 0 ? (
360
- <Text color={C.dim}>No data yet — run some builds and this fills in.</Text>
361
- ) : (
362
- <>
363
- <Text color={C.dim}>{"role/diff".padEnd(16)}{"base".padEnd(8)}{"actual".padEnd(8)}{"Δ".padEnd(8)}{"n".padEnd(4)}live</Text>
364
- {rows.map((r) => {
365
- const delta = r.baseOutput > 0 ? Math.round(((r.actualOutput - r.baseOutput) / r.baseOutput) * 100) : 0;
366
- return (
367
- <Text key={`${r.capability}/${r.difficulty}`}>
368
- {`${r.capability}/${r.difficulty}`.padEnd(16)}
369
- {String(r.baseOutput).padEnd(8)}
370
- <Text color={C.accent}>{String(r.actualOutput).padEnd(8)}</Text>
371
- <Text color={Math.abs(delta) > 40 ? C.warn : C.dim}>{`${delta >= 0 ? "+" : ""}${delta}%`.padEnd(8)}</Text>
372
- {String(r.n).padEnd(4)}
373
- {r.active ? <Text color={C.good}>✓</Text> : <Text color={C.dim}>·</Text>}
374
- </Text>
375
- );
376
- })}
377
- </>
378
- )}
379
- </Box>
380
- <Box marginTop={1}>
381
- <SelectInput items={[{ label: "Back", value: "back" }]} onSelect={() => setSub("menu")} />
382
- </Box>
383
- </Panel>
384
- </Box>
385
- );
386
- }
387
-
388
- // ---------- default stack ----------
389
- if (sub === "stack") {
390
- const current = getPreferredStack();
391
- return (
392
- <Box flexDirection="column">
393
- <Panel title="Default stack for new web builds">
394
- <Text color={C.textMuted}>“Ask each time” shows the picker; anything else skips it. Current: {current}.</Text>
395
- <Box marginTop={1}>
396
- <SelectInput
397
- items={[
398
- { label: `Ask each time${current === "ask" ? " ✓" : ""}`, value: "ask" },
399
- { label: `Vanilla HTML/CSS/JS${current === "vanilla" ? " ✓" : ""}`, value: "vanilla" },
400
- { label: `React (CDN, no build)${current === "react" ? " ✓" : ""}`, value: "react" },
401
- { label: `Let the AI decide${current === "ai" ? " ✓" : ""}`, value: "ai" },
402
- { label: "Back", value: "__back" },
403
- ]}
404
- onSelect={(i) => {
405
- if (i.value !== "__back") {
406
- setPreferredStack(i.value as "ask" | "vanilla" | "react" | "ai");
407
- setNotice(`Default stack: ${i.value}.`);
408
- }
409
- setSub("menu");
410
- }}
411
- />
412
- </Box>
413
- </Panel>
414
- </Box>
415
- );
416
- }
417
-
418
- // ---------- connect accounts (web subscriptions) ----------
419
- if (sub === "weblogin") {
420
- return <WebAccounts onExit={() => setSub("menu")} />;
421
- }
422
-
423
- return <Text>…</Text>;
424
- }
425
-
426
- function PrefsEditor({
427
- initial,
428
- onDone,
429
- onCancel,
430
- }: {
431
- initial: Prefs;
432
- onDone: (p: Prefs) => void;
433
- onCancel: () => void;
434
- }): React.ReactElement {
435
- const [cap, setCap] = useState(String(initial.budgetCapUSD));
436
- const [conc, setConc] = useState(String(initial.concurrency));
437
- const [pct, setPct] = useState(String(initial.budgetAlertPct));
438
- const [tmo, setTmo] = useState(String(initial.taskTimeoutMin));
439
- const [tcap, setTcap] = useState(String(initial.taskCostCapUSD));
440
- const [field, setField] = useState<"cap" | "conc" | "pct" | "tmo" | "tcap">("cap");
441
-
442
- const commit = () => {
443
- const b = Math.max(1, parseFloat(cap) || initial.budgetCapUSD);
444
- const c = Math.max(1, Math.floor(parseFloat(conc) || initial.concurrency));
445
- const p = Math.min(99, Math.max(1, Math.round(parseFloat(pct) || initial.budgetAlertPct)));
446
- // "0" is a valid value here (unlimited), so only a non-number falls back.
447
- const parse0 = (s: string, fallback: number) => { const n = parseFloat(s); return Number.isFinite(n) ? Math.max(0, n) : fallback; };
448
- onDone({
449
- budgetCapUSD: b,
450
- concurrency: c,
451
- budgetAlertPct: p,
452
- taskTimeoutMin: parse0(tmo, initial.taskTimeoutMin),
453
- taskCostCapUSD: parse0(tcap, initial.taskCostCapUSD),
454
- });
455
- };
456
-
457
- return (
458
- <Box flexDirection="column">
459
- <Panel title="Budget, speed, alerts & task limits">
460
- <Box flexDirection="column">
461
- <Box>
462
- <Box width={22}><Text color={field === "cap" ? C.accent : C.text}>Budget cap (USD)</Text></Box>
463
- {field === "cap" ? (
464
- <TextInput value={cap} onChange={setCap} onSubmit={() => setField("conc")} />
465
- ) : (
466
- <Text>{cap}</Text>
467
- )}
468
- </Box>
469
- <Box>
470
- <Box width={22}><Text color={field === "conc" ? C.accent : C.text}>Tasks at once</Text></Box>
471
- {field === "conc" ? (
472
- <TextInput
473
- value={conc}
474
- onChange={setConc}
475
- onSubmit={() => setField("pct")}
476
- />
477
- ) : (
478
- <Text>{conc}</Text>
479
- )}
480
- </Box>
481
- <Box>
482
- <Box width={22}><Text color={field === "pct" ? C.accent : C.text}>Alert at % of cap</Text></Box>
483
- {field === "pct" ? (
484
- <TextInput value={pct} onChange={setPct} onSubmit={() => setField("tmo")} />
485
- ) : (
486
- <Text>{pct}%</Text>
487
- )}
488
- </Box>
489
- <Box>
490
- <Box width={22}><Text color={field === "tmo" ? C.accent : C.text}>Task timeout (min)</Text></Box>
491
- {field === "tmo" ? (
492
- <TextInput value={tmo} onChange={setTmo} onSubmit={() => setField("tcap")} />
493
- ) : (
494
- <Text>{tmo === "0" ? "unlimited" : tmo}</Text>
495
- )}
496
- </Box>
497
- <Box>
498
- <Box width={22}><Text color={field === "tcap" ? C.accent : C.text}>Task cost cap (USD)</Text></Box>
499
- {field === "tcap" ? (
500
- <TextInput value={tcap} onChange={setTcap} onSubmit={commit} />
501
- ) : (
502
- <Text>{tcap === "0" ? "unlimited" : tcap}</Text>
503
- )}
504
- </Box>
505
- </Box>
506
- <Box flexDirection="column" marginTop={1}>
507
- <Text color={C.textSubtle}>Enter moves to the next field, then saves. A task over its limit is aborted and the build halts (resumable); 0 = unlimited.</Text>
508
- <KeyHint hints={[{ keys: "Enter", label: "next / save" }, { keys: "Ctrl+C", label: "cancel" }]} />
509
- </Box>
510
- </Panel>
511
- </Box>
512
- );
513
- }
@@ -1,79 +0,0 @@
1
- // Stack picker — platform first, then (for web) the framework. Non-web platforms
2
- // fall back to a web build for now. Returns the chosen StackChoice.
3
-
4
- import React, { useState } from "react";
5
- import { Box, Text } from "ink";
6
- import { C, Panel, Menu as SelectInput, KeyHint, TextField as TextInput } from "./components.js";
7
- import { WEB_FRAMEWORKS, type Platform, type StackChoice } from "../stack.js";
8
-
9
- const OTHER = "__other__";
10
-
11
- export function StackPick({ onDone }: { onDone: (choice: StackChoice) => void }): React.ReactElement {
12
- const [platform, setPlatform] = useState<Platform | null>(null);
13
- const [typing, setTyping] = useState(false);
14
- const [draft, setDraft] = useState("");
15
-
16
- // step 1 — platform
17
- if (!platform) {
18
- return (
19
- <Box flexDirection="column">
20
- <Panel title="Target platform">
21
- <SelectInput
22
- items={[
23
- { label: "Web", value: "web" },
24
- { label: "Mobile (builds as a web app for now)", value: "mobile" },
25
- { label: "Desktop (builds as a web app for now)", value: "desktop" },
26
- ]}
27
- onSelect={(i) => {
28
- const p = i.value as Platform;
29
- if (p === "web") setPlatform("web");
30
- else onDone({ platform: p, framework: "ai" }); // web fallback, let PM decide
31
- }}
32
- />
33
- </Panel>
34
- <KeyHint hints={[{ keys: "Esc", label: "cancel the build" }]} />
35
- </Box>
36
- );
37
- }
38
-
39
- // step 2 — framework (web only)
40
- if (typing) {
41
- return (
42
- <Box flexDirection="column">
43
- <Text bold>Which framework?</Text>
44
- <Box marginTop={1}>
45
- <Text color={C.accent}>{"› "}</Text>
46
- <TextInput
47
- value={draft}
48
- onChange={setDraft}
49
- onSubmit={() => onDone({ platform: "web", framework: draft.trim() || "ai" })}
50
- />
51
- </Box>
52
- <Box flexDirection="column" marginTop={1}>
53
- <Text color={C.dim}>Name it — must run with no build step.</Text>
54
- <KeyHint hints={[{ keys: "Enter", label: "continue" }]} />
55
- </Box>
56
- </Box>
57
- );
58
- }
59
-
60
- return (
61
- <Box flexDirection="column">
62
- <Panel title="Web framework">
63
- <SelectInput
64
- items={[
65
- ...WEB_FRAMEWORKS.map((f) => ({ label: f.label, value: String(f.id) })),
66
- { label: "Something else…", value: OTHER },
67
- { label: "Platform", value: "__platform" },
68
- ]}
69
- onSelect={(i) => {
70
- if (i.value === "__platform") setPlatform(null);
71
- else if (i.value === OTHER) setTyping(true);
72
- else onDone({ platform: "web", framework: i.value });
73
- }}
74
- />
75
- </Panel>
76
- <Text color={C.dim}>Esc cancels the build.</Text>
77
- </Box>
78
- );
79
- }