@hiroleague/taskmanager 0.0.2 → 0.0.4

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 (179) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/architecture-YZFGNWBL-DoE0KxgG.js +1 -0
  3. package/dist/assets/architectureDiagram-Q4EWVU46-DeuBhy7X.js +36 -0
  4. package/dist/assets/{blockDiagram-DXYQGD6D-DQzEOPT2.js → blockDiagram-DXYQGD6D-BDBy9ns9.js} +1 -1
  5. package/dist/assets/{c4Diagram-AHTNJAMY-B2Yfcwbo.js → c4Diagram-AHTNJAMY-CpqJj_8a.js} +1 -1
  6. package/dist/assets/channel-PHRyjspt.js +1 -0
  7. package/dist/assets/{chunk-2KRD3SAO-C2e-_49I.js → chunk-2KRD3SAO-DEpUsxdZ.js} +1 -1
  8. package/dist/assets/chunk-336JU56O-BGQvSwLk.js +2 -0
  9. package/dist/assets/chunk-426QAEUC-Cl9nQN9c.js +1 -0
  10. package/dist/assets/{chunk-4TB4RGXK-AZq3s1Dh.js → chunk-4TB4RGXK-Dq7aiIrZ.js} +2 -2
  11. package/dist/assets/{chunk-5FUZZQ4R-XEga0hMC.js → chunk-5FUZZQ4R-B_HuuUjf.js} +1 -1
  12. package/dist/assets/{chunk-5PVQY5BW-BrmXs2Gs.js → chunk-5PVQY5BW-cGfZCZGU.js} +2 -2
  13. package/dist/assets/{chunk-67CJDMHE-B1-M78qu.js → chunk-67CJDMHE-BMYAVZfw.js} +1 -1
  14. package/dist/assets/{chunk-7N4EOEYR-D7mYFpz-.js → chunk-7N4EOEYR-Ct-EY7Nc.js} +1 -1
  15. package/dist/assets/{chunk-AA7GKIK3-VWI9k39i.js → chunk-AA7GKIK3-Bd4HFpeo.js} +1 -1
  16. package/dist/assets/{chunk-CIAEETIT-hnu4zamm.js → chunk-CIAEETIT-CrFUkPMT.js} +1 -1
  17. package/dist/assets/{chunk-EDXVE4YY-DxUqDyxy.js → chunk-EDXVE4YY-DMDyt0NF.js} +1 -1
  18. package/dist/assets/{chunk-ENJZ2VHE-BgZKYo1l.js → chunk-ENJZ2VHE-DrWzOrpd.js} +1 -1
  19. package/dist/assets/{chunk-FOC6F5B3-BJsh9nO9.js → chunk-FOC6F5B3-Bemzq96j.js} +1 -1
  20. package/dist/assets/{chunk-ICPOFSXX-BNR1V8rT.js → chunk-ICPOFSXX-DkUVjrLw.js} +5 -5
  21. package/dist/assets/{chunk-K5T4RW27-BLIPdXaZ.js → chunk-K5T4RW27-ALKIf000.js} +5 -5
  22. package/dist/assets/{chunk-KGLVRYIC-DvaW2TkT.js → chunk-KGLVRYIC-Bg6HNTZ-.js} +1 -1
  23. package/dist/assets/{chunk-LIHQZDEY-CUsM0M11.js → chunk-LIHQZDEY-DeyGongE.js} +1 -1
  24. package/dist/assets/{chunk-ORNJ4GCN-CfluNV0_.js → chunk-ORNJ4GCN-Bx83s1bJ.js} +1 -1
  25. package/dist/assets/{chunk-OYMX7WX6-CkWzw4JX.js → chunk-OYMX7WX6-BqRUtRpL.js} +1 -1
  26. package/dist/assets/{chunk-U2HBQHQK-DTJPeU7W.js → chunk-U2HBQHQK-DogcerR6.js} +1 -1
  27. package/dist/assets/{chunk-X2U36JSP-CrTnmMqG.js → chunk-X2U36JSP-CwVWdmZV.js} +1 -1
  28. package/dist/assets/chunk-XPW4576I-DQpNCogT.js +32 -0
  29. package/dist/assets/{chunk-YZCP3GAM-9wq0QKUn.js → chunk-YZCP3GAM-crQSbji9.js} +1 -1
  30. package/dist/assets/{chunk-ZZ45TVLE-D3I1kLlo.js → chunk-ZZ45TVLE-Bk1S1YtS.js} +1 -1
  31. package/dist/assets/classDiagram-6PBFFD2Q-B_TabGaU.js +1 -0
  32. package/dist/assets/classDiagram-v2-HSJHXN6E-CGnZkUWw.js +1 -0
  33. package/dist/assets/clone-D4ka472w.js +1 -0
  34. package/dist/assets/{cose-bilkent-S5V4N54A-BygGvZGW.js → cose-bilkent-S5V4N54A-RBTHUit8.js} +1 -1
  35. package/dist/assets/cytoscape.esm-BGJwlmkf.js +321 -0
  36. package/dist/assets/dagre-B32eYLtm.js +1 -0
  37. package/dist/assets/{dagre-KV5264BT-lveZDhBf.js → dagre-KV5264BT-nX7tuXXn.js} +1 -1
  38. package/dist/assets/diagram-5BDNPKRD-DRxMXlQr.js +10 -0
  39. package/dist/assets/diagram-G4DWMVQ6-CoojevGm.js +24 -0
  40. package/dist/assets/diagram-MMDJMWI5-CWtJyfVW.js +43 -0
  41. package/dist/assets/diagram-TYMM5635-CsDJC4Hq.js +24 -0
  42. package/dist/assets/{erDiagram-SMLLAGMA-dx09stuy.js → erDiagram-SMLLAGMA-Cf7Xtd9A.js} +2 -2
  43. package/dist/assets/{flatten-B2BZ0pzY.js → flatten-CYX_pHZ7.js} +1 -1
  44. package/dist/assets/{flowDiagram-DWJPFMVM-CJi2WISS.js → flowDiagram-DWJPFMVM-DQaeR16a.js} +3 -3
  45. package/dist/assets/{ganttDiagram-T4ZO3ILL-OCTvbRxF.js → ganttDiagram-T4ZO3ILL-8EIcztcH.js} +1 -1
  46. package/dist/assets/gitGraph-7Q5UKJZL-BH9A1SAZ.js +1 -0
  47. package/dist/assets/{gitGraphDiagram-UUTBAWPF-Bjj94M12.js → gitGraphDiagram-UUTBAWPF-DO9ODqYw.js} +1 -1
  48. package/dist/assets/graphlib-bPBqlJKT.js +1 -0
  49. package/dist/assets/identity-Me9aart9.js +1 -0
  50. package/dist/assets/index-BpzHnKdP.css +1 -0
  51. package/dist/assets/index-DmNErTAP.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-Cnc1bwBo.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-CwHbIze0.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-DswAJiEd.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-DgQTCddl.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-D1moa23Z.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-Dvhj7HGn.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-z-IncVmK.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-D-kouujI.js → wardleyDiagram-NUSXRM2D-5Q201ea3.js} +1 -1
  78. package/dist/assets/{xychartDiagram-5P7HB3ND-D1lnM0pL.js → xychartDiagram-5P7HB3ND-BPZv_axd.js} +3 -3
  79. package/dist/index.html +17 -13
  80. package/package.json +2 -4
  81. package/skills/hiro-task-manager-cli/SKILL.md +6 -4
  82. package/skills/hiro-task-manager-cli/reference/cli-access-policy.md +1 -0
  83. package/skills/hiro-task-manager-cli/reference/releases.md +14 -0
  84. package/src/cli/bootstrap/launcher.ts +19 -5
  85. package/src/cli/bootstrap/program.test.ts +46 -0
  86. package/src/cli/bootstrap/program.ts +50 -1
  87. package/src/cli/commands/query.ts +56 -56
  88. package/src/cli/commands/releases.ts +22 -0
  89. package/src/cli/handlers/boards.test.ts +669 -669
  90. package/src/cli/handlers/cli-wiring.test.ts +38 -1
  91. package/src/cli/handlers/releases.ts +15 -0
  92. package/src/cli/handlers/search.test.ts +374 -374
  93. package/src/cli/handlers/search.ts +17 -17
  94. package/src/cli/lib/cli-http-errors.test.ts +85 -85
  95. package/src/cli/lib/launcherUi.test.ts +74 -0
  96. package/src/cli/lib/launcherUi.ts +47 -0
  97. package/src/cli/lib/write/releases.ts +64 -1
  98. package/src/cli/lib/write-result.test.ts +3 -0
  99. package/src/cli/lib/write-result.ts +3 -0
  100. package/src/cli/lib/writeCommands.breadth.test.ts +143 -0
  101. package/src/cli/lib/writeCommands.ts +1 -0
  102. package/src/cli/subprocess.real-stack.test.ts +625 -611
  103. package/src/cli/subprocess.smoke.test.ts +954 -954
  104. package/src/client/api/useBoardChangeStream.ts +421 -168
  105. package/src/client/api/useBoardIndexStream.ts +35 -0
  106. package/src/client/components/board/BoardStatsChips.tsx +233 -233
  107. package/src/client/components/board/BoardStatsContext.tsx +41 -41
  108. package/src/client/components/board/boardHeaderButtonStyles.ts +38 -38
  109. package/src/client/components/board/shortcuts/useBoardShortcutKeydown.ts +49 -49
  110. package/src/client/components/board/useBoardCanvasPanScroll.ts +108 -108
  111. package/src/client/components/board/useBoardTaskContainerDroppableReact.ts +33 -33
  112. package/src/client/components/board/useBoardTaskSortableReact.ts +26 -26
  113. package/src/client/components/layout/AppShell.tsx +5 -2
  114. package/src/client/components/layout/NotificationToasts.tsx +38 -1
  115. package/src/client/components/multi-select.tsx +1206 -1206
  116. package/src/client/components/routing/BoardPage.tsx +20 -20
  117. package/src/client/components/routing/NavigationRegistrar.tsx +13 -13
  118. package/src/client/components/task/TaskCard.tsx +643 -643
  119. package/src/client/components/ui/badge.tsx +49 -49
  120. package/src/client/components/ui/button.tsx +65 -65
  121. package/src/client/components/ui/command.tsx +193 -193
  122. package/src/client/components/ui/dialog.tsx +163 -163
  123. package/src/client/components/ui/input-group.tsx +155 -155
  124. package/src/client/components/ui/input.tsx +19 -19
  125. package/src/client/components/ui/popover.tsx +87 -87
  126. package/src/client/components/ui/separator.tsx +28 -28
  127. package/src/client/components/ui/textarea.tsx +18 -18
  128. package/src/client/index.css +248 -248
  129. package/src/client/lib/appNavigate.ts +16 -16
  130. package/src/client/lib/taskCardDate.ts +111 -111
  131. package/src/client/lib/utils.ts +6 -6
  132. package/src/client/store/notificationUi.ts +14 -0
  133. package/src/server/auth.ts +351 -351
  134. package/src/server/events.ts +31 -4
  135. package/src/server/migrations/registry.ts +43 -43
  136. package/src/server/notificationEvents.ts +8 -1
  137. package/src/server/routes/boards.ts +15 -1
  138. package/src/server/routes/trash.ts +6 -1
  139. package/src/shared/boardEvents.ts +6 -0
  140. package/src/shared/runtimeConfig.ts +256 -256
  141. package/src/shared/skillsInstall.ts +2 -3
  142. package/dist/assets/architecture-YZFGNWBL-C1MoQeSs.js +0 -1
  143. package/dist/assets/architectureDiagram-Q4EWVU46-DUEfvDBu.js +0 -36
  144. package/dist/assets/channel-yBmN_ln0.js +0 -1
  145. package/dist/assets/classDiagram-6PBFFD2Q-Dx_f-9b7.js +0 -1
  146. package/dist/assets/classDiagram-v2-HSJHXN6E-CSfvZ-nt.js +0 -1
  147. package/dist/assets/clone-CXokakwV.js +0 -1
  148. package/dist/assets/cytoscape.esm-BIYWHPG0.js +0 -321
  149. package/dist/assets/dagre-Do0eD9eI.js +0 -1
  150. package/dist/assets/diagram-5BDNPKRD-Dq5yM_uY.js +0 -10
  151. package/dist/assets/diagram-G4DWMVQ6-D-SYOmKm.js +0 -24
  152. package/dist/assets/diagram-MMDJMWI5-lU5t9BZA.js +0 -43
  153. package/dist/assets/diagram-TYMM5635-6tfUbY3R.js +0 -24
  154. package/dist/assets/gitGraph-7Q5UKJZL-BXTuQaDM.js +0 -1
  155. package/dist/assets/graphlib-BIlXYGdM.js +0 -1
  156. package/dist/assets/identity-D4WOnl_h.js +0 -1
  157. package/dist/assets/index-CZZuue3D.js +0 -304
  158. package/dist/assets/index-hMFTu7sr.css +0 -1
  159. package/dist/assets/info-OMHHGYJF-BeeKt8-X.js +0 -1
  160. package/dist/assets/infoDiagram-42DDH7IO-wq_opQKO.js +0 -2
  161. package/dist/assets/mermaid-parser.core-DrLhKJ48.js +0 -4
  162. package/dist/assets/packet-4T2RLAQJ-DQ-H9_jd.js +0 -1
  163. package/dist/assets/pie-ZZUOXDRM-BSj0Jsyj.js +0 -1
  164. package/dist/assets/radar-PYXPWWZC-B7-oRPFL.js +0 -1
  165. package/dist/assets/reduce-Uumu9GdR.js +0 -1
  166. package/dist/assets/stateDiagram-FHFEXIEX-Dx5CjenB.js +0 -1
  167. package/dist/assets/stateDiagram-v2-QKLJ7IA2-C_PkrTdc.js +0 -1
  168. package/dist/assets/treeView-SZITEDCU-CFXle9Az.js +0 -1
  169. package/dist/assets/treemap-W4RFUUIX-CAW3vWh8.js +0 -1
  170. package/dist/assets/vennDiagram-DHZGUBPP-CT1ehozU.js +0 -34
  171. package/dist/assets/wardley-RL74JXVD-7q3ju4kc.js +0 -1
  172. package/scripts/postinstall-message.mjs +0 -160
  173. /package/dist/assets/{chunk-4BX2VUAB-ean5NKtU.js → chunk-4BX2VUAB-C70mcfQR.js} +0 -0
  174. /package/dist/assets/{chunk-55IACEB6-CvSRyJqy.js → chunk-55IACEB6-CWfnqcLM.js} +0 -0
  175. /package/dist/assets/{chunk-BSJP7CBP-D8kBlJsf.js → chunk-BSJP7CBP-B0LrXV9y.js} +0 -0
  176. /package/dist/assets/{chunk-FMBD7UC4-DrNhFt1N.js → chunk-FMBD7UC4-_mV71Mwu.js} +0 -0
  177. /package/dist/assets/{chunk-QZHKN3VN-Csp3OYJY.js → chunk-QZHKN3VN-t2nrsegL.js} +0 -0
  178. /package/dist/assets/{katex-8mXVa4k3.js → katex-B2dtGfSp.js} +0 -0
  179. /package/dist/assets/{rough.esm-DtEqI08j.js → rough.esm-DEh6Frf9.js} +0 -0
@@ -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
+ }
@@ -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
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { printSetupContinuePrompt, printSetupNextSteps } from "./launcherUi";
3
+
4
+ async function captureConsoleLog(
5
+ run: () => Promise<void> | void,
6
+ ): Promise<string> {
7
+ const originalLog = console.log;
8
+ const lines: string[] = [];
9
+ console.log = (...args: unknown[]) => {
10
+ lines.push(args.map(String).join(" "));
11
+ };
12
+ try {
13
+ await run();
14
+ } finally {
15
+ console.log = originalLog;
16
+ }
17
+ return `${lines.join("\n")}\n`;
18
+ }
19
+
20
+ describe("printSetupNextSteps", () => {
21
+ test("prints a prominent table with required repo skills install guidance", async () => {
22
+ const stdout = await captureConsoleLog(async () => {
23
+ printSetupNextSteps({
24
+ profileName: "default",
25
+ skillsInstalled: false,
26
+ });
27
+ });
28
+
29
+ expect(stdout).toContain("+-");
30
+ expect(stdout).toContain("REQUIRED BEFORE USING hirotm");
31
+ expect(stdout).toContain("Install AI agent skills now. The hirotm CLI depends on these skills.");
32
+ expect(stdout).toContain("hirotm --help");
33
+ expect(stdout).toContain("1. Repo skills : npx skills add hiro-league/hirotaskmanager");
34
+ expect(stdout).toContain("2. Update later : npx skills update");
35
+ expect(stdout).toContain("npx skills add hiro-league/hirotaskmanager");
36
+ expect(stdout).not.toContain("npx skills add \"$HOME/.taskmanager/skills\"");
37
+ });
38
+
39
+ test("prints local skills command when bundled skills were copied", async () => {
40
+ const stdout = await captureConsoleLog(async () => {
41
+ printSetupNextSteps({
42
+ profileName: "default",
43
+ skillsInstalled: true,
44
+ });
45
+ });
46
+
47
+ expect(stdout).toContain("npx skills add hiro-league/hirotaskmanager");
48
+ expect(stdout).toContain("2. Local skills : npx skills add \"$HOME/.taskmanager/skills\"");
49
+ expect(stdout).toContain("3. Update later : npx skills update");
50
+ expect(stdout).toContain("npx skills add \"$HOME/.taskmanager/skills\"");
51
+ expect(stdout).toContain("npx skills update");
52
+ });
53
+
54
+ test("prints profile-specific CLI guidance for named profiles", async () => {
55
+ const stdout = await captureConsoleLog(async () => {
56
+ printSetupNextSteps({
57
+ profileName: "work",
58
+ skillsInstalled: false,
59
+ });
60
+ });
61
+
62
+ expect(stdout).toContain("hirotm --profile work --help");
63
+ });
64
+ });
65
+
66
+ describe("printSetupContinuePrompt", () => {
67
+ test("prints a separate enter-to-continue message", async () => {
68
+ const stdout = await captureConsoleLog(() => {
69
+ printSetupContinuePrompt();
70
+ });
71
+
72
+ expect(stdout).toContain("Press Enter to continue and start TaskManager...");
73
+ });
74
+ });
@@ -39,6 +39,18 @@ function paintWarning(text: string): string {
39
39
  return paint(out, text, ansi.bold + ansi.yellow);
40
40
  }
41
41
 
42
+ function printAsciiTable(lines: readonly string[]): void {
43
+ const innerWidth = Math.max(76, ...lines.map((line) => line.length));
44
+ const border = ` +-${"-".repeat(innerWidth)}-+`;
45
+ line();
46
+ line(border);
47
+ for (const entry of lines) {
48
+ line(` | ${entry.padEnd(innerWidth)} |`);
49
+ }
50
+ line(border);
51
+ line();
52
+ }
53
+
42
54
  export type SpinnerHandle = {
43
55
  stop: (finalText?: string | null) => void;
44
56
  };
@@ -126,6 +138,41 @@ export function printSavedProfileSummary(opts: {
126
138
  );
127
139
  }
128
140
 
141
+ export function printSetupNextSteps(opts: {
142
+ profileName: string;
143
+ skillsInstalled: boolean;
144
+ }): void {
145
+ const cliHelpCommand =
146
+ opts.profileName === "default"
147
+ ? "hirotm --help"
148
+ : `hirotm --profile ${opts.profileName} --help`;
149
+ const skillLines = [
150
+ "1. Repo skills : npx skills add hiro-league/hirotaskmanager",
151
+ ];
152
+ if (opts.skillsInstalled) {
153
+ skillLines.push("2. Local skills : npx skills add \"$HOME/.taskmanager/skills\"");
154
+ skillLines.push("3. Update later : npx skills update");
155
+ } else {
156
+ skillLines.push("2. Update later : npx skills update");
157
+ }
158
+ const lines = [
159
+ "REQUIRED BEFORE USING hirotm",
160
+ "Install AI agent skills now. The hirotm CLI depends on these skills.",
161
+ "",
162
+ ...skillLines,
163
+ "",
164
+ `After skills install: ${cliHelpCommand}`,
165
+ ];
166
+
167
+ // Package managers may block postinstall hooks, so setup must surface the
168
+ // exact skills commands in a standalone table before startup continues.
169
+ printAsciiTable(lines);
170
+ }
171
+
172
+ export function printSetupContinuePrompt(): void {
173
+ line("Press Enter to continue and start TaskManager...");
174
+ }
175
+
129
176
  export async function printPassphraseHint(): Promise<void> {
130
177
  // Keep the browser handoff to one line because the recovery key prints later from the server.
131
178
  const text =
@@ -3,7 +3,7 @@ import type {
3
3
  ReleaseMutationResult,
4
4
  } from "../../../shared/mutationResults";
5
5
  import type { PaginatedListBody } from "../../../shared/pagination";
6
- import type { ReleaseDefinition } from "../../../shared/models";
6
+ import type { Board, ReleaseDefinition } from "../../../shared/models";
7
7
  import type { CliContext } from "../../types/context";
8
8
  import { CLI_ERR } from "../../types/errors";
9
9
  import { CLI_DEFAULTS } from "../constants";
@@ -19,6 +19,7 @@ import { fetchAllPages } from "../paginatedFetch";
19
19
  import { CliError } from "../output";
20
20
  import { assertMutuallyExclusive } from "../validation";
21
21
  import {
22
+ compactBoardEntity,
22
23
  compactReleaseEntity,
23
24
  writeReleaseDelete,
24
25
  writeSuccess,
@@ -306,3 +307,65 @@ export async function runReleasesDelete(
306
307
  enrichNotFoundError(e, { board: boardId, releaseId: rid });
307
308
  }
308
309
  }
310
+
311
+ /**
312
+ * Set or clear the board default release (PATCH board `defaultReleaseId`).
313
+ * Validates the release exists on the board before PATCH so errors are clearer than a generic 404.
314
+ */
315
+ export async function runReleasesSetDefault(
316
+ ctx: CliContext,
317
+ opts: {
318
+ port?: number;
319
+ board: string | undefined;
320
+ releaseId: string | undefined;
321
+ clear: boolean;
322
+ },
323
+ ): Promise<void> {
324
+ const boardId = opts.board?.trim();
325
+ if (!boardId) {
326
+ throw new CliError("Missing required option: --board", 2, {
327
+ code: CLI_ERR.missingRequired,
328
+ });
329
+ }
330
+ assertMutuallyExclusive([
331
+ ["<release-id>", opts.releaseId, "--clear", opts.clear],
332
+ ]);
333
+ const ridRaw = opts.releaseId?.trim();
334
+ if (!opts.clear && (ridRaw === undefined || ridRaw === "")) {
335
+ throw new CliError("Provide <release-id> or use --clear", 2, {
336
+ code: CLI_ERR.missingRequired,
337
+ });
338
+ }
339
+ if (!opts.clear) {
340
+ const rid = parsePositiveInt("releaseId", ridRaw);
341
+ if (rid === undefined) {
342
+ throw new CliError("Invalid release id", 2, {
343
+ code: CLI_ERR.invalidValue,
344
+ releaseId: ridRaw,
345
+ });
346
+ }
347
+ const rows = await fetchAllBoardReleases(ctx, opts.port, boardId);
348
+ if (!rows.some((r) => r.releaseId === rid)) {
349
+ throw new CliError("Release not found", 3, {
350
+ code: CLI_ERR.notFound,
351
+ board: boardId,
352
+ releaseId: rid,
353
+ });
354
+ }
355
+ }
356
+
357
+ const patch: Record<string, unknown> = {
358
+ defaultReleaseId: opts.clear ? null : Number(ridRaw),
359
+ };
360
+
361
+ try {
362
+ const board = await ctx.fetchApiMutate<Board>(
363
+ `/boards/${encodeURIComponent(boardId)}`,
364
+ { method: "PATCH", body: patch },
365
+ { port: opts.port },
366
+ );
367
+ ctx.printJson(writeSuccess(board, compactBoardEntity(board)));
368
+ } catch (e) {
369
+ enrichNotFoundError(e, { board: boardId });
370
+ }
371
+ }
@@ -29,6 +29,7 @@ describe("compact*Entity", () => {
29
29
  emoji: null,
30
30
  createdAt: "c",
31
31
  updatedAt: "u",
32
+ defaultReleaseId: null,
32
33
  });
33
34
  });
34
35
 
@@ -103,6 +104,7 @@ describe("writeSuccess / trashedEntity / writeTrashMove", () => {
103
104
  emoji: null,
104
105
  createdAt: "c",
105
106
  updatedAt: "u",
107
+ defaultReleaseId: null,
106
108
  },
107
109
  );
108
110
  expect(envelope).toEqual({
@@ -118,6 +120,7 @@ describe("writeSuccess / trashedEntity / writeTrashMove", () => {
118
120
  emoji: null,
119
121
  createdAt: "c",
120
122
  updatedAt: "u",
123
+ defaultReleaseId: null,
121
124
  },
122
125
  });
123
126
  });
@@ -9,6 +9,8 @@ export type WriteBoardEntity = {
9
9
  emoji: string | null;
10
10
  createdAt: string;
11
11
  updatedAt: string;
12
+ /** Board default release for new tasks (when set). */
13
+ defaultReleaseId?: number | null;
12
14
  };
13
15
 
14
16
  export type WriteListEntity = {
@@ -91,6 +93,7 @@ export function compactBoardEntity(board: Board): WriteBoardEntity {
91
93
  emoji: board.emoji ?? null,
92
94
  createdAt: board.createdAt,
93
95
  updatedAt: board.updatedAt,
96
+ defaultReleaseId: board.defaultReleaseId ?? null,
94
97
  };
95
98
  }
96
99
 
@@ -25,6 +25,7 @@ import {
25
25
  runListsUpdate,
26
26
  runReleasesAdd,
27
27
  runReleasesDelete,
28
+ runReleasesSetDefault,
28
29
  runReleasesUpdate,
29
30
  runTasksAdd,
30
31
  runTasksDelete,
@@ -118,6 +119,48 @@ describe("writeCommands breadth — validation", () => {
118
119
  });
119
120
  });
120
121
 
122
+ test("runReleasesSetDefault throws without board", async () => {
123
+ await expect(
124
+ runReleasesSetDefault(ctx, {
125
+ port: 1,
126
+ board: undefined,
127
+ releaseId: "1",
128
+ clear: false,
129
+ }),
130
+ ).rejects.toMatchObject({
131
+ exitCode: 2,
132
+ details: expect.objectContaining({ code: CLI_ERR.missingRequired }),
133
+ });
134
+ });
135
+
136
+ test("runReleasesSetDefault throws without release id and without --clear", async () => {
137
+ await expect(
138
+ runReleasesSetDefault(ctx, {
139
+ port: 1,
140
+ board: "b",
141
+ releaseId: undefined,
142
+ clear: false,
143
+ }),
144
+ ).rejects.toMatchObject({
145
+ exitCode: 2,
146
+ details: expect.objectContaining({ code: CLI_ERR.missingRequired }),
147
+ });
148
+ });
149
+
150
+ test("runReleasesSetDefault throws when release id and --clear together", async () => {
151
+ await expect(
152
+ runReleasesSetDefault(ctx, {
153
+ port: 1,
154
+ board: "b",
155
+ releaseId: "2",
156
+ clear: true,
157
+ }),
158
+ ).rejects.toMatchObject({
159
+ exitCode: 2,
160
+ details: expect.objectContaining({ code: CLI_ERR.mutuallyExclusiveOptions }),
161
+ });
162
+ });
163
+
121
164
  test("runListsMove throws on multiple placement flags", async () => {
122
165
  await expect(
123
166
  runListsMove(ctx, {
@@ -687,6 +730,106 @@ describe("writeCommands breadth — mock fetch happy paths", () => {
687
730
  });
688
731
  });
689
732
 
733
+ test("runReleasesSetDefault GETs releases then PATCHes board defaultReleaseId", async () => {
734
+ setMockFetch(async (input, init) => {
735
+ const u = reqUrl(input);
736
+ if (
737
+ u.includes("/boards/b/releases") &&
738
+ (init?.method === "GET" || init?.method === undefined)
739
+ ) {
740
+ return new Response(
741
+ JSON.stringify({
742
+ items: [
743
+ {
744
+ releaseId: 2,
745
+ name: "v1",
746
+ createdAt: "2026-01-01T00:00:00.000Z",
747
+ },
748
+ ],
749
+ total: 1,
750
+ limit: 500,
751
+ offset: 0,
752
+ }),
753
+ { status: 200, headers: { "content-type": "application/json" } },
754
+ );
755
+ }
756
+ if (u.includes("/api/boards/b") && !u.includes("/releases") && init?.method === "PATCH") {
757
+ expect(JSON.parse(String(init?.body))).toEqual({ defaultReleaseId: 2 });
758
+ return new Response(
759
+ JSON.stringify({
760
+ boardId: 1,
761
+ slug: "b",
762
+ name: "B",
763
+ emoji: null,
764
+ createdAt: "2026-01-01T00:00:00.000Z",
765
+ updatedAt: "2026-01-02T00:00:00.000Z",
766
+ defaultReleaseId: 2,
767
+ releases: [],
768
+ lists: [],
769
+ taskGroups: [],
770
+ taskPriorities: [],
771
+ defaultTaskGroupId: 1,
772
+ autoAssignReleaseOnCreateUi: false,
773
+ autoAssignReleaseOnCreateCli: false,
774
+ } as unknown as Board),
775
+ { status: 200, headers: { "content-type": "application/json" } },
776
+ );
777
+ }
778
+ throw new Error(`unexpected fetch: ${u} ${init?.method}`);
779
+ });
780
+ const out = await captureStdout(() =>
781
+ runReleasesSetDefault(ctx, {
782
+ port: 22025,
783
+ board: "b",
784
+ releaseId: "2",
785
+ clear: false,
786
+ }),
787
+ );
788
+ expect(JSON.parse(out.trim())).toMatchObject({
789
+ ok: true,
790
+ entity: { type: "board", defaultReleaseId: 2 },
791
+ });
792
+ });
793
+
794
+ test("runReleasesSetDefault --clear PATCHes defaultReleaseId null", async () => {
795
+ setMockFetch(async (input, init) => {
796
+ expect(reqUrl(input)).toContain("/api/boards/b");
797
+ expect(init?.method).toBe("PATCH");
798
+ expect(JSON.parse(String(init?.body))).toEqual({ defaultReleaseId: null });
799
+ return new Response(
800
+ JSON.stringify({
801
+ boardId: 1,
802
+ slug: "b",
803
+ name: "B",
804
+ emoji: null,
805
+ createdAt: "2026-01-01T00:00:00.000Z",
806
+ updatedAt: "2026-01-02T00:00:00.000Z",
807
+ defaultReleaseId: null,
808
+ releases: [],
809
+ lists: [],
810
+ taskGroups: [],
811
+ taskPriorities: [],
812
+ defaultTaskGroupId: 1,
813
+ autoAssignReleaseOnCreateUi: false,
814
+ autoAssignReleaseOnCreateCli: false,
815
+ } as unknown as Board),
816
+ { status: 200, headers: { "content-type": "application/json" } },
817
+ );
818
+ });
819
+ const out = await captureStdout(() =>
820
+ runReleasesSetDefault(ctx, {
821
+ port: 22026,
822
+ board: "b",
823
+ releaseId: undefined,
824
+ clear: true,
825
+ }),
826
+ );
827
+ expect(JSON.parse(out.trim())).toMatchObject({
828
+ ok: true,
829
+ entity: { type: "board", defaultReleaseId: null },
830
+ });
831
+ });
832
+
690
833
  test("mutating fetch sends runtime client name header", async () => {
691
834
  setRuntimeCliClientName("Cursor Agent");
692
835
  setMockFetch(async (_input, init) => {
@@ -27,6 +27,7 @@ export {
27
27
  runReleasesAdd,
28
28
  runReleasesDelete,
29
29
  runReleasesList,
30
+ runReleasesSetDefault,
30
31
  runReleasesShow,
31
32
  runReleasesUpdate,
32
33
  } from "./write/releases";