quoxcli 0.0.0-stage → 0.2.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 (348) hide show
  1. package/LICENSE +35 -0
  2. package/README.md +142 -2
  3. package/bin/quox.js +292 -0
  4. package/bin/quoxon.js +8 -0
  5. package/lib/api/endpoints/admin.js +125 -0
  6. package/lib/api/endpoints/aee.js +38 -0
  7. package/lib/api/endpoints/agent-attach.js +126 -0
  8. package/lib/api/endpoints/agents.js +56 -0
  9. package/lib/api/endpoints/assistants.js +50 -0
  10. package/lib/api/endpoints/audit.js +14 -0
  11. package/lib/api/endpoints/auth.js +50 -0
  12. package/lib/api/endpoints/awareness.js +34 -0
  13. package/lib/api/endpoints/backup-schedules.js +20 -0
  14. package/lib/api/endpoints/backups.js +86 -0
  15. package/lib/api/endpoints/brain.js +140 -0
  16. package/lib/api/endpoints/budgets.js +62 -0
  17. package/lib/api/endpoints/calendarq.js +71 -0
  18. package/lib/api/endpoints/catalog.js +14 -0
  19. package/lib/api/endpoints/chat.js +54 -0
  20. package/lib/api/endpoints/consent.js +26 -0
  21. package/lib/api/endpoints/contactsq.js +66 -0
  22. package/lib/api/endpoints/conversations.js +32 -0
  23. package/lib/api/endpoints/custom-roles.js +92 -0
  24. package/lib/api/endpoints/divisions.js +56 -0
  25. package/lib/api/endpoints/domains.js +38 -0
  26. package/lib/api/endpoints/engagement.js +231 -0
  27. package/lib/api/endpoints/engine.js +11 -0
  28. package/lib/api/endpoints/entity.js +50 -0
  29. package/lib/api/endpoints/executions.js +32 -0
  30. package/lib/api/endpoints/file.js +20 -0
  31. package/lib/api/endpoints/files.js +26 -0
  32. package/lib/api/endpoints/github.js +21 -0
  33. package/lib/api/endpoints/governance.js +43 -0
  34. package/lib/api/endpoints/hat.js +36 -0
  35. package/lib/api/endpoints/inbox.js +32 -0
  36. package/lib/api/endpoints/inboxq.js +120 -0
  37. package/lib/api/endpoints/index.js +228 -0
  38. package/lib/api/endpoints/invites.js +19 -0
  39. package/lib/api/endpoints/llm-policy.js +43 -0
  40. package/lib/api/endpoints/lucid.js +89 -0
  41. package/lib/api/endpoints/members.js +32 -0
  42. package/lib/api/endpoints/memory.js +75 -0
  43. package/lib/api/endpoints/mfa.js +32 -0
  44. package/lib/api/endpoints/mon.js +178 -0
  45. package/lib/api/endpoints/notesq.js +43 -0
  46. package/lib/api/endpoints/objectives.js +62 -0
  47. package/lib/api/endpoints/orgs.js +38 -0
  48. package/lib/api/endpoints/plugins.js +134 -0
  49. package/lib/api/endpoints/powers.js +75 -0
  50. package/lib/api/endpoints/prompts.js +69 -0
  51. package/lib/api/endpoints/qlarity.js +122 -0
  52. package/lib/api/endpoints/quoxbox.js +55 -0
  53. package/lib/api/endpoints/quoxchat.js +54 -0
  54. package/lib/api/endpoints/quoxflow.js +32 -0
  55. package/lib/api/endpoints/quoxplan.js +114 -0
  56. package/lib/api/endpoints/quoxseo.js +108 -0
  57. package/lib/api/endpoints/reach.js +47 -0
  58. package/lib/api/endpoints/runs.js +38 -0
  59. package/lib/api/endpoints/safety.js +46 -0
  60. package/lib/api/endpoints/scim.js +46 -0
  61. package/lib/api/endpoints/scopes.js +14 -0
  62. package/lib/api/endpoints/security.js +69 -0
  63. package/lib/api/endpoints/setup.js +90 -0
  64. package/lib/api/endpoints/skills.js +180 -0
  65. package/lib/api/endpoints/sso.js +67 -0
  66. package/lib/api/endpoints/tags.js +32 -0
  67. package/lib/api/endpoints/teams.js +56 -0
  68. package/lib/api/endpoints/telephony.js +46 -0
  69. package/lib/api/endpoints/tg.js +56 -0
  70. package/lib/api/endpoints/tools.js +50 -0
  71. package/lib/api/endpoints/traces.js +157 -0
  72. package/lib/api/endpoints/triggers.js +70 -0
  73. package/lib/api/endpoints/user-library.js +41 -0
  74. package/lib/api/endpoints/users.js +20 -0
  75. package/lib/api/endpoints/v1.js +499 -0
  76. package/lib/api/endpoints/vault.js +192 -0
  77. package/lib/api/endpoints/voice.js +22 -0
  78. package/lib/api/endpoints/volt.js +84 -0
  79. package/lib/api/endpoints/ward.js +40 -0
  80. package/lib/api/endpoints/webhooks.js +46 -0
  81. package/lib/api/endpoints/workflows.js +162 -0
  82. package/lib/api/endpoints/x.js +24 -0
  83. package/lib/api/endpoints.js +15 -0
  84. package/lib/commands/admin.js +359 -0
  85. package/lib/commands/aee.js +99 -0
  86. package/lib/commands/agent.js +201 -0
  87. package/lib/commands/agentAttach.js +1440 -0
  88. package/lib/commands/approvals.js +47 -0
  89. package/lib/commands/assistant.js +114 -0
  90. package/lib/commands/auth.js +458 -0
  91. package/lib/commands/awareness.js +88 -0
  92. package/lib/commands/backup.js +373 -0
  93. package/lib/commands/brain.js +994 -0
  94. package/lib/commands/budget.js +241 -0
  95. package/lib/commands/build.js +156 -0
  96. package/lib/commands/calendar.js +216 -0
  97. package/lib/commands/capabilities.js +34 -0
  98. package/lib/commands/chat.js +240 -0
  99. package/lib/commands/claude.js +354 -0
  100. package/lib/commands/config-cmd.js +106 -0
  101. package/lib/commands/consent.js +84 -0
  102. package/lib/commands/contacts.js +967 -0
  103. package/lib/commands/contactsSelftest.js +132 -0
  104. package/lib/commands/conversation.js +74 -0
  105. package/lib/commands/department.js +75 -0
  106. package/lib/commands/dev.js +620 -0
  107. package/lib/commands/devResident.js +314 -0
  108. package/lib/commands/devSupervisor.js +1050 -0
  109. package/lib/commands/discord.js +96 -0
  110. package/lib/commands/doctor.js +181 -0
  111. package/lib/commands/domain.js +114 -0
  112. package/lib/commands/done.js +277 -0
  113. package/lib/commands/engagement.js +586 -0
  114. package/lib/commands/entity.js +119 -0
  115. package/lib/commands/facebook.js +212 -0
  116. package/lib/commands/file.js +128 -0
  117. package/lib/commands/fleet.js +882 -0
  118. package/lib/commands/governance.js +638 -0
  119. package/lib/commands/gripe.js +324 -0
  120. package/lib/commands/hat.js +205 -0
  121. package/lib/commands/helpers.js +79 -0
  122. package/lib/commands/inbox.js +122 -0
  123. package/lib/commands/instagram.js +264 -0
  124. package/lib/commands/integration.js +100 -0
  125. package/lib/commands/inventory.js +115 -0
  126. package/lib/commands/invites.js +45 -0
  127. package/lib/commands/licence.js +280 -0
  128. package/lib/commands/linkedin.js +178 -0
  129. package/lib/commands/llm-policy.js +117 -0
  130. package/lib/commands/llm.js +45 -0
  131. package/lib/commands/lucid.js +591 -0
  132. package/lib/commands/mail.js +531 -0
  133. package/lib/commands/memory.js +284 -0
  134. package/lib/commands/mon.js +514 -0
  135. package/lib/commands/monitoring.js +86 -0
  136. package/lib/commands/motion.js +22 -0
  137. package/lib/commands/notes.js +1246 -0
  138. package/lib/commands/notesBatch.js +160 -0
  139. package/lib/commands/notesBridges.js +56 -0
  140. package/lib/commands/notesLinks.js +60 -0
  141. package/lib/commands/notification.js +57 -0
  142. package/lib/commands/nucleus.js +92 -0
  143. package/lib/commands/objective.js +229 -0
  144. package/lib/commands/onboard.js +328 -0
  145. package/lib/commands/open.js +56 -0
  146. package/lib/commands/org.js +266 -0
  147. package/lib/commands/plan.js +906 -0
  148. package/lib/commands/plugin-doctor.js +1091 -0
  149. package/lib/commands/plugin.js +539 -0
  150. package/lib/commands/policy.js +78 -0
  151. package/lib/commands/powers.js +296 -0
  152. package/lib/commands/prompt.js +350 -0
  153. package/lib/commands/qlarity.js +690 -0
  154. package/lib/commands/quoxbox.js +270 -0
  155. package/lib/commands/quoxchat.js +266 -0
  156. package/lib/commands/quoxseo.js +340 -0
  157. package/lib/commands/reach.js +158 -0
  158. package/lib/commands/role.js +286 -0
  159. package/lib/commands/run.js +132 -0
  160. package/lib/commands/safety.js +107 -0
  161. package/lib/commands/scim.js +88 -0
  162. package/lib/commands/scope.js +58 -0
  163. package/lib/commands/security.js +157 -0
  164. package/lib/commands/service.js +89 -0
  165. package/lib/commands/settings.js +99 -0
  166. package/lib/commands/setup.js +543 -0
  167. package/lib/commands/skillManifest.js +113 -0
  168. package/lib/commands/skillRightsFormat.js +38 -0
  169. package/lib/commands/skills.js +644 -0
  170. package/lib/commands/slot.js +1198 -0
  171. package/lib/commands/sso.js +229 -0
  172. package/lib/commands/status.js +137 -0
  173. package/lib/commands/tag.js +91 -0
  174. package/lib/commands/team.js +140 -0
  175. package/lib/commands/tg.js +240 -0
  176. package/lib/commands/tiktok.js +218 -0
  177. package/lib/commands/tool.js +216 -0
  178. package/lib/commands/trace.js +525 -0
  179. package/lib/commands/trigger.js +128 -0
  180. package/lib/commands/vault.js +1424 -0
  181. package/lib/commands/voice.js +182 -0
  182. package/lib/commands/volt.js +528 -0
  183. package/lib/commands/watch.js +115 -0
  184. package/lib/commands/webhook.js +137 -0
  185. package/lib/commands/workflow.js +503 -0
  186. package/lib/commands/x.js +439 -0
  187. package/lib/core/actingAs.js +136 -0
  188. package/lib/core/aee.js +53 -0
  189. package/lib/core/agentAlias.js +163 -0
  190. package/lib/core/approvalContext.js +61 -0
  191. package/lib/core/auth.js +350 -0
  192. package/lib/core/banner.js +143 -0
  193. package/lib/core/brainPrime.js +90 -0
  194. package/lib/core/callerSource.js +129 -0
  195. package/lib/core/claudeLs.js +94 -0
  196. package/lib/core/client.js +584 -0
  197. package/lib/core/completions.js +163 -0
  198. package/lib/core/config.js +551 -0
  199. package/lib/core/confirm.js +37 -0
  200. package/lib/core/deeplinks.js +76 -0
  201. package/lib/core/doneEngineFetch.js +219 -0
  202. package/lib/core/errors.js +132 -0
  203. package/lib/core/global-flags.js +90 -0
  204. package/lib/core/governedSession.js +402 -0
  205. package/lib/core/lazyCommands.js +119 -0
  206. package/lib/core/liveSessionsReport.js +112 -0
  207. package/lib/core/org-override.js +52 -0
  208. package/lib/core/output.js +239 -0
  209. package/lib/core/prodEnv.js +7 -0
  210. package/lib/core/quoxglyph.js +158 -0
  211. package/lib/core/residentAlarm.js +282 -0
  212. package/lib/core/residentAuth.js +243 -0
  213. package/lib/core/residentPump.js +229 -0
  214. package/lib/core/residentSeed.js +104 -0
  215. package/lib/core/sessionHeartbeat.js +192 -0
  216. package/lib/core/sessionsOverview.js +160 -0
  217. package/lib/core/slotKeeper.js +282 -0
  218. package/lib/core/supervisorEngine.js +864 -0
  219. package/lib/core/systemdInstaller.js +256 -0
  220. package/lib/core/version.js +87 -0
  221. package/lib/core/websiteSession.js +148 -0
  222. package/lib/lucid/LucidIntro.jsx +105 -0
  223. package/lib/lucid/blind.js +71 -0
  224. package/lib/lucid/deck.js +32 -0
  225. package/lib/lucid/fmt.js +139 -0
  226. package/lib/lucid/sitrep.js +173 -0
  227. package/lib/plugins/loader.js +93 -0
  228. package/lib/quoxon/activity.js +104 -0
  229. package/lib/quoxon/api.js +473 -0
  230. package/lib/quoxon/app.js +1262 -0
  231. package/lib/quoxon/capabilities.js +75 -0
  232. package/lib/quoxon/commands.js +154 -0
  233. package/lib/quoxon/connect.js +467 -0
  234. package/lib/quoxon/connectCard.js +28 -0
  235. package/lib/quoxon/context.js +68 -0
  236. package/lib/quoxon/copy.js +33 -0
  237. package/lib/quoxon/demo.fixtures.js +24 -0
  238. package/lib/quoxon/demo.js +35 -0
  239. package/lib/quoxon/egg.js +36 -0
  240. package/lib/quoxon/events.js +458 -0
  241. package/lib/quoxon/gallery.js +223 -0
  242. package/lib/quoxon/indicator.js +193 -0
  243. package/lib/quoxon/intent.js +41 -0
  244. package/lib/quoxon/keys.js +121 -0
  245. package/lib/quoxon/main.js +244 -0
  246. package/lib/quoxon/markdown.js +317 -0
  247. package/lib/quoxon/model.js +205 -0
  248. package/lib/quoxon/motion/clock.js +69 -0
  249. package/lib/quoxon/motion/color.js +35 -0
  250. package/lib/quoxon/motion/frame.js +14 -0
  251. package/lib/quoxon/motion/reveal.js +54 -0
  252. package/lib/quoxon/motion/ring.js +79 -0
  253. package/lib/quoxon/objects/agent.js +61 -0
  254. package/lib/quoxon/objects/crystal.js +100 -0
  255. package/lib/quoxon/objects/fetch.js +170 -0
  256. package/lib/quoxon/objects/index.js +85 -0
  257. package/lib/quoxon/objects/ladder.js +71 -0
  258. package/lib/quoxon/objects/memory.js +44 -0
  259. package/lib/quoxon/objects/model.js +118 -0
  260. package/lib/quoxon/objects/proof.js +110 -0
  261. package/lib/quoxon/objects/skill.js +105 -0
  262. package/lib/quoxon/objects/view.js +76 -0
  263. package/lib/quoxon/objects/workflow.js +119 -0
  264. package/lib/quoxon/observers.js +42 -0
  265. package/lib/quoxon/onboarding/copy.js +235 -0
  266. package/lib/quoxon/onboarding/credential.js +95 -0
  267. package/lib/quoxon/onboarding/detect.js +22 -0
  268. package/lib/quoxon/onboarding/finish.js +119 -0
  269. package/lib/quoxon/onboarding/index.js +67 -0
  270. package/lib/quoxon/onboarding/installRunner.js +225 -0
  271. package/lib/quoxon/onboarding/intent.js +53 -0
  272. package/lib/quoxon/onboarding/journey.js +73 -0
  273. package/lib/quoxon/onboarding/preflight.js +142 -0
  274. package/lib/quoxon/onboarding/runner.js +58 -0
  275. package/lib/quoxon/onboarding/view.js +625 -0
  276. package/lib/quoxon/print.js +94 -0
  277. package/lib/quoxon/qart.js +379 -0
  278. package/lib/quoxon/qglyph.js +143 -0
  279. package/lib/quoxon/qhello.js +58 -0
  280. package/lib/quoxon/qmotion.js +111 -0
  281. package/lib/quoxon/render.js +648 -0
  282. package/lib/quoxon/roster.js +221 -0
  283. package/lib/quoxon/route.js +38 -0
  284. package/lib/quoxon/rules.js +130 -0
  285. package/lib/quoxon/session.js +68 -0
  286. package/lib/quoxon/skills.js +30 -0
  287. package/lib/quoxon/slash/compact.js +90 -0
  288. package/lib/quoxon/slash/ledger.js +83 -0
  289. package/lib/quoxon/slash/listView.js +28 -0
  290. package/lib/quoxon/slash/palette.js +104 -0
  291. package/lib/quoxon/slash/paletteController.js +59 -0
  292. package/lib/quoxon/slash/panels.js +101 -0
  293. package/lib/quoxon/slash/picker.js +38 -0
  294. package/lib/quoxon/slash/restart.js +144 -0
  295. package/lib/quoxon/slash/resume.js +59 -0
  296. package/lib/quoxon/slash/settings.js +50 -0
  297. package/lib/quoxon/sound/index.js +186 -0
  298. package/lib/quoxon/sound/mapping.js +119 -0
  299. package/lib/quoxon/sound/output.js +52 -0
  300. package/lib/quoxon/sound/synth.js +122 -0
  301. package/lib/quoxon/term.js +106 -0
  302. package/lib/quoxon/textBuffer.js +242 -0
  303. package/lib/quoxon/theme.js +253 -0
  304. package/lib/quoxon/transcript.js +150 -0
  305. package/lib/quoxon/web/qface.js +81 -0
  306. package/lib/quoxon/welcome.js +52 -0
  307. package/lib/tui/app.jsx +289 -0
  308. package/lib/tui/components/Breadcrumb.jsx +42 -0
  309. package/lib/tui/components/ChatInput.jsx +68 -0
  310. package/lib/tui/components/Confirm.jsx +32 -0
  311. package/lib/tui/components/DataTable.jsx +270 -0
  312. package/lib/tui/components/DetailPanel.jsx +241 -0
  313. package/lib/tui/components/HelpOverlay.jsx +67 -0
  314. package/lib/tui/components/Layout.jsx +129 -0
  315. package/lib/tui/components/Palette.jsx +141 -0
  316. package/lib/tui/components/SearchInput.jsx +125 -0
  317. package/lib/tui/components/Sparkline.jsx +55 -0
  318. package/lib/tui/components/StatusBar.jsx +133 -0
  319. package/lib/tui/components/Toast.jsx +52 -0
  320. package/lib/tui/hooks/useApi.js +44 -0
  321. package/lib/tui/hooks/useHealth.js +90 -0
  322. package/lib/tui/hooks/usePolledApi.js +86 -0
  323. package/lib/tui/hooks/useTerminalSize.js +46 -0
  324. package/lib/tui/index.js +56 -0
  325. package/lib/tui/intro/Intro.jsx +332 -0
  326. package/lib/tui/jsx-loader.js +32 -0
  327. package/lib/tui/keys.js +87 -0
  328. package/lib/tui/registry.js +49 -0
  329. package/lib/tui/theme.js +103 -0
  330. package/lib/tui/views/AdminView.jsx +398 -0
  331. package/lib/tui/views/AgentView.jsx +204 -0
  332. package/lib/tui/views/BackupView.jsx +225 -0
  333. package/lib/tui/views/BrainView.jsx +223 -0
  334. package/lib/tui/views/ChatView.jsx +468 -0
  335. package/lib/tui/views/FilesView.jsx +218 -0
  336. package/lib/tui/views/FleetView.jsx +234 -0
  337. package/lib/tui/views/GovernanceView.jsx +453 -0
  338. package/lib/tui/views/HealthView.jsx +216 -0
  339. package/lib/tui/views/InboxView.jsx +228 -0
  340. package/lib/tui/views/LucidDeck.jsx +727 -0
  341. package/lib/tui/views/MainMenu.jsx +123 -0
  342. package/lib/tui/views/MemoryView.jsx +377 -0
  343. package/lib/tui/views/ObjectivesView.jsx +230 -0
  344. package/lib/tui/views/OrgView.jsx +229 -0
  345. package/lib/tui/views/ToolView.jsx +221 -0
  346. package/lib/tui/views/VaultView.jsx +422 -0
  347. package/lib/tui/views/WorkflowView.jsx +329 -0
  348. package/package.json +72 -4
@@ -0,0 +1,625 @@
1
+ /**
2
+ * Getting started: the Ink view bare `quox` shows when no server is configured (FIRST_RUN_JOURNEY.md, C2a).
3
+ * A labelled, scripted setup guide: no model, no account, no network beyond local checks. It is deliberately a
4
+ * separate module from app.js; main.js picks one or the other.
5
+ *
6
+ * Steps: ask -> offer (Y/n) | declined -> preflight -> credential -> key -> install (installStop) ->
7
+ * ownerEmail -> ownerPassword -> finishing -> finale | notyet
8
+ * With no finisher (the dry run) install goes straight to the finale, as in C2a.
9
+ * Every line of copy comes from ./copy.js.
10
+ */
11
+ import React from 'react';
12
+ import { Box, Static, Text, useStdin, useStdout } from 'ink';
13
+ import { HistoryItem, Row, introAnimates, clampCols } from '../render.js';
14
+ import { c, clip } from '../theme.js';
15
+ import { parseChunk, initialKeyState, PASTE_ON, PASTE_OFF } from '../keys.js';
16
+ import { packageVersion } from '../welcome.js';
17
+ import * as T from './copy.js';
18
+ import { matchIntent, yesNo, parseConnect } from './intent.js';
19
+ import { loadJourney, saveJourney, clearJourney, resumeStep } from './journey.js';
20
+ import { pendingRows, runPreflight, preflightPassed, detectOllama, defaultDeps } from './preflight.js';
21
+ import { credentialOptions, KEY_PROVIDERS, normaliseOllamaUrl } from './credential.js';
22
+ import { HelloIntro, helloIntroMs } from '../qmotion.js';
23
+ import { launchEgg } from '../egg.js';
24
+ import { passwordProblem, emailOk, generatePassword } from './finish.js';
25
+
26
+ const h = React.createElement;
27
+ const GLYPH = { ok: '✔', fail: '✖', warn: '⚠', running: '◐', pending: '·' };
28
+ const TONE = { ok: 'ok', fail: 'fail', warn: 'warn', running: 'sky', pending: 'dim' };
29
+ const fmtS = (ms) => `${(Math.max(0, ms) / 1000).toFixed(1)}s`;
30
+
31
+ /** Word wrap to `width` columns; each line is its own <Text>, so every wrapped line keeps its colour. */
32
+ export function wrapWords(text, width) {
33
+ const w = Math.max(8, width);
34
+ const out = [];
35
+ for (const para of String(text ?? '').split('\n')) {
36
+ let line = '';
37
+ for (const word of para.split(/ +/)) {
38
+ let wd = word;
39
+ while ([...wd].length > w) { if (line) { out.push(line); line = ''; } out.push([...wd].slice(0, w).join('')); wd = [...wd].slice(w).join(''); }
40
+ if (!line) line = wd;
41
+ else if ([...line].length + 1 + [...wd].length <= w) line += ' ' + wd;
42
+ else { out.push(line); line = wd; }
43
+ }
44
+ out.push(line);
45
+ }
46
+ return out;
47
+ }
48
+ const Para = ({ text, width, color, bold }) => h(Box, { flexDirection: 'column' }, ...wrapWords(text, width).map((l, i) => h(Text, { key: i, color, bold }, l)));
49
+
50
+ // ---- static items -----------------------------------------------------------------------------
51
+ function Item({ item, cols }) {
52
+ switch (item.kind) {
53
+ case 'intro': return h(HelloIntro, { item, cols, text: T.HELLO });
54
+ case 'welcome': case 'user': return h(HistoryItem, { item, cols });
55
+ case 'header': return h(Box, { flexDirection: 'column', marginTop: 1 },
56
+ h(Text, null, h(Text, { color: c('violet'), bold: true }, T.LABEL), h(Text, { color: c('dim') }, ' ' + '─'.repeat(Math.max(1, cols - T.LABEL.length - 1)))),
57
+ h(Text, { color: c('dim') }, clip(T.LABEL_SUB, cols)));
58
+ case 'guide': return h(Box, { marginTop: item.gap === false ? 0 : 1, paddingLeft: 2 }, h(Para, { width: cols - 2, color: c(item.tone || 'text'), bold: Boolean(item.bold), text: item.text }));
59
+ case 'dim': return h(Box, { paddingLeft: 2 }, h(Para, { width: cols - 2, color: c(item.tone || 'dim'), text: item.text }));
60
+ case 'cmd': return h(Box, { paddingLeft: 4 }, h(Para, { width: cols - 4, color: c('sky'), text: item.text }));
61
+ case 'long': { // a URL or hash that must never be hard-wrapped (copy, click): one line if it fits, else its own line
62
+ if ([...item.text].length <= cols - 2) return h(Box, { marginTop: item.gap === false ? 0 : 1, paddingLeft: 2 }, h(Text, { color: c(item.tone || 'text') }, item.text));
63
+ return h(Box, { flexDirection: 'column', marginTop: item.gap === false ? 0 : 1, paddingLeft: 2 },
64
+ h(Para, { width: cols - 2, color: c(item.tone || 'text'), text: item.lead }),
65
+ h(Box, { width: [...item.value].length + 2 }, h(Text, { color: c('sky'), wrap: 'truncate-end' }, item.value)));
66
+ }
67
+ case 'list': return h(Box, { flexDirection: 'column', paddingLeft: 2 }, ...item.items.map((t, i) => h(Box, { key: i, flexDirection: 'row' },
68
+ h(Text, { color: c('sky') }, item.numbered ? `${i + 1} ` : '· '), h(Para, { width: cols - 5, color: c('text'), text: t }))));
69
+ case 'rows': return h(Box, { flexDirection: 'column', marginTop: 1 }, ...item.rows.map((r, i) => h(CheckRow, { key: i, r, cols })));
70
+ default: return null;
71
+ }
72
+ }
73
+
74
+ function CheckRow({ r, cols }) {
75
+ return h(Box, { flexDirection: 'column' },
76
+ h(Row, { glyph: GLYPH[r.status] || '·', tone: TONE[r.status] || 'dim', name: r.name, body: r.body, tail: r.tail, cols }),
77
+ r.status === 'fail' && r.fix ? h(Box, { paddingLeft: 6, flexDirection: 'column' },
78
+ h(Para, { width: cols - 6, color: c('placeholder'), text: r.fix }),
79
+ r.link ? h(Text, { color: c('sky') }, clip(r.link, cols - 6)) : null) : null);
80
+ }
81
+
82
+ function InputLine({ text, placeholder, cols, masked = false }) {
83
+ const shown = masked ? '•'.repeat(Math.min([...text].length, Math.max(4, cols - 20))) : text;
84
+ const room = cols - 4;
85
+ const tail = [...shown].slice(-room).join('');
86
+ return h(Box, { marginTop: 1 },
87
+ h(Text, { color: c('sky'), bold: true }, '› '),
88
+ tail ? h(Text, { color: c('input') }, tail) : null,
89
+ h(Text, { inverse: true }, ' '),
90
+ !tail && placeholder ? h(Text, { color: c('placeholder') }, ' ' + clip(placeholder, room - 2)) : null,
91
+ masked && text ? h(Text, { color: c('dim') }, ` ${[...text].length} chars`) : null);
92
+ }
93
+
94
+ const Hint = ({ text, cols }) => h(Box, { paddingLeft: 2 }, h(Text, { color: c('dim') }, clip(text, cols - 2)));
95
+
96
+ // ---- the view ---------------------------------------------------------------------------------
97
+ export function GettingStarted({
98
+ columns, version = '0.2.0', cwd = '', welcome = false, animateIntro, earlyInput = null,
99
+ journeyPath, dryRun = false, runner = null, preflightDeps, runChecks = runPreflight, findOllama = detectOllama,
100
+ requestCredential, broker = null, finisher = null, onExit = () => {}, now = () => Date.now()
101
+ }) {
102
+ const { stdin, setRawMode, isRawModeSupported } = useStdin();
103
+ const { stdout } = useStdout();
104
+ const cols = clampCols(columns ?? ((stdout?.columns || 80) - 1));
105
+ const [, force] = React.useState(0);
106
+ const rerender = () => force(n => n + 1);
107
+ const seq = React.useRef(0);
108
+ const nid = () => `g${seq.current++}`;
109
+
110
+ const saved = React.useMemo(() => (journeyPath ? loadJourney(journeyPath) : null), []);
111
+ const resumeAt = resumeStep(saved);
112
+ const introItem = { id: 'intro', kind: 'intro', text: `${version} · Getting started · ${cwd}` };
113
+ const opening = () => {
114
+ const items = [introItem, ...(welcome ? [{ id: 'welcome', kind: 'welcome', version: packageVersion() }] : []), { id: 'hdr', kind: 'header' }];
115
+ if (resumeAt) {
116
+ items.push({ id: 'resume', kind: 'guide', text: T.RESUME });
117
+ if (saved.request) items.push({ id: 'resumeReq', kind: 'dim', text: T.resumeRequest(saved.request) });
118
+ } else {
119
+ items.push({ id: 'greet', kind: 'guide', text: T.GREETING_REST }, { id: 'q', kind: 'guide', text: T.QUESTION }, { id: 'ex', kind: 'list', items: T.EXAMPLES, numbered: true });
120
+ }
121
+ return items;
122
+ };
123
+
124
+ const S = React.useRef({
125
+ history: [], step: 'ask', input: '', intent: saved?.intent || null, request: saved?.request || null, provider: null,
126
+ rows: [], fixNote: null, options: [], sel: 1, flash: null, install: null, logs: [], showLog: false, done: false, secret: null,
127
+ installStage: saved?.installStage || null, installDir: saved?.installDir || null, dashboardUrl: saved?.dashboardUrl || null,
128
+ ownerEmail: saved?.ownerEmail || null, finRows: [], stopping: false, vault: null
129
+ });
130
+ const set = (patch) => { S.current = { ...S.current, ...(typeof patch === 'function' ? patch(S.current) : patch) }; rerender(); };
131
+ const push = (...items) => set(s => ({ history: [...s.history, ...items.map(it => ({ id: nid(), ...it }))] }));
132
+ const persist = (extra = {}) => {
133
+ if (!journeyPath) return;
134
+ const c = S.current;
135
+ saveJourney({ step: c.step, intent: c.intent, request: c.request, provider: c.provider, installStage: c.installStage, installDir: c.installDir, dashboardUrl: c.dashboardUrl, ownerEmail: c.ownerEmail, ...(c.ollamaUrl ? { ollamaUrl: c.ollamaUrl } : {}), ...extra }, journeyPath, () => new Date(now()).toISOString());
136
+ };
137
+ const live = React.useRef(true);
138
+ React.useEffect(() => () => { live.current = false; }, []);
139
+
140
+ // ---- intro (Orbit & Lock), then the opening lines move into <Static> --------------------------
141
+ const animating = animateIntro ?? introAnimates(stdout?.isTTY);
142
+ // Orbit & Lock, then the Q says hello (T.HELLO decodes in beside it); rarely the Dragon's Eye after that.
143
+ const eye = React.useMemo(() => (animating && animateIntro === undefined ? launchEgg({ tty: Boolean(stdout?.isTTY) }) : false), []);
144
+ const introMs = helloIntroMs({ eye, text: T.HELLO });
145
+ const introStart = React.useRef(Date.now());
146
+ const [, setFrame] = React.useState(0);
147
+ const introDone = React.useRef(false);
148
+ const finishIntro = () => {
149
+ if (introDone.current) return;
150
+ introDone.current = true;
151
+ S.current = { ...S.current, history: [...opening(), ...S.current.history] };
152
+ if (resumeAt === 'offer') offerLines();
153
+ else if (resumeAt === 'preflight') startPreflight();
154
+ else rerender();
155
+ };
156
+ React.useEffect(() => {
157
+ if (!animating) { finishIntro(); return undefined; }
158
+ introStart.current = Date.now();
159
+ const id = setInterval(() => {
160
+ if (Date.now() - introStart.current >= introMs) { clearInterval(id); setTimeout(finishIntro, 0); }
161
+ setFrame(f => f + 1);
162
+ }, 50);
163
+ return () => clearInterval(id);
164
+ }, []);
165
+
166
+ // ---- steps ------------------------------------------------------------------------------------
167
+ const exit = (result) => {
168
+ if (S.current.done) return;
169
+ S.current.secret?.drop?.();
170
+ set({ done: true, secret: null });
171
+ setTimeout(() => onExit(result), 0);
172
+ };
173
+
174
+ function offerLines() {
175
+ const items = [{ kind: 'guide', text: T.OFFER }];
176
+ items.push({ kind: 'guide', text: T.OFFER_ASK, bold: true, gap: false }, { kind: 'dim', text: T.ALREADY });
177
+ push(...items);
178
+ set({ step: 'offer' });
179
+ persist();
180
+ }
181
+
182
+ function onRequest(text) {
183
+ const m = matchIntent(text);
184
+ if (m.intent === 'empty') return;
185
+ push({ kind: 'user', text });
186
+ if (m.intent === 'connect') { push({ kind: 'guide', text: T.CONNECT_INTENT_LINE }); set({ step: 'ask' }); return; }
187
+ set({ intent: m.intent, request: text });
188
+ push({ kind: 'guide', text: m.intent === 'offscript' ? T.DEFERRAL : T.INTENT_LINES[m.intent] });
189
+ offerLines();
190
+ }
191
+
192
+ function onSlash(text) {
193
+ const t = text.trim();
194
+ if (/^\/(exit|quit|q)$/i.test(t)) { push({ kind: 'user', text: t }, { kind: 'dim', text: T.BYE }); persist(); exit({ action: 'exit' }); return true; }
195
+ if (/^\/help$/i.test(t)) { push({ kind: 'user', text: t }, { kind: 'list', items: T.HELP.map(([k, v]) => `${k} ${v}`) }); return true; }
196
+ const conn = parseConnect(t);
197
+ if (conn) {
198
+ push({ kind: 'user', text: t });
199
+ if (!conn.ok) { push({ kind: 'dim', tone: 'warn', text: conn.reason === 'url' ? T.connectBadUrl(conn.value) : T.CONNECT_USAGE }); return true; }
200
+ push({ kind: 'guide', text: T.connectHandoff(conn.url) });
201
+ exit({ action: 'connect', url: conn.url });
202
+ return true;
203
+ }
204
+ push({ kind: 'user', text: t }, { kind: 'dim', tone: 'warn', text: T.SLASH_UNKNOWN });
205
+ return true;
206
+ }
207
+
208
+ function submit() {
209
+ const s = S.current;
210
+ const text = s.input.trim();
211
+ set({ input: '' });
212
+ if (text.startsWith('/')) { onSlash(text); return; }
213
+ if (s.step === 'ask') return onRequest(text);
214
+ if (s.step === 'offer' || s.step === 'declined') {
215
+ const yn = yesNo(text);
216
+ if (yn === true) { push({ kind: 'user', text: text || 'yes' }); startPreflight(); return; }
217
+ if (yn === false) { push({ kind: 'user', text }, { kind: 'guide', text: T.DECLINED }, { kind: 'dim', text: T.DECLINED_HINT }); set({ step: 'declined' }); persist(); return; }
218
+ return onRequest(text); // a new request instead of an answer
219
+ }
220
+ }
221
+
222
+ async function startPreflight() {
223
+ set({ step: 'preflight', rows: pendingRows(), fixNote: null });
224
+ persist();
225
+ // resuming an install this guide started: its own ports being busy is expected
226
+ const deps = S.current.installStage ? { ...(preflightDeps || defaultDeps()), ownInstall: true } : preflightDeps;
227
+ const rows = await runChecks(deps, (r) => { if (live.current) set(s => ({ rows: s.rows.map(x => (x.id === r.id ? r : x)) })); });
228
+ if (!live.current || S.current.done) return;
229
+ if (!preflightPassed(rows)) { set({ step: 'preflightFail', rows, fixNote: T.PREFLIGHT_FAIL }); return; }
230
+ push({ kind: 'guide', text: T.PREFLIGHT_TITLE, bold: true }, { kind: 'rows', rows }, { kind: 'guide', tone: 'ok', text: T.PREFLIGHT_OK });
231
+ toCredential();
232
+ }
233
+
234
+ async function toCredential() {
235
+ set({ rows: [] });
236
+ const ollama = await findOllama();
237
+ if (!live.current || S.current.done) return;
238
+ const options = credentialOptions({ ollama });
239
+ push({ kind: 'guide', text: T.CRED_TITLE, bold: true });
240
+ set({ step: 'credential', options, sel: Math.max(0, options.findIndex(o => !o.disabled)), flash: null });
241
+ persist();
242
+ }
243
+
244
+ async function choose(opt) {
245
+ if (!opt) return;
246
+ if (opt.disabled) { set({ flash: T.CRED_CHATGPT_SOON }); return; }
247
+ set({ flash: null, provider: opt.id });
248
+ if (KEY_PROVIDERS.has(opt.id)) {
249
+ set({ step: 'key', input: '' });
250
+ const held = await requestCredential(opt.id); // THE hook: K1 secure card once it merges
251
+ if (!live.current || S.current.done) { held?.drop?.(); return; }
252
+ if (!held) { set({ step: 'credential', input: '' }); return; }
253
+ push({ kind: 'user', text: opt.label }, { kind: 'guide', tone: 'ok', text: T.credHeld(opt.label, held.fingerprint) });
254
+ set({ secret: held, input: '' });
255
+ } else if (opt.id === 'ollama' && !opt.detected) {
256
+ set({ step: 'ollamaUrl', input: '' }); // B3: nothing answered locally; ask for the LAN URL
257
+ return;
258
+ } else {
259
+ push({ kind: 'user', text: opt.label }, { kind: 'guide', tone: opt.id === 'skip' ? 'warn' : 'ok', text: opt.id === 'skip' ? T.CRED_SKIPPED : T.CRED_OLLAMA });
260
+ }
261
+ startInstall();
262
+ }
263
+
264
+ function submitOllamaUrl() {
265
+ const url = normaliseOllamaUrl(S.current.input);
266
+ if (!url) { set({ flash: T.OLLAMA_URL_BAD }); return; }
267
+ push({ kind: 'user', text: url }, { kind: 'guide', tone: 'ok', text: T.ollamaChosen(url) });
268
+ set({ input: '', flash: null, ollamaUrl: url });
269
+ startInstall();
270
+ }
271
+
272
+ async function startInstall() {
273
+ if (!runner) { // no runner (should not happen outside tests): say so, and give the installer command
274
+ push({ kind: 'guide', text: T.INSTALL_NOT_YET }, { kind: 'cmd', text: T.INSTALL_COMMAND }, { kind: 'dim', text: T.INSTALL_NOT_YET_AFTER });
275
+ if (S.current.secret) { S.current.secret.drop(); push({ kind: 'dim', tone: 'warn', text: T.INSTALL_KEY_GONE }); }
276
+ set({ step: 'notyet', secret: null });
277
+ persist({ step: 'install' });
278
+ return;
279
+ }
280
+ const dry = runner.kind === 'dry-run';
281
+ const healthOnly = !dry && (S.current.installStage === 'start' || S.current.installStage === 'health');
282
+ if (!dry && runner.installDir) set({ installDir: runner.installDir });
283
+ if (!dry && S.current.install == null) {
284
+ if (runner.installDir) push({ kind: 'dim', text: T.installWhere(runner.installDir) });
285
+ if (healthOnly) push({ kind: 'dim', text: T.INSTALL_HEALTH_ONLY });
286
+ }
287
+ const stages = runner.stages.map(st => ({ id: st.id, name: st.id.toUpperCase(), body: st.label, status: 'pending', startedAt: null, ms: null, earlier: false }));
288
+ set({ step: 'install', install: { stages, kind: runner.kind, failed: null, degraded: false }, logs: [], showLog: false, stopping: false });
289
+ persist();
290
+ const ac = new AbortController();
291
+ S.current.abort = ac;
292
+ const res = await runner.run({
293
+ signal: ac.signal, request: S.current.request,
294
+ resumeFrom: S.current.installStage, resumeDashboardUrl: S.current.dashboardUrl,
295
+ onCheckpoint: (id, extra = {}) => { // the last COMPLETED stage, so the next quox resumes honestly
296
+ set({ installStage: id, ...(extra.dashboardUrl ? { dashboardUrl: extra.dashboardUrl } : {}) });
297
+ persist({ step: 'install' });
298
+ },
299
+ onEvent: (ev) => {
300
+ if (!live.current) return;
301
+ if (ev.type === 'log') set(s => ({ logs: [...s.logs, ev.line].slice(-8) }));
302
+ if (ev.type === 'script') push({ kind: 'long', tone: 'dim', gap: false, text: T.installScript(ev.url, ev.sha256, ev.bytes), lead: T.installScriptLead(ev.url, ev.bytes), value: ev.sha256 });
303
+ if (ev.type === 'stage') set(s => ({ install: { ...s.install, stages: s.install.stages.map(x => (x.id === ev.id ? { ...x, status: ev.status, startedAt: ev.status === 'running' ? ev.at : x.startedAt, ms: ev.ms ?? x.ms, earlier: Boolean(ev.earlier) } : x)) } }));
304
+ }
305
+ });
306
+ S.current.abort = null;
307
+ if (!live.current || S.current.done) return;
308
+ if (res.interrupted || S.current.stopping) {
309
+ push({ kind: 'dim', text: T.INSTALL_STOPPED }); persist({ step: 'install' }); exit({ action: 'exit' }); return;
310
+ }
311
+ if (res.dashboardUrl) set({ dashboardUrl: res.dashboardUrl });
312
+ const rows = S.current.install.stages.map(st => ({ ...st, tail: st.earlier ? T.INSTALL_EARLIER : st.ms != null ? fmtS(st.ms) : '' }));
313
+ if (!res.ok) { set(s => ({ step: 'installFail', install: { ...s.install, failed: res.failedStage || '?', degraded: Boolean(res.degraded), error: res.error || null } })); persist({ step: 'install' }); return; }
314
+ push({ kind: 'guide', text: T.INSTALL_TITLE + (dry ? ` (${T.INSTALL_DRY})` : ''), bold: true }, { kind: 'rows', rows });
315
+ if (dry || !finisher) { finale({ dashboardUrl: res.dashboardUrl, dry }); return; }
316
+ toOwner();
317
+ }
318
+
319
+ // ---- owner account (C2c) ----------------------------------------------------------------------
320
+ function toOwner(note = null) {
321
+ if (note) push({ kind: 'guide', tone: 'warn', text: note });
322
+ else push({ kind: 'guide', text: T.OWNER_TITLE, bold: true }, { kind: 'dim', text: T.OWNER_WHY });
323
+ set({ step: 'ownerEmail', input: S.current.ownerEmail || '', flash: null });
324
+ persist({ step: 'owner' });
325
+ }
326
+ function onOwnerEmail() {
327
+ const email = S.current.input.trim();
328
+ if (!emailOk(email)) { set({ flash: T.OWNER_EMAIL_BAD }); return; }
329
+ push({ kind: 'user', text: email });
330
+ set({ ownerEmail: email, step: 'ownerPassword', input: '', flash: null });
331
+ persist({ step: 'owner' });
332
+ }
333
+ function onOwnerPassword() {
334
+ let pw = S.current.input;
335
+ set({ input: '' });
336
+ if (!pw) {
337
+ pw = generatePassword(); // shown once, here, then only the token is kept
338
+ push({ kind: 'guide', tone: 'ok', text: T.ownerPwShownOnce(pw) }, { kind: 'dim', text: T.OWNER_PW_SAVE });
339
+ } else {
340
+ const bad = passwordProblem(pw);
341
+ if (bad) { set({ flash: bad }); return; }
342
+ }
343
+ set({ flash: null });
344
+ runFinish(S.current.ownerEmail, pw);
345
+ }
346
+
347
+ async function runFinish(email, password) {
348
+ const F = T.FIN;
349
+ const opt = credentialOptions({ ollama: true }).find(o => o.id === S.current.provider);
350
+ const rows = [
351
+ { id: 'account', name: F.account.name, status: 'running', body: F.account.running },
352
+ { id: 'signin', name: F.signin.name, status: 'pending', body: F.signin.running },
353
+ { id: 'vault', name: F.vault.name, status: 'pending', body: S.current.secret ? F.vault.running(opt?.label || 'key') : F.vault.skipped },
354
+ { id: 'link', name: F.link.name, status: 'pending', body: F.link.running }
355
+ ];
356
+ const row = (id, patch) => set(s => ({ finRows: s.finRows.map(r => (r.id === id ? { ...r, ...patch } : r)) }));
357
+ set({ step: 'finishing', finRows: rows });
358
+ persist({ step: 'finishing' });
359
+
360
+ const made = await finisher.createOwner({ email, password });
361
+ if (!live.current || S.current.done) return;
362
+ if (!made.ok && !made.existed) { set({ finRows: [] }); toOwner(made.error); set({ step: 'ownerPassword', input: '' }); return; }
363
+ row('account', { status: 'ok', body: made.existed ? F.account.existing(email) : F.account.ok(email) });
364
+
365
+ row('signin', { status: 'running' });
366
+ const signed = await finisher.signIn({ email, password });
367
+ password = null; // the password is not needed again; only the token is kept (in the profile)
368
+ if (!live.current || S.current.done) return;
369
+ if (!signed.ok) {
370
+ set({ finRows: [] });
371
+ toOwner(made.existed ? T.OWNER_EXISTS : signed.error);
372
+ return;
373
+ }
374
+ row('signin', { status: 'ok', body: F.signin.ok(signed.org) });
375
+
376
+ let vault = null;
377
+ if (S.current.secret) {
378
+ row('vault', { status: 'running' });
379
+ vault = await finisher.storeCredential(S.current.secret); // drops the key, stored or not
380
+ set({ secret: null });
381
+ if (!live.current || S.current.done) return;
382
+ row('vault', { status: vault.ok ? 'ok' : 'warn', body: String(vault.text || '').replace(/^[✔✖]\s*/, '') }); // the row glyph already says it
383
+ } else {
384
+ row('vault', { status: 'warn', body: S.current.provider === 'ollama' ? F.vault.ollama : F.vault.skipped });
385
+ }
386
+ set({ vault });
387
+
388
+ row('link', { status: 'running' });
389
+ const link = await finisher.loginLink(S.current.dashboardUrl);
390
+ if (!live.current || S.current.done) return;
391
+ row('link', link.ok ? { status: 'ok', body: F.link.ok } : { status: 'warn', body: F.link.fail(link.error) });
392
+
393
+ push({ kind: 'guide', text: T.FINISH_TITLE, bold: true }, { kind: 'rows', rows: S.current.finRows });
394
+ if (vault) push({ kind: 'dim', text: vault.ok ? T.KEY_GONE_AFTER : `${T.KEY_GONE_AFTER} ${T.keyNotStored(vault.provider || S.current.provider)}` });
395
+ set({ finRows: [] });
396
+ finale({ dashboardUrl: S.current.dashboardUrl, link: link.ok ? link.url : null, dry: false });
397
+ }
398
+
399
+ function replayText() {
400
+ const { request, intent } = S.current;
401
+ if (!request || intent === 'explore') return T.REPLAY_TOUR;
402
+ return request;
403
+ }
404
+
405
+ function finale({ dashboardUrl, link = null, dry }) {
406
+ const items = [];
407
+ const intent = S.current.intent && T.TIPS[S.current.intent] ? S.current.intent : 'explore';
408
+ if (dry) items.push({ kind: 'guide', tone: 'warn', text: T.FINALE_DRY_BANNER });
409
+ items.push({ kind: 'guide', tone: 'ok', bold: true, text: T.FINALE_RUNNING },
410
+ { kind: 'guide', text: T.finaleReplay(S.current.request && S.current.intent !== 'explore' ? S.current.request : null) });
411
+ if (dry) items.push({ kind: 'dim', text: T.FINALE_REPLAY_DRY });
412
+ const url = dashboardUrl || 'https://localhost:3000';
413
+ if (dry || link) items.push({ kind: 'long', text: T.finaleDashboard(link || url), lead: T.FINALE_DASHBOARD_LEAD, value: link || url });
414
+ else items.push({ kind: 'long', text: T.finaleDashboardPlain(url), lead: T.FINALE_DASHBOARD_LEAD_PLAIN, value: url });
415
+ items.push({ kind: 'guide', gap: false, text: T.FINALE_PROOF });
416
+ if (!dry) items.push({ kind: 'dim', text: T.FINALE_CERT });
417
+ items.push({ kind: 'guide', text: T.FINALE_TIPS_TITLE }, { kind: 'list', items: dry ? T.FINALE_TIPS : T.TIPS[intent] });
418
+ const noAi = !dry && (S.current.provider === 'skip' || (S.current.vault && !S.current.vault.ok));
419
+ if (!dry) items.push({ kind: 'guide', text: noAi ? T.k2NoAi(S.current.provider === 'anthropic' ? 'anthropic' : 'openai') : T.K2[intent] });
420
+ push(...items);
421
+ S.current.secret?.drop?.();
422
+ set({ step: 'finale', secret: null, finaleDry: dry });
423
+ if (!dry && journeyPath) clearJourney(journeyPath);
424
+ }
425
+
426
+ // ---- keys -------------------------------------------------------------------------------------
427
+ const K = React.useRef({ keys: initialKeyState() });
428
+ const onKey = (ev) => {
429
+ const s = S.current;
430
+ if (s.done) return;
431
+ if (ev.type === 'key' && (ev.name === 'ctrl-c' || ev.name === 'ctrl-d') && s.step === 'install') {
432
+ set({ step: 'installStop' }); // never abort a running install on one keypress: ask first
433
+ return;
434
+ }
435
+ if (ev.type === 'key' && (ev.name === 'ctrl-c' || ev.name === 'ctrl-d') && s.step === 'installStop') { stopInstall(); return; }
436
+ if (ev.type === 'key' && (ev.name === 'ctrl-c' || ev.name === 'ctrl-d')) {
437
+ if (s.step === 'key') broker?.cancel?.();
438
+ S.current.abort?.abort?.();
439
+ push({ kind: 'dim', text: T.BYE }); persist(); exit({ action: 'exit' }); return;
440
+ }
441
+ if (!introDone.current) { if (ev.type !== 'key') set({ input: s.input + ev.text.replace(/[\r\n]+/g, ' ') }); return; }
442
+ switch (s.step) {
443
+ case 'ask': case 'offer': case 'declined': return textKey(ev, submit);
444
+ case 'key': return textKey(ev, () => {
445
+ const v = S.current.input;
446
+ if (!v.trim()) { set({ flash: T.CRED_EMPTY }); return; }
447
+ set({ input: '', flash: null });
448
+ broker?.submit?.(v.trim());
449
+ }, () => { set({ input: '', flash: null }); broker?.cancel?.(); });
450
+ case 'ollamaUrl': return textKey(ev, submitOllamaUrl, () => set({ step: 'credential', input: '', flash: null }));
451
+ case 'preflightFail':
452
+ if (ev.type === 'text' && /^r$/i.test(ev.text)) startPreflight();
453
+ else if (ev.type === 'text' && /^c$/i.test(ev.text) && dryRun) {
454
+ push({ kind: 'guide', text: T.PREFLIGHT_TITLE, bold: true }, { kind: 'rows', rows: s.rows }, { kind: 'guide', tone: 'warn', text: T.PREFLIGHT_CONTINUED });
455
+ toCredential();
456
+ }
457
+ else if (ev.type === 'text' && /^q$/i.test(ev.text)) { push({ kind: 'dim', text: T.BYE }); persist({ step: 'preflight' }); exit({ action: 'exit' }); }
458
+ return;
459
+ case 'credential': {
460
+ const n = s.options.length;
461
+ if (ev.type === 'key' && ev.name === 'up') set({ sel: (s.sel - 1 + n) % n, flash: null });
462
+ else if (ev.type === 'key' && (ev.name === 'down' || ev.name === 'tab')) set({ sel: (s.sel + 1) % n, flash: null });
463
+ else if (ev.type === 'key' && ev.name === 'enter') choose(s.options[s.sel]);
464
+ else if (ev.type === 'text' && /^[1-9]$/.test(ev.text) && Number(ev.text) <= n) { set({ sel: Number(ev.text) - 1 }); choose(s.options[Number(ev.text) - 1]); }
465
+ return;
466
+ }
467
+ case 'install': if (ev.type === 'text' && /^v$/i.test(ev.text)) set({ showLog: !s.showLog }); return;
468
+ case 'installStop':
469
+ if (ev.type === 'text' && /^y/i.test(ev.text)) stopInstall();
470
+ else if ((ev.type === 'text' && /^n/i.test(ev.text)) || (ev.type === 'key' && (ev.name === 'enter' || ev.name === 'escape'))) set({ step: 'install' });
471
+ return;
472
+ case 'ownerEmail': return textKey(ev, onOwnerEmail);
473
+ case 'ownerPassword': return textKey(ev, onOwnerPassword, () => set({ step: 'ownerEmail', input: S.current.ownerEmail || '', flash: null }));
474
+ case 'finishing': return;
475
+ case 'installFail':
476
+ if (ev.type === 'text' && /^v$/i.test(ev.text)) set({ showLog: !s.showLog });
477
+ else if (ev.type === 'text' && /^r$/i.test(ev.text)) startInstall();
478
+ return;
479
+ case 'notyet': if (ev.type === 'key' && ev.name === 'enter') exit({ action: 'exit' }); return;
480
+ case 'finale':
481
+ if (ev.type === 'key' && ev.name === 'enter') exit(s.finaleDry || !finisher ? { action: 'exit' } : { action: 'connected', replay: replayText() });
482
+ return;
483
+ default:
484
+ }
485
+ };
486
+ function stopInstall() {
487
+ push({ kind: 'dim', text: T.INSTALL_STOP_ASK }, { kind: 'user', text: 'yes' });
488
+ set({ stopping: true, step: 'install' });
489
+ S.current.abort?.abort?.(); // the runner stops the installer's process group; startInstall then exits
490
+ }
491
+
492
+ function textKey(ev, onEnter, onEscape) {
493
+ const s = S.current;
494
+ const masked = s.step === 'key' || s.step === 'ownerPassword';
495
+ if (ev.type === 'text' || ev.type === 'paste') set({ input: s.input + ev.text.replace(/[\r\n]+/g, masked ? '' : ' '), flash: null });
496
+ else if (ev.name === 'backspace') set({ input: [...s.input].slice(0, -1).join('') });
497
+ else if (ev.name === 'kill-line') set({ input: '' });
498
+ else if (ev.name === 'enter') onEnter();
499
+ else if (ev.name === 'escape' && onEscape) onEscape();
500
+ }
501
+
502
+ React.useEffect(() => {
503
+ if (!stdin) return undefined;
504
+ if (isRawModeSupported) setRawMode(true);
505
+ if (stdout?.isTTY) stdout.write(PASTE_ON);
506
+ const handle = (chunk) => {
507
+ const parsed = parseChunk(chunk, K.current.keys);
508
+ K.current.keys = parsed.state;
509
+ for (const ev of parsed.events) onKey(ev);
510
+ };
511
+ if (earlyInput) { // typed before mount: into the input, never submitted
512
+ earlyInput.detach();
513
+ for (const chunk of earlyInput.chunks.splice(0)) {
514
+ const parsed = parseChunk(chunk, K.current.keys);
515
+ K.current.keys = parsed.state;
516
+ for (const ev of parsed.events) if (ev.type === 'text' || ev.type === 'paste') S.current.input += ev.text.replace(/[\r\n]+/g, ' ');
517
+ }
518
+ }
519
+ stdin.on('data', handle);
520
+ return () => {
521
+ stdin.off('data', handle);
522
+ if (stdout?.isTTY) stdout.write(PASTE_OFF);
523
+ if (isRawModeSupported) setRawMode(false);
524
+ };
525
+ }, [stdin]);
526
+
527
+ // a ticking clock while a stage runs (elapsed time, never a fake percentage)
528
+ React.useEffect(() => {
529
+ if (S.current.step !== 'install' && S.current.step !== 'installStop') return undefined;
530
+ const id = setInterval(rerender, 250);
531
+ return () => clearInterval(id);
532
+ }, [S.current.step]);
533
+
534
+ // the broker drives the masked card (the K1 secure card replaces it)
535
+ React.useEffect(() => broker?.subscribe?.(() => rerender()), [broker]);
536
+
537
+ // ---- render -----------------------------------------------------------------------------------
538
+ const s = S.current;
539
+ const liveParts = [];
540
+ if (!introDone.current) liveParts.push(h(HelloIntro, { key: 'intro', item: introItem, cols, text: T.HELLO, eye, t: Date.now() - introStart.current }));
541
+ else if (!s.done) {
542
+ switch (s.step) {
543
+ case 'ask':
544
+ liveParts.push(h(InputLine, { key: 'in', text: s.input, placeholder: T.INPUT_PLACEHOLDER, cols }), h(Hint, { key: 'hint', text: 'Enter to send · /help · /exit', cols }));
545
+ break;
546
+ case 'offer': case 'declined':
547
+ liveParts.push(h(InputLine, { key: 'in', text: s.input, placeholder: s.step === 'offer' ? 'Enter for yes, or n' : 'yes, /connect server <url>, or /exit', cols }), h(Hint, { key: 'hint', text: 'Enter to send · /exit', cols }));
548
+ break;
549
+ case 'preflight': case 'preflightFail':
550
+ liveParts.push(h(Box, { key: 'pf', marginTop: 1, paddingLeft: 2 }, h(Text, { bold: true, color: c('text') }, T.PREFLIGHT_TITLE)),
551
+ ...s.rows.map((r) => h(CheckRow, { key: r.id, r, cols })));
552
+ if (s.step === 'preflightFail') liveParts.push(h(Box, { key: 'pfn', marginTop: 1, paddingLeft: 2, width: cols }, h(Para, { width: cols - 2, color: c('warn'), text: s.fixNote })), h(Hint, { key: 'h', text: dryRun ? T.PREFLIGHT_RETRY_DRY : T.PREFLIGHT_RETRY, cols }));
553
+ break;
554
+ case 'credential':
555
+ liveParts.push(h(Box, { key: 'opts', flexDirection: 'column', marginTop: 1 }, ...s.options.map((o, i) => {
556
+ const on = i === s.sel;
557
+ return h(Text, { key: o.id },
558
+ h(Text, { color: c(on ? 'sky' : 'dim') }, on ? ' › ' : ' '),
559
+ h(Text, { color: c(o.disabled ? 'dim' : on ? 'input' : 'text'), bold: on && !o.disabled }, `${i + 1} ${o.label}`),
560
+ o.note ? h(Text, { color: c(o.id === 'skip' ? 'warn' : 'dim') }, clip(` ${o.note}`, Math.max(4, cols - o.label.length - 9))) : null);
561
+ })));
562
+ if (s.flash) liveParts.push(h(Box, { key: 'fl', paddingLeft: 2, width: cols }, h(Para, { width: cols - 2, color: c('warn'), text: s.flash })));
563
+ liveParts.push(h(Hint, { key: 'h', text: T.CRED_HINT, cols }));
564
+ break;
565
+ case 'key': {
566
+ const opt = s.options.find(o => o.id === s.provider);
567
+ liveParts.push(h(Box, { key: 'card', flexDirection: 'column', marginTop: 1 },
568
+ h(Text, { color: c('violet') }, '─'.repeat(cols)),
569
+ h(Box, { paddingLeft: 2, width: cols }, h(Para, { width: cols - 2, color: c('text'), text: T.credPrompt(opt?.label || 'API key') })),
570
+ h(InputLine, { text: s.input, cols, masked: true }),
571
+ s.flash ? h(Box, { paddingLeft: 2, width: cols }, h(Para, { width: cols - 2, color: c('warn'), text: s.flash })) : null,
572
+ h(Hint, { text: T.CRED_INPUT_HINT, cols }),
573
+ h(Text, { color: c('violet') }, '─'.repeat(cols))));
574
+ break;
575
+ }
576
+ case 'ollamaUrl':
577
+ liveParts.push(h(Box, { key: 'card', flexDirection: 'column', marginTop: 1 },
578
+ h(Text, { color: c('violet') }, '─'.repeat(cols)),
579
+ h(Box, { paddingLeft: 2, width: cols }, h(Para, { width: cols - 2, color: c('text'), text: T.OLLAMA_URL_PROMPT })),
580
+ h(InputLine, { text: s.input, cols }),
581
+ s.flash ? h(Box, { paddingLeft: 2, width: cols }, h(Para, { width: cols - 2, color: c('warn'), text: s.flash })) : null,
582
+ h(Hint, { text: T.OLLAMA_URL_HINT, cols }),
583
+ h(Text, { color: c('violet') }, '─'.repeat(cols))));
584
+ break;
585
+ case 'ownerEmail': case 'ownerPassword': {
586
+ const pw = s.step === 'ownerPassword';
587
+ if (pw) liveParts.push(h(Box, { key: 'pwp', paddingLeft: 2, width: cols, marginTop: 1 }, h(Para, { width: cols - 2, color: c('text'), text: T.OWNER_PW_PROMPT })));
588
+ liveParts.push(h(InputLine, { key: 'in', text: s.input, placeholder: pw ? '' : T.OWNER_EMAIL_PLACEHOLDER, cols, masked: pw }));
589
+ if (s.flash) liveParts.push(h(Box, { key: 'fl', paddingLeft: 2, width: cols }, h(Para, { width: cols - 2, color: c('warn'), text: s.flash })));
590
+ liveParts.push(h(Hint, { key: 'h', text: pw ? T.OWNER_PW_HINT : 'Enter to continue', cols }));
591
+ break;
592
+ }
593
+ case 'finishing':
594
+ liveParts.push(h(Box, { key: 'ft', marginTop: 1, paddingLeft: 2 }, h(Text, { bold: true, color: c('text') }, T.FINISH_TITLE)),
595
+ ...s.finRows.map((r) => h(CheckRow, { key: r.id, r, cols })));
596
+ break;
597
+ case 'install': case 'installFail': case 'installStop': {
598
+ const t = now();
599
+ const dry = s.install.kind === 'dry-run';
600
+ liveParts.push(h(Box, { key: 'it', marginTop: 1, paddingLeft: 2, width: cols }, h(Para, { width: cols - 2, bold: true, color: c('text'), text: T.INSTALL_TITLE + (dry ? ` (${T.INSTALL_DRY})` : '') })));
601
+ for (const st of s.install.stages) {
602
+ const tail = st.earlier ? T.INSTALL_EARLIER : st.status === 'running' && st.startedAt ? fmtS(t - st.startedAt) : st.ms != null ? fmtS(st.ms) : '';
603
+ liveParts.push(h(CheckRow, { key: st.id, r: { ...st, tail }, cols }));
604
+ }
605
+ if (s.showLog) liveParts.push(h(Box, { key: 'log', flexDirection: 'column', paddingLeft: 4, marginTop: 1 }, ...s.logs.map((l, i) => h(Text, { key: i, color: c('dim') }, clip(l, cols - 4)))));
606
+ if (s.step === 'installFail') {
607
+ liveParts.push(h(Box, { key: 'if', paddingLeft: 2, width: cols, marginTop: 1, flexDirection: 'column' },
608
+ h(Para, { width: cols - 2, color: c('fail'), text: T.INSTALL_FAILED(T.STAGES[s.install.failed] || s.install.failed) }),
609
+ s.install.error ? h(Para, { width: cols - 2, color: c('dim'), text: s.install.error }) : null,
610
+ s.install.degraded ? h(Para, { width: cols - 2, color: c('warn'), text: T.INSTALL_DEGRADED }) : null));
611
+ }
612
+ if (s.step === 'installStop') liveParts.push(h(Box, { key: 'stop', paddingLeft: 2, width: cols, marginTop: 1 }, h(Para, { width: cols - 2, color: c('warn'), bold: true, text: T.INSTALL_STOP_ASK })));
613
+ liveParts.push(h(Hint, { key: 'h', text: s.step === 'installFail' ? T.INSTALL_FAIL_HINT : s.step === 'installStop' ? 'y to stop · n or Enter to carry on' : T.INSTALL_HINT, cols }));
614
+ break;
615
+ }
616
+ case 'notyet': case 'finale':
617
+ liveParts.push(h(Box, { key: 'h', marginTop: 1 }, h(Hint, { text: s.step === 'finale' && !s.finaleDry && finisher ? T.FINALE_EXIT : T.FINALE_EXIT_DRY, cols })));
618
+ break;
619
+ default:
620
+ }
621
+ }
622
+ return h(Box, { flexDirection: 'column' },
623
+ h(Static, { items: introDone.current ? s.history : [] }, (it) => h(Box, { key: it.id, flexDirection: 'column' }, h(Item, { item: it, cols }))),
624
+ ...liveParts);
625
+ }