@hiroleague/taskmanager 0.0.1 → 0.0.3

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 (196) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -52
  3. package/dist/assets/architecture-YZFGNWBL-DoE0KxgG.js +1 -0
  4. package/dist/assets/architectureDiagram-Q4EWVU46-DeuBhy7X.js +36 -0
  5. package/dist/assets/{blockDiagram-DXYQGD6D-DfOGNphI.js → blockDiagram-DXYQGD6D-BDBy9ns9.js} +1 -1
  6. package/dist/assets/{c4Diagram-AHTNJAMY-B2Yfcwbo.js → c4Diagram-AHTNJAMY-CpqJj_8a.js} +1 -1
  7. package/dist/assets/channel-PHRyjspt.js +1 -0
  8. package/dist/assets/{chunk-2KRD3SAO-9yt00aGC.js → chunk-2KRD3SAO-DEpUsxdZ.js} +1 -1
  9. package/dist/assets/chunk-336JU56O-BGQvSwLk.js +2 -0
  10. package/dist/assets/chunk-426QAEUC-Cl9nQN9c.js +1 -0
  11. package/dist/assets/{chunk-4TB4RGXK-DF8yJBFl.js → chunk-4TB4RGXK-Dq7aiIrZ.js} +2 -2
  12. package/dist/assets/{chunk-5FUZZQ4R-XEga0hMC.js → chunk-5FUZZQ4R-B_HuuUjf.js} +1 -1
  13. package/dist/assets/{chunk-5PVQY5BW-BrmXs2Gs.js → chunk-5PVQY5BW-cGfZCZGU.js} +2 -2
  14. package/dist/assets/{chunk-67CJDMHE-5wFKo04G.js → chunk-67CJDMHE-BMYAVZfw.js} +1 -1
  15. package/dist/assets/{chunk-7N4EOEYR-BRRGX_NC.js → chunk-7N4EOEYR-Ct-EY7Nc.js} +1 -1
  16. package/dist/assets/{chunk-AA7GKIK3-DUZv_pNI.js → chunk-AA7GKIK3-Bd4HFpeo.js} +1 -1
  17. package/dist/assets/{chunk-CIAEETIT-mA5aM_d7.js → chunk-CIAEETIT-CrFUkPMT.js} +1 -1
  18. package/dist/assets/{chunk-EDXVE4YY-DxUqDyxy.js → chunk-EDXVE4YY-DMDyt0NF.js} +1 -1
  19. package/dist/assets/{chunk-ENJZ2VHE-BgZKYo1l.js → chunk-ENJZ2VHE-DrWzOrpd.js} +1 -1
  20. package/dist/assets/{chunk-FOC6F5B3-B-cqGCPC.js → chunk-FOC6F5B3-Bemzq96j.js} +1 -1
  21. package/dist/assets/{chunk-ICPOFSXX-BNR1V8rT.js → chunk-ICPOFSXX-DkUVjrLw.js} +5 -5
  22. package/dist/assets/{chunk-K5T4RW27-BLRDzioh.js → chunk-K5T4RW27-ALKIf000.js} +5 -5
  23. package/dist/assets/{chunk-KGLVRYIC-CTkQSeKy.js → chunk-KGLVRYIC-Bg6HNTZ-.js} +1 -1
  24. package/dist/assets/{chunk-LIHQZDEY-Cf34Nu3J.js → chunk-LIHQZDEY-DeyGongE.js} +1 -1
  25. package/dist/assets/{chunk-ORNJ4GCN-D3uXgbay.js → chunk-ORNJ4GCN-Bx83s1bJ.js} +1 -1
  26. package/dist/assets/{chunk-OYMX7WX6-syQho5jf.js → chunk-OYMX7WX6-BqRUtRpL.js} +1 -1
  27. package/dist/assets/{chunk-U2HBQHQK-DTJPeU7W.js → chunk-U2HBQHQK-DogcerR6.js} +1 -1
  28. package/dist/assets/{chunk-X2U36JSP-CrTnmMqG.js → chunk-X2U36JSP-CwVWdmZV.js} +1 -1
  29. package/dist/assets/chunk-XPW4576I-DQpNCogT.js +32 -0
  30. package/dist/assets/{chunk-YZCP3GAM-9wq0QKUn.js → chunk-YZCP3GAM-crQSbji9.js} +1 -1
  31. package/dist/assets/{chunk-ZZ45TVLE-D3I1kLlo.js → chunk-ZZ45TVLE-Bk1S1YtS.js} +1 -1
  32. package/dist/assets/classDiagram-6PBFFD2Q-B_TabGaU.js +1 -0
  33. package/dist/assets/classDiagram-v2-HSJHXN6E-CGnZkUWw.js +1 -0
  34. package/dist/assets/clone-D4ka472w.js +1 -0
  35. package/dist/assets/{cose-bilkent-S5V4N54A-BygGvZGW.js → cose-bilkent-S5V4N54A-RBTHUit8.js} +1 -1
  36. package/dist/assets/cytoscape.esm-BGJwlmkf.js +321 -0
  37. package/dist/assets/dagre-B32eYLtm.js +1 -0
  38. package/dist/assets/{dagre-KV5264BT-BBqulDtd.js → dagre-KV5264BT-nX7tuXXn.js} +1 -1
  39. package/dist/assets/diagram-5BDNPKRD-DRxMXlQr.js +10 -0
  40. package/dist/assets/diagram-G4DWMVQ6-CoojevGm.js +24 -0
  41. package/dist/assets/diagram-MMDJMWI5-CWtJyfVW.js +43 -0
  42. package/dist/assets/diagram-TYMM5635-CsDJC4Hq.js +24 -0
  43. package/dist/assets/{erDiagram-SMLLAGMA-BN5eJerP.js → erDiagram-SMLLAGMA-Cf7Xtd9A.js} +2 -2
  44. package/dist/assets/{flatten-C5NL-f24.js → flatten-CYX_pHZ7.js} +1 -1
  45. package/dist/assets/{flowDiagram-DWJPFMVM-CbFskc8S.js → flowDiagram-DWJPFMVM-DQaeR16a.js} +3 -3
  46. package/dist/assets/{ganttDiagram-T4ZO3ILL-OCTvbRxF.js → ganttDiagram-T4ZO3ILL-8EIcztcH.js} +1 -1
  47. package/dist/assets/gitGraph-7Q5UKJZL-BH9A1SAZ.js +1 -0
  48. package/dist/assets/{gitGraphDiagram-UUTBAWPF-wpqI2kyI.js → gitGraphDiagram-UUTBAWPF-DO9ODqYw.js} +1 -1
  49. package/dist/assets/graphlib-bPBqlJKT.js +1 -0
  50. package/dist/assets/identity-Me9aart9.js +1 -0
  51. package/dist/assets/index-oKG1C41_.js +273 -0
  52. package/dist/assets/info-OMHHGYJF-BvKR-zWh.js +1 -0
  53. package/dist/assets/infoDiagram-42DDH7IO-pRTXCm5C.js +2 -0
  54. package/dist/assets/isEmpty-Cu0k-j1j.js +1 -0
  55. package/dist/assets/{ishikawaDiagram-UXIWVN3A-Epc23N_0.js → ishikawaDiagram-UXIWVN3A-BP2YE5QI.js} +2 -2
  56. package/dist/assets/{journeyDiagram-VCZTEJTY-BkMxoaPq.js → journeyDiagram-VCZTEJTY-B3l2juoL.js} +1 -1
  57. package/dist/assets/{kanban-definition-6JOO6SKY-C8dW_26n.js → kanban-definition-6JOO6SKY-BpIpEOZZ.js} +4 -4
  58. package/dist/assets/{line-DNzQATGr.js → line-otOkzGl8.js} +1 -1
  59. package/dist/assets/mermaid-parser.core-xWsW24Gq.js +4 -0
  60. package/dist/assets/{mindmap-definition-QFDTVHPH-CvpNtrKT.js → mindmap-definition-QFDTVHPH-B9khyC7X.js} +3 -3
  61. package/dist/assets/packet-4T2RLAQJ-D8Dw3nmf.js +1 -0
  62. package/dist/assets/pie-ZZUOXDRM-ZghowlAE.js +1 -0
  63. package/dist/assets/{pieDiagram-DEJITSTG-eENymoXZ.js → pieDiagram-DEJITSTG-v32hL3i7.js} +1 -1
  64. package/dist/assets/{quadrantDiagram-34T5L4WZ-c0iZxo2I.js → quadrantDiagram-34T5L4WZ-DIL3GDFt.js} +1 -1
  65. package/dist/assets/radar-PYXPWWZC-D-PK3JOd.js +1 -0
  66. package/dist/assets/reduce-CImcgAcU.js +1 -0
  67. package/dist/assets/{requirementDiagram-MS252O5E-CmRO3hLp.js → requirementDiagram-MS252O5E-D8os2-4y.js} +2 -2
  68. package/dist/assets/{sankeyDiagram-XADWPNL6-woJZoQ58.js → sankeyDiagram-XADWPNL6-BV70D4l5.js} +1 -1
  69. package/dist/assets/{sequenceDiagram-FGHM5R23-B7qNcwNo.js → sequenceDiagram-FGHM5R23-Cwu8hQW1.js} +1 -1
  70. package/dist/assets/stateDiagram-FHFEXIEX-oYUWv7Fb.js +1 -0
  71. package/dist/assets/stateDiagram-v2-QKLJ7IA2-CFUTpFu-.js +1 -0
  72. package/dist/assets/{timeline-definition-GMOUNBTQ-CQWqDPGG.js → timeline-definition-GMOUNBTQ-CxSdKxpL.js} +1 -1
  73. package/dist/assets/treeView-SZITEDCU-uVgaJQzG.js +1 -0
  74. package/dist/assets/treemap-W4RFUUIX-Dcad_9AN.js +1 -0
  75. package/dist/assets/vennDiagram-DHZGUBPP-D4wgD7QI.js +34 -0
  76. package/dist/assets/wardley-RL74JXVD-CFXrK8mx.js +1 -0
  77. package/dist/assets/{wardleyDiagram-NUSXRM2D-DNhPIFCg.js → wardleyDiagram-NUSXRM2D-5Q201ea3.js} +1 -1
  78. package/dist/assets/{xychartDiagram-5P7HB3ND-BDblAZ11.js → xychartDiagram-5P7HB3ND-BPZv_axd.js} +3 -3
  79. package/dist/index.html +16 -12
  80. package/package.json +99 -92
  81. package/scripts/stubs/node-domexception/index.cjs +18 -0
  82. package/scripts/stubs/node-domexception/package.json +7 -0
  83. package/skills/hiro-task-manager-cli/SKILL.md +97 -0
  84. package/skills/hiro-task-manager-cli/reference/boards.md +143 -0
  85. package/skills/hiro-task-manager-cli/reference/cli-access-policy.md +72 -0
  86. package/skills/hiro-task-manager-cli/reference/errors.md +85 -0
  87. package/skills/hiro-task-manager-cli/reference/lists.md +106 -0
  88. package/skills/hiro-task-manager-cli/reference/releases.md +87 -0
  89. package/skills/hiro-task-manager-cli/reference/search.md +38 -0
  90. package/skills/hiro-task-manager-cli/reference/statuses.md +25 -0
  91. package/skills/hiro-task-manager-cli/reference/tasks.md +144 -0
  92. package/skills/hiro-task-manager-cli/reference/trash.md +50 -0
  93. package/src/cli/bootstrap/launcher.test.ts +66 -0
  94. package/src/cli/bootstrap/launcher.ts +389 -35
  95. package/src/cli/bootstrap/program.test.ts +46 -0
  96. package/src/cli/bootstrap/program.ts +54 -1
  97. package/src/cli/bootstrap/runtime.test.ts +15 -0
  98. package/src/cli/bootstrap/runtime.ts +27 -1
  99. package/src/cli/commands/query.ts +56 -56
  100. package/src/cli/commands/server.ts +27 -19
  101. package/src/cli/handlers/boards.test.ts +669 -669
  102. package/src/cli/handlers/cli-wiring.test.ts +1 -1
  103. package/src/cli/handlers/search.test.ts +374 -374
  104. package/src/cli/handlers/search.ts +17 -17
  105. package/src/cli/handlers/server.test.ts +55 -13
  106. package/src/cli/handlers/server.ts +16 -3
  107. package/src/cli/lib/api-client.test.ts +35 -2
  108. package/src/cli/lib/api-client.ts +43 -10
  109. package/src/cli/lib/cli-http-errors.test.ts +85 -85
  110. package/src/cli/lib/command-helpers.ts +161 -154
  111. package/src/cli/lib/config.ts +4 -0
  112. package/src/cli/lib/launcherUi.test.ts +74 -0
  113. package/src/cli/lib/launcherUi.ts +213 -0
  114. package/src/cli/lib/process.test.ts +24 -5
  115. package/src/cli/lib/process.ts +86 -55
  116. package/src/cli/ports/process.ts +8 -2
  117. package/src/cli/subprocess.real-stack.test.ts +611 -598
  118. package/src/cli/subprocess.smoke.test.ts +954 -969
  119. package/src/cli/types/config.ts +2 -6
  120. package/src/client/components/auth/AuthScreen.tsx +3 -3
  121. package/src/client/components/board/BoardStatsChips.tsx +233 -233
  122. package/src/client/components/board/BoardStatsContext.tsx +41 -41
  123. package/src/client/components/board/boardHeaderButtonStyles.ts +38 -38
  124. package/src/client/components/board/shortcuts/useBoardShortcutKeydown.ts +49 -49
  125. package/src/client/components/board/useBoardCanvasPanScroll.ts +108 -108
  126. package/src/client/components/board/useBoardTaskContainerDroppableReact.ts +33 -33
  127. package/src/client/components/board/useBoardTaskSortableReact.ts +26 -26
  128. package/src/client/components/multi-select.tsx +1206 -1206
  129. package/src/client/components/routing/BoardPage.tsx +20 -20
  130. package/src/client/components/routing/NavigationRegistrar.tsx +13 -13
  131. package/src/client/components/settings/SettingsPage.tsx +1 -1
  132. package/src/client/components/task/TaskCard.tsx +643 -643
  133. package/src/client/components/ui/badge.tsx +49 -49
  134. package/src/client/components/ui/button.tsx +65 -65
  135. package/src/client/components/ui/command.tsx +193 -193
  136. package/src/client/components/ui/dialog.tsx +163 -163
  137. package/src/client/components/ui/input-group.tsx +155 -155
  138. package/src/client/components/ui/input.tsx +19 -19
  139. package/src/client/components/ui/popover.tsx +87 -87
  140. package/src/client/components/ui/separator.tsx +28 -28
  141. package/src/client/components/ui/textarea.tsx +18 -18
  142. package/src/client/index.css +248 -248
  143. package/src/client/lib/appNavigate.ts +16 -16
  144. package/src/client/lib/taskCardDate.ts +111 -111
  145. package/src/client/lib/utils.ts +6 -6
  146. package/src/server/auth.ts +351 -302
  147. package/src/server/bootstrapDev.ts +11 -2
  148. package/src/server/bootstrapInstalled.ts +6 -1
  149. package/src/server/index.ts +33 -7
  150. package/src/server/migrations/013_cli_policy_and_provenance.ts +2 -2
  151. package/src/server/migrations/019_cli_global_create_board_default_on.ts +14 -0
  152. package/src/server/migrations/registry.ts +43 -41
  153. package/src/server/parseBootstrapProfile.ts +42 -0
  154. package/src/server/storage/cliPolicy.ts +2 -1
  155. package/src/shared/runtimeConfig.ts +256 -237
  156. package/src/shared/runtimeIdentity.test.ts +47 -0
  157. package/src/shared/runtimeIdentity.ts +35 -0
  158. package/src/shared/serverStatus.ts +21 -0
  159. package/src/shared/skillsInstall.ts +70 -0
  160. package/src/shared/terminalColors.ts +24 -0
  161. package/dist/assets/architecture-YZFGNWBL-3h1eIYfB.js +0 -1
  162. package/dist/assets/architectureDiagram-Q4EWVU46-DSQ1_74_.js +0 -36
  163. package/dist/assets/channel-yBmN_ln0.js +0 -1
  164. package/dist/assets/classDiagram-6PBFFD2Q-CotFZI8-.js +0 -1
  165. package/dist/assets/classDiagram-v2-HSJHXN6E-DAPzeDGn.js +0 -1
  166. package/dist/assets/clone-BRQpYu_n.js +0 -1
  167. package/dist/assets/cytoscape.esm-BIYWHPG0.js +0 -321
  168. package/dist/assets/dagre-rhyPjnsQ.js +0 -1
  169. package/dist/assets/diagram-5BDNPKRD-Ky3EXXj0.js +0 -10
  170. package/dist/assets/diagram-G4DWMVQ6-t7LbT0Uz.js +0 -24
  171. package/dist/assets/diagram-MMDJMWI5-CdnLXEMx.js +0 -43
  172. package/dist/assets/diagram-TYMM5635-CnzTqJBM.js +0 -24
  173. package/dist/assets/gitGraph-7Q5UKJZL-CG8f8JF7.js +0 -1
  174. package/dist/assets/graphlib-COiJG5Qv.js +0 -1
  175. package/dist/assets/identity-D4WOnl_h.js +0 -1
  176. package/dist/assets/index-lyyIVcc_.js +0 -304
  177. package/dist/assets/info-OMHHGYJF-C8_SHoRO.js +0 -1
  178. package/dist/assets/infoDiagram-42DDH7IO-BbvTdpSV.js +0 -2
  179. package/dist/assets/mermaid-parser.core-6Tn8epr_.js +0 -4
  180. package/dist/assets/packet-4T2RLAQJ-BvpAX0kJ.js +0 -1
  181. package/dist/assets/pie-ZZUOXDRM-Ow26Yf-E.js +0 -1
  182. package/dist/assets/radar-PYXPWWZC-e_ron5jQ.js +0 -1
  183. package/dist/assets/reduce-BDOBPIXr.js +0 -1
  184. package/dist/assets/stateDiagram-FHFEXIEX-CYfGMoR8.js +0 -1
  185. package/dist/assets/stateDiagram-v2-QKLJ7IA2-CO1W_n55.js +0 -1
  186. package/dist/assets/treeView-SZITEDCU-DsEr3xeq.js +0 -1
  187. package/dist/assets/treemap-W4RFUUIX-DV7nk2AB.js +0 -1
  188. package/dist/assets/vennDiagram-DHZGUBPP-BjTbuhcb.js +0 -34
  189. package/dist/assets/wardley-RL74JXVD-CrrFU9AE.js +0 -1
  190. /package/dist/assets/{chunk-4BX2VUAB-ean5NKtU.js → chunk-4BX2VUAB-C70mcfQR.js} +0 -0
  191. /package/dist/assets/{chunk-55IACEB6-CvSRyJqy.js → chunk-55IACEB6-CWfnqcLM.js} +0 -0
  192. /package/dist/assets/{chunk-BSJP7CBP-D8kBlJsf.js → chunk-BSJP7CBP-B0LrXV9y.js} +0 -0
  193. /package/dist/assets/{chunk-FMBD7UC4-DrNhFt1N.js → chunk-FMBD7UC4-_mV71Mwu.js} +0 -0
  194. /package/dist/assets/{chunk-QZHKN3VN-Csp3OYJY.js → chunk-QZHKN3VN-t2nrsegL.js} +0 -0
  195. /package/dist/assets/{katex-8mXVa4k3.js → katex-B2dtGfSp.js} +0 -0
  196. /package/dist/assets/{rough.esm-DtEqI08j.js → rough.esm-DEh6Frf9.js} +0 -0
@@ -1,8 +1,13 @@
1
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
1
2
  import path from "node:path";
2
3
  import process from "node:process";
3
4
  import { createInterface } from "node:readline/promises";
4
5
  import { Command } from "commander";
5
- import { ensureRuntimeDirectories } from "../../shared/runtimeConfig";
6
+ import {
7
+ ensureRuntimeDirectories,
8
+ hasAnyProfileConfigOnDisk,
9
+ } from "../../shared/runtimeConfig";
10
+ import { ensureBundledSkills } from "../../shared/skillsInstall";
6
11
  import {
7
12
  getDefaultInstalledAuthDir,
8
13
  getDefaultInstalledDataDir,
@@ -16,8 +21,24 @@ import { parsePortOption } from "../lib/command-helpers";
16
21
  import { CLI_ERR } from "../types/errors";
17
22
  import { CLI_DEFAULTS } from "../lib/constants";
18
23
  import { CliError, exitWithError } from "../lib/output";
19
- import { startServer } from "../lib/process";
24
+ import { readServerStatus, startServer, stopServer } from "../lib/process";
20
25
  import { canPromptInteractively } from "../lib/tty";
26
+ import type { ServerStartMode } from "../ports/process";
27
+ import {
28
+ formatBooleanPrompt,
29
+ formatTextPrompt,
30
+ isAuthInitialized,
31
+ paintValue,
32
+ printSetupContinuePrompt,
33
+ printSetupNextSteps,
34
+ printPassphraseHint,
35
+ printRecoveryKey,
36
+ printRecoveryKeyExitHint,
37
+ printInteractiveSetupHeader,
38
+ printSavedProfileSummary,
39
+ spinForMoment,
40
+ startInlineSpinner,
41
+ } from "../lib/launcherUi";
21
42
 
22
43
  /** Phase 3: installed-app launcher logic (formerly all of app.ts). */
23
44
 
@@ -28,6 +49,50 @@ interface LauncherOptions {
28
49
  profile?: string;
29
50
  }
30
51
 
52
+ interface LauncherServerOptions {
53
+ profile?: string;
54
+ }
55
+
56
+ interface LauncherServerStartOptions extends LauncherServerOptions {
57
+ dataDir?: string;
58
+ foreground?: boolean;
59
+ }
60
+
61
+ export interface LauncherSetupResult {
62
+ config: CliConfigFile;
63
+ setupMeta: {
64
+ /** User went through interactive prompts (not bunx / non-TTY defaults). */
65
+ justFinishedInteractiveSetup: boolean;
66
+ /** No profile had config.json on disk before this run’s save. */
67
+ firstProfileOnMachine: boolean;
68
+ };
69
+ }
70
+
71
+ export function resolveLauncherStartPlan(options: {
72
+ shouldRunSetup: boolean;
73
+ needsRecoveryKeyExitFlow: boolean;
74
+ alreadyRunning: boolean;
75
+ shouldOpenBrowser: boolean;
76
+ preferForegroundWhenNotSetup?: boolean;
77
+ }): {
78
+ startMode: ServerStartMode;
79
+ readyLabel: "Started" | "Already started";
80
+ shouldOpenBrowserOnReady: boolean;
81
+ } {
82
+ return {
83
+ startMode: options.shouldRunSetup
84
+ ? options.needsRecoveryKeyExitFlow
85
+ ? "background-attached"
86
+ : "foreground"
87
+ : options.preferForegroundWhenNotSetup
88
+ ? "foreground"
89
+ : "background",
90
+ readyLabel: options.alreadyRunning ? "Already started" : "Started",
91
+ shouldOpenBrowserOnReady:
92
+ options.shouldOpenBrowser && !options.alreadyRunning,
93
+ };
94
+ }
95
+
31
96
  function parseBrowserMode(browser: string | undefined): boolean | undefined {
32
97
  if (!browser?.trim()) return undefined;
33
98
 
@@ -83,7 +148,7 @@ async function promptWithDefault(
83
148
  output: process.stdout,
84
149
  });
85
150
  try {
86
- const answer = await rl.question(`${question} [${defaultValue}]: `);
151
+ const answer = await rl.question(`${question} `);
87
152
  return answer.trim() || defaultValue;
88
153
  } finally {
89
154
  rl.close();
@@ -94,14 +159,13 @@ async function promptBoolean(
94
159
  question: string,
95
160
  defaultValue: boolean,
96
161
  ): Promise<boolean> {
97
- const label = defaultValue ? "Y/n" : "y/N";
98
162
  const rl = createInterface({
99
163
  input: process.stdin,
100
164
  output: process.stdout,
101
165
  });
102
166
  try {
103
167
  for (;;) {
104
- const answer = (await rl.question(`${question} [${label}]: `))
168
+ const answer = (await rl.question(`${question}: `))
105
169
  .trim()
106
170
  .toLowerCase();
107
171
  if (!answer) return defaultValue;
@@ -119,10 +183,11 @@ async function runLauncherSetup(overrides: {
119
183
  port?: number;
120
184
  dataDir?: string;
121
185
  openBrowser?: boolean;
122
- }): Promise<CliConfigFile> {
186
+ }): Promise<LauncherSetupResult> {
123
187
  const defaults = resolveLauncherDefaults(overrides);
124
188
  const configScope = { profile: overrides.profile, kind: "installed" as const };
125
189
  const existing = readConfigFile(configScope);
190
+ const machineHadNoProfilesBefore = !hasAnyProfileConfigOnDisk();
126
191
 
127
192
  // Keep the first-run path working for bunx and other non-interactive entry
128
193
  // points by saving sane defaults instead of failing on missing prompts.
@@ -134,18 +199,47 @@ async function runLauncherSetup(overrides: {
134
199
  dataDir: config.data_dir,
135
200
  authDir: config.auth_dir,
136
201
  });
137
- return config;
202
+ return {
203
+ config,
204
+ setupMeta: {
205
+ justFinishedInteractiveSetup: false,
206
+ firstProfileOnMachine: machineHadNoProfilesBefore,
207
+ },
208
+ };
138
209
  }
139
210
 
140
- console.log("TaskManager first-run setup");
211
+ printInteractiveSetupHeader({
212
+ profileName: resolveProfileName(configScope),
213
+ firstProfileOnMachine: machineHadNoProfilesBefore,
214
+ });
215
+
216
+ // Show the profile context line first so the user understands why the
217
+ // following prompts are being asked.
218
+ await spinForMoment(
219
+ "Looking for existing profiles...",
220
+ machineHadNoProfilesBefore
221
+ ? `Creating Profile: ${paintValue(resolveProfileName(configScope))}`
222
+ : `Using Profile: ${paintValue(resolveProfileName(configScope))}`,
223
+ );
224
+
225
+ const portValue = await promptWithDefault(
226
+ formatTextPrompt("Pick a port for web/api", String(defaults.port)),
227
+ String(defaults.port),
228
+ );
141
229
 
142
- const portValue = await promptWithDefault("Port", String(defaults.port));
143
230
  const dataDirValue = await promptWithDefault(
144
- "Data directory",
231
+ formatTextPrompt(
232
+ "Pick a Data Directory to place the database",
233
+ defaults.data_dir,
234
+ ),
145
235
  defaults.data_dir,
146
236
  );
237
+
147
238
  const openBrowser = await promptBoolean(
148
- "Open browser automatically",
239
+ formatBooleanPrompt(
240
+ "Open default browser when starting the server with hirotaskmanager",
241
+ defaults.open_browser,
242
+ ),
149
243
  defaults.open_browser,
150
244
  );
151
245
 
@@ -156,14 +250,34 @@ async function runLauncherSetup(overrides: {
156
250
  auth_dir: defaults.auth_dir,
157
251
  open_browser: openBrowser,
158
252
  };
159
- const savedPath = writeConfigFile(config, configScope);
253
+ writeConfigFile(config, configScope);
160
254
  ensureRuntimeDirectories({
161
255
  ...configScope,
162
256
  dataDir: config.data_dir,
163
257
  authDir: config.auth_dir,
164
258
  });
165
- console.log(`Saved launcher config to ${savedPath}`);
166
- return config;
259
+
260
+ await spinForMoment(
261
+ machineHadNoProfilesBefore
262
+ ? `Saving Profile: ${paintValue(resolveProfileName(configScope))}`
263
+ : `Saving Profile: ${paintValue(resolveProfileName(configScope))}`,
264
+ );
265
+
266
+ printSavedProfileSummary({
267
+ created: machineHadNoProfilesBefore,
268
+ profileName: resolveProfileName(configScope),
269
+ appUrl: `http://127.0.0.1:${config.port}`,
270
+ dataDir: path.resolve(config.data_dir!),
271
+ openBrowser,
272
+ });
273
+
274
+ return {
275
+ config,
276
+ setupMeta: {
277
+ justFinishedInteractiveSetup: true,
278
+ firstProfileOnMachine: machineHadNoProfilesBefore,
279
+ },
280
+ };
167
281
  }
168
282
 
169
283
  async function openBrowser(url: string): Promise<void> {
@@ -188,6 +302,48 @@ async function openBrowser(url: string): Promise<void> {
188
302
  }
189
303
  }
190
304
 
305
+ /**
306
+ * Poll for the recovery-key sidecar file written by the server during
307
+ * `setupPassphrase`. Returns the key string once available, then deletes the
308
+ * file so it is never left on disk.
309
+ */
310
+ async function waitForRecoveryKeyFile(authDir: string): Promise<string> {
311
+ const keyPath = path.join(authDir, "recovery-key.tmp");
312
+ while (!existsSync(keyPath)) {
313
+ await Bun.sleep(250);
314
+ }
315
+ const key = readFileSync(keyPath, "utf8").trim();
316
+ try {
317
+ unlinkSync(keyPath);
318
+ } catch {
319
+ // Best-effort cleanup; the file has owner-only perms already.
320
+ }
321
+ return key;
322
+ }
323
+
324
+ async function waitForEnterKey(): Promise<void> {
325
+ const rl = createInterface({
326
+ input: process.stdin,
327
+ output: process.stdout,
328
+ });
329
+ try {
330
+ await rl.question("");
331
+ } finally {
332
+ rl.close();
333
+ }
334
+ }
335
+
336
+ function resolveInstalledLauncherProfile(profile: string | undefined): string {
337
+ return resolveProfileName({
338
+ profile,
339
+ kind: "installed",
340
+ });
341
+ }
342
+
343
+ function printLauncherJson(data: unknown): void {
344
+ process.stdout.write(`${JSON.stringify(data)}\n`);
345
+ }
346
+
191
347
  export function createHirotaskmanagerProgram(): Command {
192
348
  const program = new Command();
193
349
  program
@@ -206,21 +362,46 @@ export function createHirotaskmanagerProgram(): Command {
206
362
  ? path.resolve(options.dataDir.trim())
207
363
  : undefined;
208
364
  const overrideOpenBrowser = parseBrowserMode(options.browser);
209
- const selectedProfile = resolveProfileName({
210
- profile: options.profile,
211
- kind: "installed",
212
- });
365
+ const selectedProfile = resolveInstalledLauncherProfile(options.profile);
213
366
 
214
367
  const shouldRunSetup =
215
368
  options.setup ||
216
369
  !hasCliConfigFile({ profile: selectedProfile, kind: "installed" });
217
- const launcherConfig = shouldRunSetup
370
+
371
+ const setupResult: LauncherSetupResult = shouldRunSetup
218
372
  ? await runLauncherSetup({
219
373
  profile: selectedProfile,
220
374
  dataDir: overrideDataDir,
221
375
  openBrowser: overrideOpenBrowser,
222
376
  })
223
- : readConfigFile({ profile: selectedProfile, kind: "installed" });
377
+ : {
378
+ config: readConfigFile({
379
+ profile: selectedProfile,
380
+ kind: "installed",
381
+ }),
382
+ setupMeta: {
383
+ justFinishedInteractiveSetup: false,
384
+ firstProfileOnMachine: false,
385
+ },
386
+ };
387
+
388
+ // Registry installs can skip lifecycle hooks, so setup itself must
389
+ // print the exact `npx skills` commands users should run next.
390
+ const skillsInstalled = ensureBundledSkills();
391
+ if (shouldRunSetup) {
392
+ printSetupNextSteps({
393
+ profileName: selectedProfile,
394
+ skillsInstalled,
395
+ });
396
+ if (setupResult.setupMeta.justFinishedInteractiveSetup) {
397
+ // Keep setup blocked here so users see the required skills step
398
+ // before the server starts and the browser steals focus.
399
+ printSetupContinuePrompt();
400
+ await waitForEnterKey();
401
+ }
402
+ }
403
+
404
+ const launcherConfig = setupResult.config;
224
405
 
225
406
  const port =
226
407
  launcherConfig.port ?? CLI_DEFAULTS.INSTALLED_DEFAULT_PORT;
@@ -232,32 +413,205 @@ export function createHirotaskmanagerProgram(): Command {
232
413
  kind: "installed",
233
414
  }),
234
415
  );
416
+ const authDir = path.resolve(
417
+ launcherConfig.auth_dir ??
418
+ getDefaultInstalledAuthDir({
419
+ profile: selectedProfile,
420
+ kind: "installed",
421
+ }),
422
+ );
235
423
  const shouldOpenBrowser =
236
424
  overrideOpenBrowser ?? launcherConfig.open_browser ?? true;
237
425
 
426
+ const url = `http://127.0.0.1:${port}`;
427
+ const needsRecoveryKeyExitFlow =
428
+ setupResult.setupMeta.justFinishedInteractiveSetup &&
429
+ !isAuthInitialized(authDir);
430
+ // Keep normal launcher runs non-blocking, and avoid reopening the browser
431
+ // when the launcher is only attaching to an already running profile.
432
+ const alreadyRunning = shouldRunSetup
433
+ ? false
434
+ : (
435
+ await readServerStatus({
436
+ kind: "installed",
437
+ profile: selectedProfile,
438
+ port,
439
+ })
440
+ ).running;
441
+ const startPlan = resolveLauncherStartPlan({
442
+ shouldRunSetup,
443
+ needsRecoveryKeyExitFlow,
444
+ alreadyRunning,
445
+ shouldOpenBrowser,
446
+ });
447
+
448
+ const startupSpinner = startInlineSpinner(
449
+ `${alreadyRunning ? "Checking Server" : "Starting Server"} with profile ${paintValue(selectedProfile)}: ${paintValue(url)}`,
450
+ );
451
+ const previousSilentStartup = process.env.TASKMANAGER_SILENT_STARTUP_LOG;
452
+ // Let the launcher own startup copy so first-time setup stays compact.
453
+ process.env.TASKMANAGER_SILENT_STARTUP_LOG = "1";
454
+
238
455
  let browserHandled = false;
239
- await startServer(
240
- {
456
+ let runningUrl = url;
457
+ try {
458
+ await startServer(
459
+ {
460
+ kind: "installed",
461
+ profile: selectedProfile,
462
+ port,
463
+ dataDir,
464
+ },
465
+ startPlan.startMode,
466
+ async (status) => {
467
+ const finalUrl = status.url;
468
+ runningUrl = finalUrl;
469
+ startupSpinner.stop(
470
+ `${startPlan.readyLabel}, listening at ${paintValue(finalUrl)}`,
471
+ );
472
+
473
+ if (!browserHandled && startPlan.shouldOpenBrowserOnReady) {
474
+ browserHandled = true;
475
+ await openBrowser(finalUrl);
476
+ }
477
+
478
+ if (needsRecoveryKeyExitFlow) {
479
+ await printPassphraseHint();
480
+ }
481
+ },
482
+ );
483
+
484
+ if (needsRecoveryKeyExitFlow) {
485
+ const recoveryKey = await waitForRecoveryKeyFile(authDir);
486
+ printRecoveryKey(recoveryKey);
487
+ printRecoveryKeyExitHint(runningUrl);
488
+ await waitForEnterKey();
489
+ }
490
+ } finally {
491
+ if (previousSilentStartup === undefined) {
492
+ delete process.env.TASKMANAGER_SILENT_STARTUP_LOG;
493
+ } else {
494
+ process.env.TASKMANAGER_SILENT_STARTUP_LOG = previousSilentStartup;
495
+ }
496
+ startupSpinner.stop(null);
497
+ }
498
+ } catch (error) {
499
+ exitWithError(error);
500
+ }
501
+ });
502
+
503
+ const server = program
504
+ .command("server")
505
+ .description("Start, stop, or inspect the installed TaskManager server");
506
+
507
+ server
508
+ .command("start")
509
+ .description("Start the installed TaskManager server")
510
+ .option("--profile <name>", "Launcher profile name for this command")
511
+ .option("--data-dir <path>", "Override the task data directory")
512
+ .option("--foreground", "Run the server in the foreground")
513
+ .action(async (options: LauncherServerStartOptions, command: Command) => {
514
+ try {
515
+ const profile = resolveInstalledLauncherProfile(
516
+ (command.optsWithGlobals() as LauncherServerStartOptions).profile ?? options.profile,
517
+ );
518
+ const overrideDataDir = options.dataDir?.trim()
519
+ ? path.resolve(options.dataDir.trim())
520
+ : undefined;
521
+ const config = readConfigFile({ profile, kind: "installed" });
522
+ const port = config.port ?? CLI_DEFAULTS.INSTALLED_DEFAULT_PORT;
523
+ const dataDir = path.resolve(
524
+ overrideDataDir ??
525
+ config.data_dir ??
526
+ getDefaultInstalledDataDir({ profile, kind: "installed" }),
527
+ );
528
+ const status = await readServerStatus({
529
+ kind: "installed",
530
+ profile,
531
+ port,
532
+ });
533
+ const startPlan = resolveLauncherStartPlan({
534
+ shouldRunSetup: false,
535
+ needsRecoveryKeyExitFlow: false,
536
+ alreadyRunning: status.running,
537
+ shouldOpenBrowser: false,
538
+ preferForegroundWhenNotSetup: options.foreground === true,
539
+ });
540
+ const startupSpinner = startInlineSpinner(
541
+ `${status.running ? "Checking Server" : "Starting Server"} with profile ${paintValue(profile)}: ${paintValue(status.running ? status.url : `http://127.0.0.1:${port}`)}`,
542
+ );
543
+
544
+ try {
545
+ // Launcher `server start` is human-facing, so prefer concise text
546
+ // instead of JSON while still sharing the same server lifecycle path.
547
+ await startServer(
548
+ {
549
+ kind: "installed",
550
+ profile,
551
+ port,
552
+ dataDir,
553
+ },
554
+ startPlan.startMode,
555
+ async (started) => {
556
+ startupSpinner.stop(
557
+ `${startPlan.readyLabel}, listening at ${paintValue(started.url)}`,
558
+ );
559
+ },
560
+ );
561
+ } finally {
562
+ startupSpinner.stop(null);
563
+ }
564
+ } catch (error) {
565
+ exitWithError(error);
566
+ }
567
+ });
568
+
569
+ server
570
+ .command("status")
571
+ .description("Show whether the installed TaskManager server is running")
572
+ .option("--profile <name>", "Launcher profile name for this command")
573
+ .action(async (options: LauncherServerOptions, command: Command) => {
574
+ try {
575
+ const profile = resolveInstalledLauncherProfile(
576
+ (command.optsWithGlobals() as LauncherServerOptions).profile ?? options.profile,
577
+ );
578
+ printLauncherJson(
579
+ await readServerStatus({
241
580
  kind: "installed",
242
- profile: selectedProfile,
243
- port,
244
- dataDir,
245
- },
246
- false,
247
- async (status) => {
248
- const url = status.url ?? `http://127.0.0.1:${port}`;
249
- console.log(`TaskManager running at ${url}`);
250
- if (!browserHandled && shouldOpenBrowser) {
251
- browserHandled = true;
252
- await openBrowser(url);
253
- }
254
- },
581
+ profile,
582
+ }),
255
583
  );
256
584
  } catch (error) {
257
585
  exitWithError(error);
258
586
  }
259
587
  });
260
588
 
589
+ server
590
+ .command("stop")
591
+ .description("Stop a background installed server started for this profile")
592
+ .option("--profile <name>", "Launcher profile name for this command")
593
+ .action(async (options: LauncherServerOptions, command: Command) => {
594
+ try {
595
+ const profile = resolveInstalledLauncherProfile(
596
+ (command.optsWithGlobals() as LauncherServerOptions).profile ?? options.profile,
597
+ );
598
+ const stopSpinner = startInlineSpinner(
599
+ `Stopping Server with profile ${paintValue(profile)}`,
600
+ );
601
+ try {
602
+ await stopServer({
603
+ kind: "installed",
604
+ profile,
605
+ });
606
+ stopSpinner.stop("Server stopped");
607
+ } finally {
608
+ stopSpinner.stop(null);
609
+ }
610
+ } catch (error) {
611
+ exitWithError(error);
612
+ }
613
+ });
614
+
261
615
  return program;
262
616
  }
263
617
 
@@ -0,0 +1,46 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ getHirotmLauncherSetupCommand,
4
+ shouldRequireLauncherSetupForHirotm,
5
+ } from "./program";
6
+
7
+ describe("shouldRequireLauncherSetupForHirotm", () => {
8
+ test("requires launcher setup for installed mode without a saved profile", () => {
9
+ expect(
10
+ shouldRequireLauncherSetupForHirotm({
11
+ runtimeKind: "installed",
12
+ hasInstalledProfileConfig: false,
13
+ }),
14
+ ).toBe(true);
15
+ });
16
+
17
+ test("allows installed mode once the profile config exists", () => {
18
+ expect(
19
+ shouldRequireLauncherSetupForHirotm({
20
+ runtimeKind: "installed",
21
+ hasInstalledProfileConfig: true,
22
+ }),
23
+ ).toBe(false);
24
+ });
25
+
26
+ test("never blocks dev mode on launcher setup", () => {
27
+ expect(
28
+ shouldRequireLauncherSetupForHirotm({
29
+ runtimeKind: "dev",
30
+ hasInstalledProfileConfig: false,
31
+ }),
32
+ ).toBe(false);
33
+ });
34
+ });
35
+
36
+ describe("getHirotmLauncherSetupCommand", () => {
37
+ test("uses the plain launcher command for the default profile", () => {
38
+ expect(getHirotmLauncherSetupCommand("default")).toBe("hirotaskmanager");
39
+ });
40
+
41
+ test("includes the selected profile for named profiles", () => {
42
+ expect(getHirotmLauncherSetupCommand("work")).toBe(
43
+ "hirotaskmanager --profile work",
44
+ );
45
+ });
46
+ });
@@ -8,8 +8,56 @@ import { registerStatusCommands } from "../commands/statuses";
8
8
  import { registerTaskCommands } from "../commands/tasks";
9
9
  import { registerTrashCommands } from "../commands/trash";
10
10
  import { createDefaultCliContext } from "../handlers/context";
11
+ import { hasCliConfigFile, resolveProfileName, resolveRuntimeKind } from "../lib/config";
11
12
  import { syncCliOutputFormatFromGlobals } from "../lib/cliFormat";
12
- import { exitWithError, resetCliOutputFormat } from "../lib/output";
13
+ import { CliError, exitWithError, resetCliOutputFormat } from "../lib/output";
14
+ import { CLI_ERR } from "../types/errors";
15
+
16
+ export function shouldRequireLauncherSetupForHirotm(options: {
17
+ runtimeKind: "installed" | "dev";
18
+ hasInstalledProfileConfig: boolean;
19
+ }): boolean {
20
+ return (
21
+ options.runtimeKind === "installed" &&
22
+ options.hasInstalledProfileConfig === false
23
+ );
24
+ }
25
+
26
+ export function getHirotmLauncherSetupCommand(profileName: string): string {
27
+ return profileName === "default"
28
+ ? "hirotaskmanager"
29
+ : `hirotaskmanager --profile ${profileName}`;
30
+ }
31
+
32
+ function ensureInstalledProfileIsReadyForHirotm(): void {
33
+ const runtimeKind = resolveRuntimeKind();
34
+ const profileName = resolveProfileName({ kind: "installed" });
35
+ const hasInstalledProfileConfig = hasCliConfigFile({
36
+ profile: profileName,
37
+ kind: "installed",
38
+ });
39
+ if (
40
+ !shouldRequireLauncherSetupForHirotm({
41
+ runtimeKind,
42
+ hasInstalledProfileConfig,
43
+ })
44
+ ) {
45
+ return;
46
+ }
47
+
48
+ const setupCommand = getHirotmLauncherSetupCommand(profileName);
49
+ // Package managers can skip lifecycle scripts, so actionable `hirotm`
50
+ // commands must direct users back to the launcher for first-run setup.
51
+ throw new CliError(
52
+ `No installed TaskManager profile found for "${profileName}". Run \`${setupCommand}\` first to create and configure this profile.`,
53
+ 2,
54
+ {
55
+ code: CLI_ERR.missingRequired,
56
+ profile: profileName,
57
+ hint: `Run \`${setupCommand}\` first.`,
58
+ },
59
+ );
60
+ }
13
61
 
14
62
  /**
15
63
  * Build the hirotm Commander program.
@@ -21,6 +69,10 @@ export function createHirotmProgram(): Command {
21
69
  .name("hirotm")
22
70
  .description("TaskManager CLI for local app control and JSON queries")
23
71
  .option("--profile <name>", "Runtime profile name (default: default, dev)")
72
+ .option(
73
+ "--port <port>",
74
+ "HTTP port for the local API (default: from profile config.json)",
75
+ )
24
76
  .option(
25
77
  "--client-name <name>",
26
78
  "Human-friendly client label sent with API requests (for notifications)",
@@ -41,6 +93,7 @@ export function createHirotmProgram(): Command {
41
93
  quiet?: boolean;
42
94
  };
43
95
  syncCliOutputFormatFromGlobals(opts);
96
+ ensureInstalledProfileIsReadyForHirotm();
44
97
  });
45
98
 
46
99
  const ctx = createDefaultCliContext();
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  readClientNameArg,
4
+ readPortArg,
4
5
  readProfileArg,
5
6
  } from "./runtime";
6
7
 
@@ -33,3 +34,17 @@ describe("readProfileArg", () => {
33
34
  expect(readProfileArg([])).toBeUndefined();
34
35
  });
35
36
  });
37
+
38
+ describe("readPortArg", () => {
39
+ test("parses --port value form", () => {
40
+ expect(readPortArg(["hirotm", "--port", "3002", "boards"])).toBe(3002);
41
+ });
42
+
43
+ test("parses --port=value form", () => {
44
+ expect(readPortArg(["--port=4000"])).toBe(4000);
45
+ });
46
+
47
+ test("returns undefined when absent", () => {
48
+ expect(readPortArg(["boards", "list"])).toBeUndefined();
49
+ });
50
+ });