@hiroleague/taskmanager 0.0.1 → 0.0.2

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 (146) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -52
  3. package/dist/assets/architecture-YZFGNWBL-C1MoQeSs.js +1 -0
  4. package/dist/assets/{architectureDiagram-Q4EWVU46-DSQ1_74_.js → architectureDiagram-Q4EWVU46-DUEfvDBu.js} +1 -1
  5. package/dist/assets/{blockDiagram-DXYQGD6D-DfOGNphI.js → blockDiagram-DXYQGD6D-DQzEOPT2.js} +1 -1
  6. package/dist/assets/{chunk-2KRD3SAO-9yt00aGC.js → chunk-2KRD3SAO-C2e-_49I.js} +1 -1
  7. package/dist/assets/{chunk-4TB4RGXK-DF8yJBFl.js → chunk-4TB4RGXK-AZq3s1Dh.js} +1 -1
  8. package/dist/assets/{chunk-67CJDMHE-5wFKo04G.js → chunk-67CJDMHE-B1-M78qu.js} +1 -1
  9. package/dist/assets/{chunk-7N4EOEYR-BRRGX_NC.js → chunk-7N4EOEYR-D7mYFpz-.js} +1 -1
  10. package/dist/assets/{chunk-AA7GKIK3-DUZv_pNI.js → chunk-AA7GKIK3-VWI9k39i.js} +1 -1
  11. package/dist/assets/{chunk-CIAEETIT-mA5aM_d7.js → chunk-CIAEETIT-hnu4zamm.js} +1 -1
  12. package/dist/assets/{chunk-FOC6F5B3-B-cqGCPC.js → chunk-FOC6F5B3-BJsh9nO9.js} +1 -1
  13. package/dist/assets/{chunk-K5T4RW27-BLRDzioh.js → chunk-K5T4RW27-BLIPdXaZ.js} +1 -1
  14. package/dist/assets/{chunk-KGLVRYIC-CTkQSeKy.js → chunk-KGLVRYIC-DvaW2TkT.js} +1 -1
  15. package/dist/assets/{chunk-LIHQZDEY-Cf34Nu3J.js → chunk-LIHQZDEY-CUsM0M11.js} +1 -1
  16. package/dist/assets/{chunk-ORNJ4GCN-D3uXgbay.js → chunk-ORNJ4GCN-CfluNV0_.js} +1 -1
  17. package/dist/assets/{chunk-OYMX7WX6-syQho5jf.js → chunk-OYMX7WX6-CkWzw4JX.js} +1 -1
  18. package/dist/assets/{classDiagram-6PBFFD2Q-CotFZI8-.js → classDiagram-6PBFFD2Q-Dx_f-9b7.js} +1 -1
  19. package/dist/assets/{classDiagram-v2-HSJHXN6E-DAPzeDGn.js → classDiagram-v2-HSJHXN6E-CSfvZ-nt.js} +1 -1
  20. package/dist/assets/clone-CXokakwV.js +1 -0
  21. package/dist/assets/{dagre-rhyPjnsQ.js → dagre-Do0eD9eI.js} +1 -1
  22. package/dist/assets/{dagre-KV5264BT-BBqulDtd.js → dagre-KV5264BT-lveZDhBf.js} +1 -1
  23. package/dist/assets/{diagram-5BDNPKRD-Ky3EXXj0.js → diagram-5BDNPKRD-Dq5yM_uY.js} +1 -1
  24. package/dist/assets/{diagram-G4DWMVQ6-t7LbT0Uz.js → diagram-G4DWMVQ6-D-SYOmKm.js} +1 -1
  25. package/dist/assets/{diagram-MMDJMWI5-CdnLXEMx.js → diagram-MMDJMWI5-lU5t9BZA.js} +1 -1
  26. package/dist/assets/{diagram-TYMM5635-CnzTqJBM.js → diagram-TYMM5635-6tfUbY3R.js} +1 -1
  27. package/dist/assets/{erDiagram-SMLLAGMA-BN5eJerP.js → erDiagram-SMLLAGMA-dx09stuy.js} +1 -1
  28. package/dist/assets/{flatten-C5NL-f24.js → flatten-B2BZ0pzY.js} +1 -1
  29. package/dist/assets/{flowDiagram-DWJPFMVM-CbFskc8S.js → flowDiagram-DWJPFMVM-CJi2WISS.js} +1 -1
  30. package/dist/assets/gitGraph-7Q5UKJZL-BXTuQaDM.js +1 -0
  31. package/dist/assets/{gitGraphDiagram-UUTBAWPF-wpqI2kyI.js → gitGraphDiagram-UUTBAWPF-Bjj94M12.js} +1 -1
  32. package/dist/assets/{graphlib-COiJG5Qv.js → graphlib-BIlXYGdM.js} +1 -1
  33. package/dist/assets/{index-lyyIVcc_.js → index-CZZuue3D.js} +5 -5
  34. package/dist/assets/info-OMHHGYJF-BeeKt8-X.js +1 -0
  35. package/dist/assets/{infoDiagram-42DDH7IO-BbvTdpSV.js → infoDiagram-42DDH7IO-wq_opQKO.js} +1 -1
  36. package/dist/assets/{ishikawaDiagram-UXIWVN3A-Epc23N_0.js → ishikawaDiagram-UXIWVN3A-Cnc1bwBo.js} +1 -1
  37. package/dist/assets/{kanban-definition-6JOO6SKY-C8dW_26n.js → kanban-definition-6JOO6SKY-CwHbIze0.js} +1 -1
  38. package/dist/assets/{mermaid-parser.core-6Tn8epr_.js → mermaid-parser.core-DrLhKJ48.js} +2 -2
  39. package/dist/assets/{mindmap-definition-QFDTVHPH-CvpNtrKT.js → mindmap-definition-QFDTVHPH-DswAJiEd.js} +1 -1
  40. package/dist/assets/packet-4T2RLAQJ-DQ-H9_jd.js +1 -0
  41. package/dist/assets/pie-ZZUOXDRM-BSj0Jsyj.js +1 -0
  42. package/dist/assets/{pieDiagram-DEJITSTG-eENymoXZ.js → pieDiagram-DEJITSTG-DgQTCddl.js} +1 -1
  43. package/dist/assets/radar-PYXPWWZC-B7-oRPFL.js +1 -0
  44. package/dist/assets/{reduce-BDOBPIXr.js → reduce-Uumu9GdR.js} +1 -1
  45. package/dist/assets/{requirementDiagram-MS252O5E-CmRO3hLp.js → requirementDiagram-MS252O5E-D1moa23Z.js} +1 -1
  46. package/dist/assets/{sequenceDiagram-FGHM5R23-B7qNcwNo.js → sequenceDiagram-FGHM5R23-Dvhj7HGn.js} +1 -1
  47. package/dist/assets/{stateDiagram-FHFEXIEX-CYfGMoR8.js → stateDiagram-FHFEXIEX-Dx5CjenB.js} +1 -1
  48. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-CO1W_n55.js → stateDiagram-v2-QKLJ7IA2-C_PkrTdc.js} +1 -1
  49. package/dist/assets/{timeline-definition-GMOUNBTQ-CQWqDPGG.js → timeline-definition-GMOUNBTQ-z-IncVmK.js} +1 -1
  50. package/dist/assets/treeView-SZITEDCU-CFXle9Az.js +1 -0
  51. package/dist/assets/treemap-W4RFUUIX-CAW3vWh8.js +1 -0
  52. package/dist/assets/{vennDiagram-DHZGUBPP-BjTbuhcb.js → vennDiagram-DHZGUBPP-CT1ehozU.js} +1 -1
  53. package/dist/assets/wardley-RL74JXVD-7q3ju4kc.js +1 -0
  54. package/dist/assets/{wardleyDiagram-NUSXRM2D-DNhPIFCg.js → wardleyDiagram-NUSXRM2D-D-kouujI.js} +1 -1
  55. package/dist/assets/{xychartDiagram-5P7HB3ND-BDblAZ11.js → xychartDiagram-5P7HB3ND-D1lnM0pL.js} +1 -1
  56. package/dist/index.html +1 -1
  57. package/package.json +101 -92
  58. package/scripts/postinstall-message.mjs +160 -0
  59. package/scripts/stubs/node-domexception/index.cjs +18 -0
  60. package/scripts/stubs/node-domexception/package.json +7 -0
  61. package/skills/hiro-task-manager-cli/SKILL.md +97 -0
  62. package/skills/hiro-task-manager-cli/reference/boards.md +143 -0
  63. package/skills/hiro-task-manager-cli/reference/cli-access-policy.md +72 -0
  64. package/skills/hiro-task-manager-cli/reference/errors.md +85 -0
  65. package/skills/hiro-task-manager-cli/reference/lists.md +106 -0
  66. package/skills/hiro-task-manager-cli/reference/releases.md +87 -0
  67. package/skills/hiro-task-manager-cli/reference/search.md +38 -0
  68. package/skills/hiro-task-manager-cli/reference/statuses.md +25 -0
  69. package/skills/hiro-task-manager-cli/reference/tasks.md +144 -0
  70. package/skills/hiro-task-manager-cli/reference/trash.md +50 -0
  71. package/src/cli/bootstrap/launcher.test.ts +66 -0
  72. package/src/cli/bootstrap/launcher.ts +375 -35
  73. package/src/cli/bootstrap/program.ts +4 -0
  74. package/src/cli/bootstrap/runtime.test.ts +15 -0
  75. package/src/cli/bootstrap/runtime.ts +27 -1
  76. package/src/cli/commands/query.ts +56 -56
  77. package/src/cli/commands/server.ts +27 -19
  78. package/src/cli/handlers/boards.test.ts +669 -669
  79. package/src/cli/handlers/cli-wiring.test.ts +1 -1
  80. package/src/cli/handlers/search.test.ts +374 -374
  81. package/src/cli/handlers/search.ts +17 -17
  82. package/src/cli/handlers/server.test.ts +55 -13
  83. package/src/cli/handlers/server.ts +16 -3
  84. package/src/cli/lib/api-client.test.ts +35 -2
  85. package/src/cli/lib/api-client.ts +43 -10
  86. package/src/cli/lib/cli-http-errors.test.ts +85 -85
  87. package/src/cli/lib/command-helpers.ts +161 -154
  88. package/src/cli/lib/config.ts +4 -0
  89. package/src/cli/lib/launcherUi.ts +166 -0
  90. package/src/cli/lib/process.test.ts +24 -5
  91. package/src/cli/lib/process.ts +86 -55
  92. package/src/cli/ports/process.ts +8 -2
  93. package/src/cli/subprocess.real-stack.test.ts +611 -598
  94. package/src/cli/subprocess.smoke.test.ts +954 -969
  95. package/src/cli/types/config.ts +2 -6
  96. package/src/client/components/auth/AuthScreen.tsx +3 -3
  97. package/src/client/components/board/BoardStatsChips.tsx +233 -233
  98. package/src/client/components/board/BoardStatsContext.tsx +41 -41
  99. package/src/client/components/board/boardHeaderButtonStyles.ts +38 -38
  100. package/src/client/components/board/shortcuts/useBoardShortcutKeydown.ts +49 -49
  101. package/src/client/components/board/useBoardCanvasPanScroll.ts +108 -108
  102. package/src/client/components/board/useBoardTaskContainerDroppableReact.ts +33 -33
  103. package/src/client/components/board/useBoardTaskSortableReact.ts +26 -26
  104. package/src/client/components/multi-select.tsx +1206 -1206
  105. package/src/client/components/routing/BoardPage.tsx +20 -20
  106. package/src/client/components/routing/NavigationRegistrar.tsx +13 -13
  107. package/src/client/components/settings/SettingsPage.tsx +1 -1
  108. package/src/client/components/task/TaskCard.tsx +643 -643
  109. package/src/client/components/ui/badge.tsx +49 -49
  110. package/src/client/components/ui/button.tsx +65 -65
  111. package/src/client/components/ui/command.tsx +193 -193
  112. package/src/client/components/ui/dialog.tsx +163 -163
  113. package/src/client/components/ui/input-group.tsx +155 -155
  114. package/src/client/components/ui/input.tsx +19 -19
  115. package/src/client/components/ui/popover.tsx +87 -87
  116. package/src/client/components/ui/separator.tsx +28 -28
  117. package/src/client/components/ui/textarea.tsx +18 -18
  118. package/src/client/index.css +248 -248
  119. package/src/client/lib/appNavigate.ts +16 -16
  120. package/src/client/lib/taskCardDate.ts +111 -111
  121. package/src/client/lib/utils.ts +6 -6
  122. package/src/server/auth.ts +351 -302
  123. package/src/server/bootstrapDev.ts +11 -2
  124. package/src/server/bootstrapInstalled.ts +6 -1
  125. package/src/server/index.ts +33 -7
  126. package/src/server/migrations/013_cli_policy_and_provenance.ts +2 -2
  127. package/src/server/migrations/019_cli_global_create_board_default_on.ts +14 -0
  128. package/src/server/migrations/registry.ts +43 -41
  129. package/src/server/parseBootstrapProfile.ts +42 -0
  130. package/src/server/storage/cliPolicy.ts +2 -1
  131. package/src/shared/runtimeConfig.ts +256 -237
  132. package/src/shared/runtimeIdentity.test.ts +47 -0
  133. package/src/shared/runtimeIdentity.ts +35 -0
  134. package/src/shared/serverStatus.ts +21 -0
  135. package/src/shared/skillsInstall.ts +71 -0
  136. package/src/shared/terminalColors.ts +24 -0
  137. package/dist/assets/architecture-YZFGNWBL-3h1eIYfB.js +0 -1
  138. package/dist/assets/clone-BRQpYu_n.js +0 -1
  139. package/dist/assets/gitGraph-7Q5UKJZL-CG8f8JF7.js +0 -1
  140. package/dist/assets/info-OMHHGYJF-C8_SHoRO.js +0 -1
  141. package/dist/assets/packet-4T2RLAQJ-BvpAX0kJ.js +0 -1
  142. package/dist/assets/pie-ZZUOXDRM-Ow26Yf-E.js +0 -1
  143. package/dist/assets/radar-PYXPWWZC-e_ron5jQ.js +0 -1
  144. package/dist/assets/treeView-SZITEDCU-DsEr3xeq.js +0 -1
  145. package/dist/assets/treemap-W4RFUUIX-DV7nk2AB.js +0 -1
  146. package/dist/assets/wardley-RL74JXVD-CrrFU9AE.js +0 -1
@@ -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,22 @@ 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
+ printPassphraseHint,
33
+ printRecoveryKey,
34
+ printRecoveryKeyExitHint,
35
+ printInteractiveSetupHeader,
36
+ printSavedProfileSummary,
37
+ spinForMoment,
38
+ startInlineSpinner,
39
+ } from "../lib/launcherUi";
21
40
 
22
41
  /** Phase 3: installed-app launcher logic (formerly all of app.ts). */
23
42
 
@@ -28,6 +47,50 @@ interface LauncherOptions {
28
47
  profile?: string;
29
48
  }
30
49
 
50
+ interface LauncherServerOptions {
51
+ profile?: string;
52
+ }
53
+
54
+ interface LauncherServerStartOptions extends LauncherServerOptions {
55
+ dataDir?: string;
56
+ foreground?: boolean;
57
+ }
58
+
59
+ export interface LauncherSetupResult {
60
+ config: CliConfigFile;
61
+ setupMeta: {
62
+ /** User went through interactive prompts (not bunx / non-TTY defaults). */
63
+ justFinishedInteractiveSetup: boolean;
64
+ /** No profile had config.json on disk before this run’s save. */
65
+ firstProfileOnMachine: boolean;
66
+ };
67
+ }
68
+
69
+ export function resolveLauncherStartPlan(options: {
70
+ shouldRunSetup: boolean;
71
+ needsRecoveryKeyExitFlow: boolean;
72
+ alreadyRunning: boolean;
73
+ shouldOpenBrowser: boolean;
74
+ preferForegroundWhenNotSetup?: boolean;
75
+ }): {
76
+ startMode: ServerStartMode;
77
+ readyLabel: "Started" | "Already started";
78
+ shouldOpenBrowserOnReady: boolean;
79
+ } {
80
+ return {
81
+ startMode: options.shouldRunSetup
82
+ ? options.needsRecoveryKeyExitFlow
83
+ ? "background-attached"
84
+ : "foreground"
85
+ : options.preferForegroundWhenNotSetup
86
+ ? "foreground"
87
+ : "background",
88
+ readyLabel: options.alreadyRunning ? "Already started" : "Started",
89
+ shouldOpenBrowserOnReady:
90
+ options.shouldOpenBrowser && !options.alreadyRunning,
91
+ };
92
+ }
93
+
31
94
  function parseBrowserMode(browser: string | undefined): boolean | undefined {
32
95
  if (!browser?.trim()) return undefined;
33
96
 
@@ -83,7 +146,7 @@ async function promptWithDefault(
83
146
  output: process.stdout,
84
147
  });
85
148
  try {
86
- const answer = await rl.question(`${question} [${defaultValue}]: `);
149
+ const answer = await rl.question(`${question} `);
87
150
  return answer.trim() || defaultValue;
88
151
  } finally {
89
152
  rl.close();
@@ -94,14 +157,13 @@ async function promptBoolean(
94
157
  question: string,
95
158
  defaultValue: boolean,
96
159
  ): Promise<boolean> {
97
- const label = defaultValue ? "Y/n" : "y/N";
98
160
  const rl = createInterface({
99
161
  input: process.stdin,
100
162
  output: process.stdout,
101
163
  });
102
164
  try {
103
165
  for (;;) {
104
- const answer = (await rl.question(`${question} [${label}]: `))
166
+ const answer = (await rl.question(`${question}: `))
105
167
  .trim()
106
168
  .toLowerCase();
107
169
  if (!answer) return defaultValue;
@@ -119,10 +181,11 @@ async function runLauncherSetup(overrides: {
119
181
  port?: number;
120
182
  dataDir?: string;
121
183
  openBrowser?: boolean;
122
- }): Promise<CliConfigFile> {
184
+ }): Promise<LauncherSetupResult> {
123
185
  const defaults = resolveLauncherDefaults(overrides);
124
186
  const configScope = { profile: overrides.profile, kind: "installed" as const };
125
187
  const existing = readConfigFile(configScope);
188
+ const machineHadNoProfilesBefore = !hasAnyProfileConfigOnDisk();
126
189
 
127
190
  // Keep the first-run path working for bunx and other non-interactive entry
128
191
  // points by saving sane defaults instead of failing on missing prompts.
@@ -134,18 +197,47 @@ async function runLauncherSetup(overrides: {
134
197
  dataDir: config.data_dir,
135
198
  authDir: config.auth_dir,
136
199
  });
137
- return config;
200
+ return {
201
+ config,
202
+ setupMeta: {
203
+ justFinishedInteractiveSetup: false,
204
+ firstProfileOnMachine: machineHadNoProfilesBefore,
205
+ },
206
+ };
138
207
  }
139
208
 
140
- console.log("TaskManager first-run setup");
209
+ printInteractiveSetupHeader({
210
+ profileName: resolveProfileName(configScope),
211
+ firstProfileOnMachine: machineHadNoProfilesBefore,
212
+ });
213
+
214
+ // Show the profile context line first so the user understands why the
215
+ // following prompts are being asked.
216
+ await spinForMoment(
217
+ "Looking for existing profiles...",
218
+ machineHadNoProfilesBefore
219
+ ? `Creating Profile: ${paintValue(resolveProfileName(configScope))}`
220
+ : `Using Profile: ${paintValue(resolveProfileName(configScope))}`,
221
+ );
222
+
223
+ const portValue = await promptWithDefault(
224
+ formatTextPrompt("Pick a port for web/api", String(defaults.port)),
225
+ String(defaults.port),
226
+ );
141
227
 
142
- const portValue = await promptWithDefault("Port", String(defaults.port));
143
228
  const dataDirValue = await promptWithDefault(
144
- "Data directory",
229
+ formatTextPrompt(
230
+ "Pick a Data Directory to place the database",
231
+ defaults.data_dir,
232
+ ),
145
233
  defaults.data_dir,
146
234
  );
235
+
147
236
  const openBrowser = await promptBoolean(
148
- "Open browser automatically",
237
+ formatBooleanPrompt(
238
+ "Open default browser when starting the server with hirotaskmanager",
239
+ defaults.open_browser,
240
+ ),
149
241
  defaults.open_browser,
150
242
  );
151
243
 
@@ -156,14 +248,34 @@ async function runLauncherSetup(overrides: {
156
248
  auth_dir: defaults.auth_dir,
157
249
  open_browser: openBrowser,
158
250
  };
159
- const savedPath = writeConfigFile(config, configScope);
251
+ writeConfigFile(config, configScope);
160
252
  ensureRuntimeDirectories({
161
253
  ...configScope,
162
254
  dataDir: config.data_dir,
163
255
  authDir: config.auth_dir,
164
256
  });
165
- console.log(`Saved launcher config to ${savedPath}`);
166
- return config;
257
+
258
+ await spinForMoment(
259
+ machineHadNoProfilesBefore
260
+ ? `Saving Profile: ${paintValue(resolveProfileName(configScope))}`
261
+ : `Saving Profile: ${paintValue(resolveProfileName(configScope))}`,
262
+ );
263
+
264
+ printSavedProfileSummary({
265
+ created: machineHadNoProfilesBefore,
266
+ profileName: resolveProfileName(configScope),
267
+ appUrl: `http://127.0.0.1:${config.port}`,
268
+ dataDir: path.resolve(config.data_dir!),
269
+ openBrowser,
270
+ });
271
+
272
+ return {
273
+ config,
274
+ setupMeta: {
275
+ justFinishedInteractiveSetup: true,
276
+ firstProfileOnMachine: machineHadNoProfilesBefore,
277
+ },
278
+ };
167
279
  }
168
280
 
169
281
  async function openBrowser(url: string): Promise<void> {
@@ -188,6 +300,48 @@ async function openBrowser(url: string): Promise<void> {
188
300
  }
189
301
  }
190
302
 
303
+ /**
304
+ * Poll for the recovery-key sidecar file written by the server during
305
+ * `setupPassphrase`. Returns the key string once available, then deletes the
306
+ * file so it is never left on disk.
307
+ */
308
+ async function waitForRecoveryKeyFile(authDir: string): Promise<string> {
309
+ const keyPath = path.join(authDir, "recovery-key.tmp");
310
+ while (!existsSync(keyPath)) {
311
+ await Bun.sleep(250);
312
+ }
313
+ const key = readFileSync(keyPath, "utf8").trim();
314
+ try {
315
+ unlinkSync(keyPath);
316
+ } catch {
317
+ // Best-effort cleanup; the file has owner-only perms already.
318
+ }
319
+ return key;
320
+ }
321
+
322
+ async function waitForEnterToExitLauncher(): Promise<void> {
323
+ const rl = createInterface({
324
+ input: process.stdin,
325
+ output: process.stdout,
326
+ });
327
+ try {
328
+ await rl.question("");
329
+ } finally {
330
+ rl.close();
331
+ }
332
+ }
333
+
334
+ function resolveInstalledLauncherProfile(profile: string | undefined): string {
335
+ return resolveProfileName({
336
+ profile,
337
+ kind: "installed",
338
+ });
339
+ }
340
+
341
+ function printLauncherJson(data: unknown): void {
342
+ process.stdout.write(`${JSON.stringify(data)}\n`);
343
+ }
344
+
191
345
  export function createHirotaskmanagerProgram(): Command {
192
346
  const program = new Command();
193
347
  program
@@ -206,21 +360,34 @@ export function createHirotaskmanagerProgram(): Command {
206
360
  ? path.resolve(options.dataDir.trim())
207
361
  : undefined;
208
362
  const overrideOpenBrowser = parseBrowserMode(options.browser);
209
- const selectedProfile = resolveProfileName({
210
- profile: options.profile,
211
- kind: "installed",
212
- });
363
+ const selectedProfile = resolveInstalledLauncherProfile(options.profile);
213
364
 
214
365
  const shouldRunSetup =
215
366
  options.setup ||
216
367
  !hasCliConfigFile({ profile: selectedProfile, kind: "installed" });
217
- const launcherConfig = shouldRunSetup
368
+
369
+ const setupResult: LauncherSetupResult = shouldRunSetup
218
370
  ? await runLauncherSetup({
219
371
  profile: selectedProfile,
220
372
  dataDir: overrideDataDir,
221
373
  openBrowser: overrideOpenBrowser,
222
374
  })
223
- : readConfigFile({ profile: selectedProfile, kind: "installed" });
375
+ : {
376
+ config: readConfigFile({
377
+ profile: selectedProfile,
378
+ kind: "installed",
379
+ }),
380
+ setupMeta: {
381
+ justFinishedInteractiveSetup: false,
382
+ firstProfileOnMachine: false,
383
+ },
384
+ };
385
+
386
+ // Safety net: copy bundled skills to ~/.taskmanager/skills/ if
387
+ // postinstall was skipped (--ignore-scripts, CI, bunx) or stale.
388
+ ensureBundledSkills();
389
+
390
+ const launcherConfig = setupResult.config;
224
391
 
225
392
  const port =
226
393
  launcherConfig.port ?? CLI_DEFAULTS.INSTALLED_DEFAULT_PORT;
@@ -232,32 +399,205 @@ export function createHirotaskmanagerProgram(): Command {
232
399
  kind: "installed",
233
400
  }),
234
401
  );
402
+ const authDir = path.resolve(
403
+ launcherConfig.auth_dir ??
404
+ getDefaultInstalledAuthDir({
405
+ profile: selectedProfile,
406
+ kind: "installed",
407
+ }),
408
+ );
235
409
  const shouldOpenBrowser =
236
410
  overrideOpenBrowser ?? launcherConfig.open_browser ?? true;
237
411
 
412
+ const url = `http://127.0.0.1:${port}`;
413
+ const needsRecoveryKeyExitFlow =
414
+ setupResult.setupMeta.justFinishedInteractiveSetup &&
415
+ !isAuthInitialized(authDir);
416
+ // Keep normal launcher runs non-blocking, and avoid reopening the browser
417
+ // when the launcher is only attaching to an already running profile.
418
+ const alreadyRunning = shouldRunSetup
419
+ ? false
420
+ : (
421
+ await readServerStatus({
422
+ kind: "installed",
423
+ profile: selectedProfile,
424
+ port,
425
+ })
426
+ ).running;
427
+ const startPlan = resolveLauncherStartPlan({
428
+ shouldRunSetup,
429
+ needsRecoveryKeyExitFlow,
430
+ alreadyRunning,
431
+ shouldOpenBrowser,
432
+ });
433
+
434
+ const startupSpinner = startInlineSpinner(
435
+ `${alreadyRunning ? "Checking Server" : "Starting Server"} with profile ${paintValue(selectedProfile)}: ${paintValue(url)}`,
436
+ );
437
+ const previousSilentStartup = process.env.TASKMANAGER_SILENT_STARTUP_LOG;
438
+ // Let the launcher own startup copy so first-time setup stays compact.
439
+ process.env.TASKMANAGER_SILENT_STARTUP_LOG = "1";
440
+
238
441
  let browserHandled = false;
239
- await startServer(
240
- {
442
+ let runningUrl = url;
443
+ try {
444
+ await startServer(
445
+ {
446
+ kind: "installed",
447
+ profile: selectedProfile,
448
+ port,
449
+ dataDir,
450
+ },
451
+ startPlan.startMode,
452
+ async (status) => {
453
+ const finalUrl = status.url;
454
+ runningUrl = finalUrl;
455
+ startupSpinner.stop(
456
+ `${startPlan.readyLabel}, listening at ${paintValue(finalUrl)}`,
457
+ );
458
+
459
+ if (!browserHandled && startPlan.shouldOpenBrowserOnReady) {
460
+ browserHandled = true;
461
+ await openBrowser(finalUrl);
462
+ }
463
+
464
+ if (needsRecoveryKeyExitFlow) {
465
+ await printPassphraseHint();
466
+ }
467
+ },
468
+ );
469
+
470
+ if (needsRecoveryKeyExitFlow) {
471
+ const recoveryKey = await waitForRecoveryKeyFile(authDir);
472
+ printRecoveryKey(recoveryKey);
473
+ printRecoveryKeyExitHint(runningUrl);
474
+ await waitForEnterToExitLauncher();
475
+ }
476
+ } finally {
477
+ if (previousSilentStartup === undefined) {
478
+ delete process.env.TASKMANAGER_SILENT_STARTUP_LOG;
479
+ } else {
480
+ process.env.TASKMANAGER_SILENT_STARTUP_LOG = previousSilentStartup;
481
+ }
482
+ startupSpinner.stop(null);
483
+ }
484
+ } catch (error) {
485
+ exitWithError(error);
486
+ }
487
+ });
488
+
489
+ const server = program
490
+ .command("server")
491
+ .description("Start, stop, or inspect the installed TaskManager server");
492
+
493
+ server
494
+ .command("start")
495
+ .description("Start the installed TaskManager server")
496
+ .option("--profile <name>", "Launcher profile name for this command")
497
+ .option("--data-dir <path>", "Override the task data directory")
498
+ .option("--foreground", "Run the server in the foreground")
499
+ .action(async (options: LauncherServerStartOptions, command: Command) => {
500
+ try {
501
+ const profile = resolveInstalledLauncherProfile(
502
+ (command.optsWithGlobals() as LauncherServerStartOptions).profile ?? options.profile,
503
+ );
504
+ const overrideDataDir = options.dataDir?.trim()
505
+ ? path.resolve(options.dataDir.trim())
506
+ : undefined;
507
+ const config = readConfigFile({ profile, kind: "installed" });
508
+ const port = config.port ?? CLI_DEFAULTS.INSTALLED_DEFAULT_PORT;
509
+ const dataDir = path.resolve(
510
+ overrideDataDir ??
511
+ config.data_dir ??
512
+ getDefaultInstalledDataDir({ profile, kind: "installed" }),
513
+ );
514
+ const status = await readServerStatus({
515
+ kind: "installed",
516
+ profile,
517
+ port,
518
+ });
519
+ const startPlan = resolveLauncherStartPlan({
520
+ shouldRunSetup: false,
521
+ needsRecoveryKeyExitFlow: false,
522
+ alreadyRunning: status.running,
523
+ shouldOpenBrowser: false,
524
+ preferForegroundWhenNotSetup: options.foreground === true,
525
+ });
526
+ const startupSpinner = startInlineSpinner(
527
+ `${status.running ? "Checking Server" : "Starting Server"} with profile ${paintValue(profile)}: ${paintValue(status.running ? status.url : `http://127.0.0.1:${port}`)}`,
528
+ );
529
+
530
+ try {
531
+ // Launcher `server start` is human-facing, so prefer concise text
532
+ // instead of JSON while still sharing the same server lifecycle path.
533
+ await startServer(
534
+ {
535
+ kind: "installed",
536
+ profile,
537
+ port,
538
+ dataDir,
539
+ },
540
+ startPlan.startMode,
541
+ async (started) => {
542
+ startupSpinner.stop(
543
+ `${startPlan.readyLabel}, listening at ${paintValue(started.url)}`,
544
+ );
545
+ },
546
+ );
547
+ } finally {
548
+ startupSpinner.stop(null);
549
+ }
550
+ } catch (error) {
551
+ exitWithError(error);
552
+ }
553
+ });
554
+
555
+ server
556
+ .command("status")
557
+ .description("Show whether the installed TaskManager server is running")
558
+ .option("--profile <name>", "Launcher profile name for this command")
559
+ .action(async (options: LauncherServerOptions, command: Command) => {
560
+ try {
561
+ const profile = resolveInstalledLauncherProfile(
562
+ (command.optsWithGlobals() as LauncherServerOptions).profile ?? options.profile,
563
+ );
564
+ printLauncherJson(
565
+ await readServerStatus({
241
566
  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
- },
567
+ profile,
568
+ }),
255
569
  );
256
570
  } catch (error) {
257
571
  exitWithError(error);
258
572
  }
259
573
  });
260
574
 
575
+ server
576
+ .command("stop")
577
+ .description("Stop a background installed server started for this profile")
578
+ .option("--profile <name>", "Launcher profile name for this command")
579
+ .action(async (options: LauncherServerOptions, command: Command) => {
580
+ try {
581
+ const profile = resolveInstalledLauncherProfile(
582
+ (command.optsWithGlobals() as LauncherServerOptions).profile ?? options.profile,
583
+ );
584
+ const stopSpinner = startInlineSpinner(
585
+ `Stopping Server with profile ${paintValue(profile)}`,
586
+ );
587
+ try {
588
+ await stopServer({
589
+ kind: "installed",
590
+ profile,
591
+ });
592
+ stopSpinner.stop("Server stopped");
593
+ } finally {
594
+ stopSpinner.stop(null);
595
+ }
596
+ } catch (error) {
597
+ exitWithError(error);
598
+ }
599
+ });
600
+
261
601
  return program;
262
602
  }
263
603
 
@@ -21,6 +21,10 @@ export function createHirotmProgram(): Command {
21
21
  .name("hirotm")
22
22
  .description("TaskManager CLI for local app control and JSON queries")
23
23
  .option("--profile <name>", "Runtime profile name (default: default, dev)")
24
+ .option(
25
+ "--port <port>",
26
+ "HTTP port for the local API (default: from profile config.json)",
27
+ )
24
28
  .option(
25
29
  "--client-name <name>",
26
30
  "Human-friendly client label sent with API requests (for notifications)",
@@ -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
+ });
@@ -1,5 +1,5 @@
1
1
  import { setRuntimeCliClientName } from "../lib/clientIdentity";
2
- import { setRuntimeProfile } from "../lib/config";
2
+ import { setRuntimeCliPort, setRuntimeKind, setRuntimeProfile } from "../lib/config";
3
3
 
4
4
  /**
5
5
  * Parse argv before Commander runs so profile and client name match this invocation
@@ -33,7 +33,33 @@ export function readProfileArg(argv: string[]): string | undefined {
33
33
  return undefined;
34
34
  }
35
35
 
36
+ export function readPortArg(argv: string[]): number | undefined {
37
+ for (let index = 0; index < argv.length; index += 1) {
38
+ const current = argv[index];
39
+ if (current === "--port") {
40
+ const next = argv[index + 1];
41
+ if (typeof next === "string") {
42
+ const parsed = Number(next);
43
+ if (Number.isInteger(parsed) && parsed > 0) return parsed;
44
+ }
45
+ }
46
+ if (current.startsWith("--port=")) {
47
+ const parsed = Number(current.slice("--port=".length));
48
+ if (Number.isInteger(parsed) && parsed > 0) return parsed;
49
+ }
50
+ }
51
+ return undefined;
52
+ }
53
+
54
+ export function readDevFlag(argv: string[]): boolean {
55
+ return argv.includes("--dev");
56
+ }
57
+
36
58
  export function applyCliRuntimeFromArgv(argv: string[]): void {
37
59
  setRuntimeCliClientName(readClientNameArg(argv));
38
60
  setRuntimeProfile(readProfileArg(argv));
61
+ setRuntimeCliPort(readPortArg(argv));
62
+ if (readDevFlag(argv)) {
63
+ setRuntimeKind("dev");
64
+ }
39
65
  }