@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,17 +1,17 @@
1
- import { runSearch } from "../lib/read/search";
2
- import type { CliContext } from "./context";
3
-
4
- export async function handleSearch(
5
- ctx: CliContext,
6
- queryParts: string[],
7
- options: {
8
- board?: string;
9
- limit?: string;
10
- offset?: string;
11
- noPrefix?: boolean;
12
- pageAll?: boolean;
13
- fields?: string;
14
- },
15
- ): Promise<void> {
16
- await runSearch(ctx, queryParts, options);
17
- }
1
+ import { runSearch } from "../lib/read/search";
2
+ import type { CliContext } from "./context";
3
+
4
+ export async function handleSearch(
5
+ ctx: CliContext,
6
+ queryParts: string[],
7
+ options: {
8
+ board?: string;
9
+ limit?: string;
10
+ offset?: string;
11
+ noPrefix?: boolean;
12
+ pageAll?: boolean;
13
+ fields?: string;
14
+ },
15
+ ): Promise<void> {
16
+ await runSearch(ctx, queryParts, options);
17
+ }
@@ -4,6 +4,8 @@ import { createTestCliRuntime } from "../lib/runtime";
4
4
  import { createDefaultCliContext } from "./context";
5
5
  import { handleServerStart, handleServerStatus, handleServerStop } from "./server";
6
6
  import type { CliContext } from "./context";
7
+ import type { ServerStartMode } from "../ports/process";
8
+ import type { RunningServerStatus } from "../../shared/serverStatus";
7
9
 
8
10
  function baseCtx(overrides: Partial<CliContext> = {}): CliContext {
9
11
  return {
@@ -19,7 +21,14 @@ function baseCtx(overrides: Partial<CliContext> = {}): CliContext {
19
21
 
20
22
  describe("handleServerStatus", () => {
21
23
  test("prints status from readServerStatus", async () => {
22
- const status = { running: true, pid: 42 };
24
+ const status = {
25
+ pid: 42,
26
+ port: 3020,
27
+ running: true,
28
+ runtime: "installed",
29
+ source: "installed",
30
+ url: "http://127.0.0.1:3020",
31
+ } satisfies RunningServerStatus;
23
32
  let printed: unknown;
24
33
  const ctx = baseCtx({
25
34
  printJson: (d) => {
@@ -37,15 +46,22 @@ describe("handleServerStatus", () => {
37
46
  describe("handleServerStart / handleServerStop", () => {
38
47
  test("background start passes dataDir + port and prints JSON status", async () => {
39
48
  let startArgs: ConfigOverrides | undefined;
40
- let backgroundFlag: boolean | undefined;
41
- const status = { running: true, pid: 99, port: 3040 };
49
+ let startMode: ServerStartMode | undefined;
50
+ const status = {
51
+ pid: 99,
52
+ port: 3040,
53
+ running: true,
54
+ runtime: "installed",
55
+ source: "installed",
56
+ url: "http://127.0.0.1:3040",
57
+ } satisfies RunningServerStatus;
42
58
  let printed: unknown;
43
59
  const ctx = baseCtx({
44
60
  resolveDataDir: () => "/custom/data",
45
61
  resolvePort: () => 3040,
46
- startServer: async (opts, bg) => {
62
+ startServer: async (opts, mode) => {
47
63
  startArgs = opts;
48
- backgroundFlag = bg;
64
+ startMode = mode;
49
65
  return status;
50
66
  },
51
67
  printJson: (d) => {
@@ -59,26 +75,52 @@ describe("handleServerStart / handleServerStop", () => {
59
75
  dataDir: "/custom/data",
60
76
  port: 3040,
61
77
  } satisfies ConfigOverrides);
62
- expect(backgroundFlag).toBe(true);
78
+ expect(startMode).toBe("background");
63
79
  expect(printed).toEqual(status);
64
80
  });
65
81
 
66
- test("foreground start awaits startServer with background false", async () => {
67
- let backgroundFlag: boolean | undefined;
82
+ test("default start uses background mode", async () => {
83
+ let startMode: ServerStartMode | undefined;
84
+ let printed: unknown;
68
85
  const ctx = baseCtx({
69
- startServer: async (_opts, bg) => {
70
- backgroundFlag = bg;
71
- return { running: false };
86
+ startServer: async (_opts, mode) => {
87
+ startMode = mode;
88
+ return {
89
+ pid: 88,
90
+ port: 3020,
91
+ running: true,
92
+ runtime: "installed",
93
+ source: "installed",
94
+ url: "http://127.0.0.1:3020",
95
+ } satisfies RunningServerStatus;
96
+ },
97
+ printJson: (d) => {
98
+ printed = d;
72
99
  },
73
100
  });
74
101
 
75
102
  await handleServerStart(ctx, {});
76
103
 
77
- expect(backgroundFlag).toBe(false);
104
+ expect(startMode).toBe("background");
105
+ expect(printed).toBeTruthy();
106
+ });
107
+
108
+ test("foreground start awaits startServer with foreground mode", async () => {
109
+ let startMode: ServerStartMode | undefined;
110
+ const ctx = baseCtx({
111
+ startServer: async (_opts, mode) => {
112
+ startMode = mode;
113
+ return { running: false as const };
114
+ },
115
+ });
116
+
117
+ await handleServerStart(ctx, { foreground: true });
118
+
119
+ expect(startMode).toBe("foreground");
78
120
  });
79
121
 
80
122
  test("stop prints status from stopServer", async () => {
81
- const status = { running: false };
123
+ const status = { running: false as const };
82
124
  let printed: unknown;
83
125
  const ctx = baseCtx({
84
126
  stopServer: async () => status,
@@ -1,23 +1,36 @@
1
+ import { CLI_ERR } from "../types/errors";
2
+ import { CliError } from "../lib/output";
1
3
  import type { CliContext } from "./context";
2
4
 
3
5
  export async function handleServerStart(
4
6
  ctx: CliContext,
5
7
  options: {
6
8
  background?: boolean;
9
+ foreground?: boolean;
7
10
  dataDir?: string;
8
11
  },
9
12
  ): Promise<void> {
10
13
  const port = ctx.resolvePort();
11
14
  const dataDir = ctx.resolveDataDir({ dataDir: options.dataDir });
12
15
 
13
- if (options.background) {
14
- const status = await ctx.startServer({ dataDir, port }, true);
16
+ if (options.background && options.foreground) {
17
+ throw new CliError("Choose either --background or --foreground", 2, {
18
+ code: CLI_ERR.invalidValue,
19
+ });
20
+ }
21
+
22
+ // Default to background so `hirotm server start` stays script/agent-friendly
23
+ // unless the caller explicitly asks to keep logs attached.
24
+ const startMode = options.foreground ? "foreground" : "background";
25
+
26
+ if (startMode === "background") {
27
+ const status = await ctx.startServer({ dataDir, port }, startMode);
15
28
  ctx.printJson(status);
16
29
  return;
17
30
  }
18
31
 
19
32
  // Run in production mode so the installed CLI uses a stable home data directory by default.
20
- await ctx.startServer({ dataDir, port }, false);
33
+ await ctx.startServer({ dataDir, port }, startMode);
21
34
  }
22
35
 
23
36
  export async function handleServerStatus(ctx: CliContext): Promise<void> {
@@ -5,6 +5,7 @@ import {
5
5
  fetchApiMutate,
6
6
  fetchApiTrashMutate,
7
7
  fetchHealth,
8
+ fetchHealthStatus,
8
9
  } from "./api-client";
9
10
  import { CliError } from "./output";
10
11
 
@@ -200,7 +201,14 @@ describe("api-client (mock fetch)", () => {
200
201
 
201
202
  test("fetchHealth true when ok JSON", async () => {
202
203
  setMockFetch(async () =>
203
- new Response(JSON.stringify({ ok: true }), {
204
+ new Response(JSON.stringify({
205
+ pid: 4321,
206
+ port: 20000,
207
+ running: true,
208
+ runtime: "installed",
209
+ source: "installed",
210
+ url: "http://127.0.0.1:20000",
211
+ }), {
204
212
  status: 200,
205
213
  headers: { "content-type": "application/json" },
206
214
  }),
@@ -209,9 +217,34 @@ describe("api-client (mock fetch)", () => {
209
217
  await expect(fetchHealth({ port: 20000 })).resolves.toBe(true);
210
218
  });
211
219
 
220
+ test("fetchHealthStatus returns runtime/source metadata when provided", async () => {
221
+ setMockFetch(async () =>
222
+ new Response(JSON.stringify({
223
+ pid: 12345,
224
+ port: 20000,
225
+ running: true,
226
+ runtime: "installed",
227
+ source: "repo",
228
+ url: "http://127.0.0.1:20000",
229
+ }), {
230
+ status: 200,
231
+ headers: { "content-type": "application/json" },
232
+ }),
233
+ );
234
+
235
+ await expect(fetchHealthStatus({ port: 20000 })).resolves.toEqual({
236
+ pid: 12345,
237
+ port: 20000,
238
+ running: true,
239
+ runtime: "installed",
240
+ source: "repo",
241
+ url: "http://127.0.0.1:20000",
242
+ });
243
+ });
244
+
212
245
  test("fetchHealth false when not ok", async () => {
213
246
  setMockFetch(async () =>
214
- new Response(JSON.stringify({ ok: false }), {
247
+ new Response(JSON.stringify({ running: false }), {
215
248
  status: 200,
216
249
  headers: { "content-type": "application/json" },
217
250
  }),
@@ -23,6 +23,7 @@ import { CLI_DEFAULTS } from "./constants";
23
23
  import { CLI_ERR } from "../types/errors";
24
24
  import { mapHttpStatusToCliFailure } from "./cli-http-errors";
25
25
  import { CliError } from "./output";
26
+ import type { RunningServerStatus } from "../../shared/serverStatus";
26
27
 
27
28
  /** Uses `CLI_DEFAULTS.API_FETCH_TIMEOUT_MS`; health polling uses short waits in `process.ts`. */
28
29
  // Aligns with docs/cli-error-handling.md: timeouts surface as exit 7 + code request_timeout.
@@ -54,16 +55,16 @@ function buildBaseUrl(overrides: ConfigOverrides = {}): string {
54
55
  }
55
56
 
56
57
  function buildStartCommand(overrides: ConfigOverrides = {}): string {
57
- const command = ["hirotm", "server", "start", "--background"];
58
+ const command = ["hirotm", "server", "start"];
58
59
  const profile = resolveProfileName(overrides);
59
60
  const runtime = resolveRuntimeKind(overrides);
60
61
 
61
- if (runtime === "dev" || profile !== "default") {
62
+ if (profile !== "default") {
62
63
  command.push("--profile", profile);
63
64
  }
64
-
65
- // Port is not a CLI flag; it comes from profile config or TASKMANAGER_PORT. The shell
66
- // inherits env when copy-pasting this command, so overrides still apply.
65
+ if (runtime === "dev") {
66
+ command.push("--dev");
67
+ }
67
68
 
68
69
  return command.join(" ");
69
70
  }
@@ -177,6 +178,8 @@ async function apiRequest<T>(
177
178
  return (await response.json()) as T;
178
179
  }
179
180
 
181
+ export type HealthStatus = RunningServerStatus;
182
+
180
183
  export async function fetchApi<T>(
181
184
  endpoint: string,
182
185
  overrides: ConfigOverrides = {},
@@ -211,17 +214,47 @@ export async function fetchApiTrashMutate<T>(
211
214
  });
212
215
  }
213
216
 
214
- export async function fetchHealth(overrides: ConfigOverrides = {}): Promise<boolean> {
217
+ export async function fetchHealthStatus(
218
+ overrides: ConfigOverrides = {},
219
+ ): Promise<HealthStatus | null> {
215
220
  const baseUrl = buildBaseUrl(overrides);
216
221
 
217
222
  try {
218
223
  const response = await fetch(`${baseUrl}/api/health`, {
219
224
  headers: taskManagerClientHeaders(),
220
225
  });
221
- if (!response.ok) return false;
222
- const body = (await response.json()) as { ok?: unknown };
223
- return body.ok === true;
226
+ if (!response.ok) return null;
227
+ const body = (await response.json()) as {
228
+ pid?: unknown;
229
+ port?: unknown;
230
+ running?: unknown;
231
+ runtime?: unknown;
232
+ source?: unknown;
233
+ url?: unknown;
234
+ };
235
+ if (
236
+ typeof body.pid !== "number" ||
237
+ typeof body.port !== "number" ||
238
+ body.running !== true ||
239
+ (body.runtime !== "dev" && body.runtime !== "installed") ||
240
+ (body.source !== "repo" && body.source !== "installed") ||
241
+ typeof body.url !== "string"
242
+ ) {
243
+ return null;
244
+ }
245
+ return {
246
+ pid: body.pid,
247
+ port: body.port,
248
+ running: true,
249
+ runtime: body.runtime,
250
+ source: body.source,
251
+ url: body.url,
252
+ };
224
253
  } catch {
225
- return false;
254
+ return null;
226
255
  }
227
256
  }
257
+
258
+ export async function fetchHealth(overrides: ConfigOverrides = {}): Promise<boolean> {
259
+ return (await fetchHealthStatus(overrides))?.running === true;
260
+ }
@@ -1,85 +1,85 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { CLI_ERR } from "../types/errors";
3
- import { enrichNotFoundError, mapHttpStatusToCliFailure } from "./cli-http-errors";
4
- import { CliError } from "./output";
5
-
6
- describe("mapHttpStatusToCliFailure", () => {
7
- test.each([
8
- [400, CLI_ERR.badRequest, 9, undefined],
9
- [401, CLI_ERR.unauthenticated, 10, undefined],
10
- [403, CLI_ERR.forbidden, 4, undefined],
11
- [404, CLI_ERR.notFound, 3, undefined],
12
- [408, CLI_ERR.requestTimeout, 7, true],
13
- [409, CLI_ERR.conflict, 5, undefined],
14
- [422, CLI_ERR.badRequest, 9, undefined],
15
- [426, CLI_ERR.versionMismatch, 8, undefined],
16
- [429, CLI_ERR.rateLimited, 1, true],
17
- [502, CLI_ERR.internalError, 1, true],
18
- [418, CLI_ERR.httpError, 1, undefined],
19
- ] as const)(
20
- "status %i → code %s exit %i retryable %j",
21
- (status, code, exit, retryable) => {
22
- const { exitCode, details } = mapHttpStatusToCliFailure(status, {
23
- status,
24
- url: "http://127.0.0.1:1/api/x",
25
- });
26
- expect(exitCode).toBe(exit);
27
- expect(details.code).toBe(code);
28
- if (retryable === undefined) {
29
- expect(details.retryable).toBeUndefined();
30
- } else {
31
- expect(details.retryable).toBe(retryable);
32
- }
33
- },
34
- );
35
-
36
- test("599 maps to internal_error (5xx branch)", () => {
37
- const { exitCode, details } = mapHttpStatusToCliFailure(599, { status: 599 });
38
- expect(exitCode).toBe(1);
39
- expect(details.code).toBe(CLI_ERR.internalError);
40
- expect(details.retryable).toBe(true);
41
- });
42
-
43
- test("preserves API code as serverCode, not duplicate top-level code", () => {
44
- const { details } = mapHttpStatusToCliFailure(400, {
45
- code: "CUSTOM",
46
- status: 400,
47
- });
48
- expect(details.code).toBe(CLI_ERR.badRequest);
49
- expect(details.serverCode).toBe("CUSTOM");
50
- expect((details as Record<string, unknown>).code).toBe(CLI_ERR.badRequest);
51
- });
52
-
53
- test("non-string code in body is ignored for serverCode", () => {
54
- const { details } = mapHttpStatusToCliFailure(404, {
55
- code: 123,
56
- status: 404,
57
- } as Record<string, unknown>);
58
- expect(details.serverCode).toBeUndefined();
59
- });
60
- });
61
-
62
- describe("enrichNotFoundError", () => {
63
- test("merges context when details.code is not_found", () => {
64
- const err = new CliError("Whatever", 3, { code: CLI_ERR.notFound });
65
- try {
66
- enrichNotFoundError(err, { board: "my-board", taskId: 9 });
67
- expect.unreachable();
68
- } catch (e) {
69
- expect(e).toBeInstanceOf(CliError);
70
- const c = e as CliError;
71
- expect(c.message).toBe("Whatever");
72
- expect(c.exitCode).toBe(3);
73
- expect(c.details).toMatchObject({
74
- code: CLI_ERR.notFound,
75
- board: "my-board",
76
- taskId: 9,
77
- });
78
- }
79
- });
80
-
81
- test("rethrows unchanged when code is not not_found", () => {
82
- const err = new CliError("Nope", 4, { code: CLI_ERR.forbidden });
83
- expect(() => enrichNotFoundError(err, { board: "x" })).toThrow(err);
84
- });
85
- });
1
+ import { describe, expect, test } from "bun:test";
2
+ import { CLI_ERR } from "../types/errors";
3
+ import { enrichNotFoundError, mapHttpStatusToCliFailure } from "./cli-http-errors";
4
+ import { CliError } from "./output";
5
+
6
+ describe("mapHttpStatusToCliFailure", () => {
7
+ test.each([
8
+ [400, CLI_ERR.badRequest, 9, undefined],
9
+ [401, CLI_ERR.unauthenticated, 10, undefined],
10
+ [403, CLI_ERR.forbidden, 4, undefined],
11
+ [404, CLI_ERR.notFound, 3, undefined],
12
+ [408, CLI_ERR.requestTimeout, 7, true],
13
+ [409, CLI_ERR.conflict, 5, undefined],
14
+ [422, CLI_ERR.badRequest, 9, undefined],
15
+ [426, CLI_ERR.versionMismatch, 8, undefined],
16
+ [429, CLI_ERR.rateLimited, 1, true],
17
+ [502, CLI_ERR.internalError, 1, true],
18
+ [418, CLI_ERR.httpError, 1, undefined],
19
+ ] as const)(
20
+ "status %i → code %s exit %i retryable %j",
21
+ (status, code, exit, retryable) => {
22
+ const { exitCode, details } = mapHttpStatusToCliFailure(status, {
23
+ status,
24
+ url: "http://127.0.0.1:1/api/x",
25
+ });
26
+ expect(exitCode).toBe(exit);
27
+ expect(details.code).toBe(code);
28
+ if (retryable === undefined) {
29
+ expect(details.retryable).toBeUndefined();
30
+ } else {
31
+ expect(details.retryable).toBe(retryable);
32
+ }
33
+ },
34
+ );
35
+
36
+ test("599 maps to internal_error (5xx branch)", () => {
37
+ const { exitCode, details } = mapHttpStatusToCliFailure(599, { status: 599 });
38
+ expect(exitCode).toBe(1);
39
+ expect(details.code).toBe(CLI_ERR.internalError);
40
+ expect(details.retryable).toBe(true);
41
+ });
42
+
43
+ test("preserves API code as serverCode, not duplicate top-level code", () => {
44
+ const { details } = mapHttpStatusToCliFailure(400, {
45
+ code: "CUSTOM",
46
+ status: 400,
47
+ });
48
+ expect(details.code).toBe(CLI_ERR.badRequest);
49
+ expect(details.serverCode).toBe("CUSTOM");
50
+ expect((details as Record<string, unknown>).code).toBe(CLI_ERR.badRequest);
51
+ });
52
+
53
+ test("non-string code in body is ignored for serverCode", () => {
54
+ const { details } = mapHttpStatusToCliFailure(404, {
55
+ code: 123,
56
+ status: 404,
57
+ } as Record<string, unknown>);
58
+ expect(details.serverCode).toBeUndefined();
59
+ });
60
+ });
61
+
62
+ describe("enrichNotFoundError", () => {
63
+ test("merges context when details.code is not_found", () => {
64
+ const err = new CliError("Whatever", 3, { code: CLI_ERR.notFound });
65
+ try {
66
+ enrichNotFoundError(err, { board: "my-board", taskId: 9 });
67
+ expect.unreachable();
68
+ } catch (e) {
69
+ expect(e).toBeInstanceOf(CliError);
70
+ const c = e as CliError;
71
+ expect(c.message).toBe("Whatever");
72
+ expect(c.exitCode).toBe(3);
73
+ expect(c.details).toMatchObject({
74
+ code: CLI_ERR.notFound,
75
+ board: "my-board",
76
+ taskId: 9,
77
+ });
78
+ }
79
+ });
80
+
81
+ test("rethrows unchanged when code is not not_found", () => {
82
+ const err = new CliError("Nope", 4, { code: CLI_ERR.forbidden });
83
+ expect(() => enrichNotFoundError(err, { board: "x" })).toThrow(err);
84
+ });
85
+ });