projectinator 0.3.1 → 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 (171) hide show
  1. package/README.md +7 -6
  2. package/bin/projectinator.mjs +22 -15
  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/cli.ts +0 -263
  125. package/src/cost.ts +0 -43
  126. package/src/council.ts +0 -175
  127. package/src/demo.ts +0 -106
  128. package/src/estimate.ts +0 -111
  129. package/src/executor.ts +0 -169
  130. package/src/git.ts +0 -72
  131. package/src/intake.ts +0 -125
  132. package/src/models.ts +0 -175
  133. package/src/narrate.ts +0 -87
  134. package/src/openrouter.ts +0 -119
  135. package/src/orchestrator.ts +0 -310
  136. package/src/pm.ts +0 -302
  137. package/src/preview.ts +0 -206
  138. package/src/registry-store.ts +0 -41
  139. package/src/registry.ts +0 -132
  140. package/src/research.ts +0 -123
  141. package/src/retro.ts +0 -99
  142. package/src/roles.ts +0 -401
  143. package/src/router.ts +0 -120
  144. package/src/run-bakeoff.ts +0 -77
  145. package/src/run-build.ts +0 -203
  146. package/src/run-web.ts +0 -87
  147. package/src/scout.ts +0 -121
  148. package/src/session-cost.ts +0 -17
  149. package/src/stack.ts +0 -46
  150. package/src/tui/App.tsx +0 -1760
  151. package/src/tui/BakeOff.tsx +0 -190
  152. package/src/tui/BoardEditor.tsx +0 -260
  153. package/src/tui/EditableBoard.tsx +0 -179
  154. package/src/tui/Frame.tsx +0 -142
  155. package/src/tui/Intake.tsx +0 -111
  156. package/src/tui/Kanban.tsx +0 -158
  157. package/src/tui/Settings.tsx +0 -513
  158. package/src/tui/StackPick.tsx +0 -79
  159. package/src/tui/WebAccounts.tsx +0 -197
  160. package/src/tui/components.tsx +0 -340
  161. package/src/tui/config.ts +0 -150
  162. package/src/tui/deploy.ts +0 -137
  163. package/src/tui/engine.ts +0 -749
  164. package/src/tui/notify.ts +0 -21
  165. package/src/tui/panels.tsx +0 -89
  166. package/src/tui/templates.ts +0 -119
  167. package/src/tui/theme.ts +0 -44
  168. package/src/tui/validate.ts +0 -53
  169. package/src/types.ts +0 -208
  170. package/src/web/oauth-anthropic.ts +0 -206
  171. 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
- }