@hiroleague/taskmanager 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +41 -52
  3. package/dist/assets/architecture-YZFGNWBL-DoE0KxgG.js +1 -0
  4. package/dist/assets/architectureDiagram-Q4EWVU46-DeuBhy7X.js +36 -0
  5. package/dist/assets/{blockDiagram-DXYQGD6D-DfOGNphI.js → blockDiagram-DXYQGD6D-BDBy9ns9.js} +1 -1
  6. package/dist/assets/{c4Diagram-AHTNJAMY-B2Yfcwbo.js → c4Diagram-AHTNJAMY-CpqJj_8a.js} +1 -1
  7. package/dist/assets/channel-PHRyjspt.js +1 -0
  8. package/dist/assets/{chunk-2KRD3SAO-9yt00aGC.js → chunk-2KRD3SAO-DEpUsxdZ.js} +1 -1
  9. package/dist/assets/chunk-336JU56O-BGQvSwLk.js +2 -0
  10. package/dist/assets/chunk-426QAEUC-Cl9nQN9c.js +1 -0
  11. package/dist/assets/{chunk-4TB4RGXK-DF8yJBFl.js → chunk-4TB4RGXK-Dq7aiIrZ.js} +2 -2
  12. package/dist/assets/{chunk-5FUZZQ4R-XEga0hMC.js → chunk-5FUZZQ4R-B_HuuUjf.js} +1 -1
  13. package/dist/assets/{chunk-5PVQY5BW-BrmXs2Gs.js → chunk-5PVQY5BW-cGfZCZGU.js} +2 -2
  14. package/dist/assets/{chunk-67CJDMHE-5wFKo04G.js → chunk-67CJDMHE-BMYAVZfw.js} +1 -1
  15. package/dist/assets/{chunk-7N4EOEYR-BRRGX_NC.js → chunk-7N4EOEYR-Ct-EY7Nc.js} +1 -1
  16. package/dist/assets/{chunk-AA7GKIK3-DUZv_pNI.js → chunk-AA7GKIK3-Bd4HFpeo.js} +1 -1
  17. package/dist/assets/{chunk-CIAEETIT-mA5aM_d7.js → chunk-CIAEETIT-CrFUkPMT.js} +1 -1
  18. package/dist/assets/{chunk-EDXVE4YY-DxUqDyxy.js → chunk-EDXVE4YY-DMDyt0NF.js} +1 -1
  19. package/dist/assets/{chunk-ENJZ2VHE-BgZKYo1l.js → chunk-ENJZ2VHE-DrWzOrpd.js} +1 -1
  20. package/dist/assets/{chunk-FOC6F5B3-B-cqGCPC.js → chunk-FOC6F5B3-Bemzq96j.js} +1 -1
  21. package/dist/assets/{chunk-ICPOFSXX-BNR1V8rT.js → chunk-ICPOFSXX-DkUVjrLw.js} +5 -5
  22. package/dist/assets/{chunk-K5T4RW27-BLRDzioh.js → chunk-K5T4RW27-ALKIf000.js} +5 -5
  23. package/dist/assets/{chunk-KGLVRYIC-CTkQSeKy.js → chunk-KGLVRYIC-Bg6HNTZ-.js} +1 -1
  24. package/dist/assets/{chunk-LIHQZDEY-Cf34Nu3J.js → chunk-LIHQZDEY-DeyGongE.js} +1 -1
  25. package/dist/assets/{chunk-ORNJ4GCN-D3uXgbay.js → chunk-ORNJ4GCN-Bx83s1bJ.js} +1 -1
  26. package/dist/assets/{chunk-OYMX7WX6-syQho5jf.js → chunk-OYMX7WX6-BqRUtRpL.js} +1 -1
  27. package/dist/assets/{chunk-U2HBQHQK-DTJPeU7W.js → chunk-U2HBQHQK-DogcerR6.js} +1 -1
  28. package/dist/assets/{chunk-X2U36JSP-CrTnmMqG.js → chunk-X2U36JSP-CwVWdmZV.js} +1 -1
  29. package/dist/assets/chunk-XPW4576I-DQpNCogT.js +32 -0
  30. package/dist/assets/{chunk-YZCP3GAM-9wq0QKUn.js → chunk-YZCP3GAM-crQSbji9.js} +1 -1
  31. package/dist/assets/{chunk-ZZ45TVLE-D3I1kLlo.js → chunk-ZZ45TVLE-Bk1S1YtS.js} +1 -1
  32. package/dist/assets/classDiagram-6PBFFD2Q-B_TabGaU.js +1 -0
  33. package/dist/assets/classDiagram-v2-HSJHXN6E-CGnZkUWw.js +1 -0
  34. package/dist/assets/clone-D4ka472w.js +1 -0
  35. package/dist/assets/{cose-bilkent-S5V4N54A-BygGvZGW.js → cose-bilkent-S5V4N54A-RBTHUit8.js} +1 -1
  36. package/dist/assets/cytoscape.esm-BGJwlmkf.js +321 -0
  37. package/dist/assets/dagre-B32eYLtm.js +1 -0
  38. package/dist/assets/{dagre-KV5264BT-BBqulDtd.js → dagre-KV5264BT-nX7tuXXn.js} +1 -1
  39. package/dist/assets/diagram-5BDNPKRD-DRxMXlQr.js +10 -0
  40. package/dist/assets/diagram-G4DWMVQ6-CoojevGm.js +24 -0
  41. package/dist/assets/diagram-MMDJMWI5-CWtJyfVW.js +43 -0
  42. package/dist/assets/diagram-TYMM5635-CsDJC4Hq.js +24 -0
  43. package/dist/assets/{erDiagram-SMLLAGMA-BN5eJerP.js → erDiagram-SMLLAGMA-Cf7Xtd9A.js} +2 -2
  44. package/dist/assets/{flatten-C5NL-f24.js → flatten-CYX_pHZ7.js} +1 -1
  45. package/dist/assets/{flowDiagram-DWJPFMVM-CbFskc8S.js → flowDiagram-DWJPFMVM-DQaeR16a.js} +3 -3
  46. package/dist/assets/{ganttDiagram-T4ZO3ILL-OCTvbRxF.js → ganttDiagram-T4ZO3ILL-8EIcztcH.js} +1 -1
  47. package/dist/assets/gitGraph-7Q5UKJZL-BH9A1SAZ.js +1 -0
  48. package/dist/assets/{gitGraphDiagram-UUTBAWPF-wpqI2kyI.js → gitGraphDiagram-UUTBAWPF-DO9ODqYw.js} +1 -1
  49. package/dist/assets/graphlib-bPBqlJKT.js +1 -0
  50. package/dist/assets/identity-Me9aart9.js +1 -0
  51. package/dist/assets/index-oKG1C41_.js +273 -0
  52. package/dist/assets/info-OMHHGYJF-BvKR-zWh.js +1 -0
  53. package/dist/assets/infoDiagram-42DDH7IO-pRTXCm5C.js +2 -0
  54. package/dist/assets/isEmpty-Cu0k-j1j.js +1 -0
  55. package/dist/assets/{ishikawaDiagram-UXIWVN3A-Epc23N_0.js → ishikawaDiagram-UXIWVN3A-BP2YE5QI.js} +2 -2
  56. package/dist/assets/{journeyDiagram-VCZTEJTY-BkMxoaPq.js → journeyDiagram-VCZTEJTY-B3l2juoL.js} +1 -1
  57. package/dist/assets/{kanban-definition-6JOO6SKY-C8dW_26n.js → kanban-definition-6JOO6SKY-BpIpEOZZ.js} +4 -4
  58. package/dist/assets/{line-DNzQATGr.js → line-otOkzGl8.js} +1 -1
  59. package/dist/assets/mermaid-parser.core-xWsW24Gq.js +4 -0
  60. package/dist/assets/{mindmap-definition-QFDTVHPH-CvpNtrKT.js → mindmap-definition-QFDTVHPH-B9khyC7X.js} +3 -3
  61. package/dist/assets/packet-4T2RLAQJ-D8Dw3nmf.js +1 -0
  62. package/dist/assets/pie-ZZUOXDRM-ZghowlAE.js +1 -0
  63. package/dist/assets/{pieDiagram-DEJITSTG-eENymoXZ.js → pieDiagram-DEJITSTG-v32hL3i7.js} +1 -1
  64. package/dist/assets/{quadrantDiagram-34T5L4WZ-c0iZxo2I.js → quadrantDiagram-34T5L4WZ-DIL3GDFt.js} +1 -1
  65. package/dist/assets/radar-PYXPWWZC-D-PK3JOd.js +1 -0
  66. package/dist/assets/reduce-CImcgAcU.js +1 -0
  67. package/dist/assets/{requirementDiagram-MS252O5E-CmRO3hLp.js → requirementDiagram-MS252O5E-D8os2-4y.js} +2 -2
  68. package/dist/assets/{sankeyDiagram-XADWPNL6-woJZoQ58.js → sankeyDiagram-XADWPNL6-BV70D4l5.js} +1 -1
  69. package/dist/assets/{sequenceDiagram-FGHM5R23-B7qNcwNo.js → sequenceDiagram-FGHM5R23-Cwu8hQW1.js} +1 -1
  70. package/dist/assets/stateDiagram-FHFEXIEX-oYUWv7Fb.js +1 -0
  71. package/dist/assets/stateDiagram-v2-QKLJ7IA2-CFUTpFu-.js +1 -0
  72. package/dist/assets/{timeline-definition-GMOUNBTQ-CQWqDPGG.js → timeline-definition-GMOUNBTQ-CxSdKxpL.js} +1 -1
  73. package/dist/assets/treeView-SZITEDCU-uVgaJQzG.js +1 -0
  74. package/dist/assets/treemap-W4RFUUIX-Dcad_9AN.js +1 -0
  75. package/dist/assets/vennDiagram-DHZGUBPP-D4wgD7QI.js +34 -0
  76. package/dist/assets/wardley-RL74JXVD-CFXrK8mx.js +1 -0
  77. package/dist/assets/{wardleyDiagram-NUSXRM2D-DNhPIFCg.js → wardleyDiagram-NUSXRM2D-5Q201ea3.js} +1 -1
  78. package/dist/assets/{xychartDiagram-5P7HB3ND-BDblAZ11.js → xychartDiagram-5P7HB3ND-BPZv_axd.js} +3 -3
  79. package/dist/index.html +16 -12
  80. package/package.json +99 -92
  81. package/scripts/stubs/node-domexception/index.cjs +18 -0
  82. package/scripts/stubs/node-domexception/package.json +7 -0
  83. package/skills/hiro-task-manager-cli/SKILL.md +97 -0
  84. package/skills/hiro-task-manager-cli/reference/boards.md +143 -0
  85. package/skills/hiro-task-manager-cli/reference/cli-access-policy.md +72 -0
  86. package/skills/hiro-task-manager-cli/reference/errors.md +85 -0
  87. package/skills/hiro-task-manager-cli/reference/lists.md +106 -0
  88. package/skills/hiro-task-manager-cli/reference/releases.md +87 -0
  89. package/skills/hiro-task-manager-cli/reference/search.md +38 -0
  90. package/skills/hiro-task-manager-cli/reference/statuses.md +25 -0
  91. package/skills/hiro-task-manager-cli/reference/tasks.md +144 -0
  92. package/skills/hiro-task-manager-cli/reference/trash.md +50 -0
  93. package/src/cli/bootstrap/launcher.test.ts +66 -0
  94. package/src/cli/bootstrap/launcher.ts +389 -35
  95. package/src/cli/bootstrap/program.test.ts +46 -0
  96. package/src/cli/bootstrap/program.ts +54 -1
  97. package/src/cli/bootstrap/runtime.test.ts +15 -0
  98. package/src/cli/bootstrap/runtime.ts +27 -1
  99. package/src/cli/commands/query.ts +56 -56
  100. package/src/cli/commands/server.ts +27 -19
  101. package/src/cli/handlers/boards.test.ts +669 -669
  102. package/src/cli/handlers/cli-wiring.test.ts +1 -1
  103. package/src/cli/handlers/search.test.ts +374 -374
  104. package/src/cli/handlers/search.ts +17 -17
  105. package/src/cli/handlers/server.test.ts +55 -13
  106. package/src/cli/handlers/server.ts +16 -3
  107. package/src/cli/lib/api-client.test.ts +35 -2
  108. package/src/cli/lib/api-client.ts +43 -10
  109. package/src/cli/lib/cli-http-errors.test.ts +85 -85
  110. package/src/cli/lib/command-helpers.ts +161 -154
  111. package/src/cli/lib/config.ts +4 -0
  112. package/src/cli/lib/launcherUi.test.ts +74 -0
  113. package/src/cli/lib/launcherUi.ts +213 -0
  114. package/src/cli/lib/process.test.ts +24 -5
  115. package/src/cli/lib/process.ts +86 -55
  116. package/src/cli/ports/process.ts +8 -2
  117. package/src/cli/subprocess.real-stack.test.ts +611 -598
  118. package/src/cli/subprocess.smoke.test.ts +954 -969
  119. package/src/cli/types/config.ts +2 -6
  120. package/src/client/components/auth/AuthScreen.tsx +3 -3
  121. package/src/client/components/board/BoardStatsChips.tsx +233 -233
  122. package/src/client/components/board/BoardStatsContext.tsx +41 -41
  123. package/src/client/components/board/boardHeaderButtonStyles.ts +38 -38
  124. package/src/client/components/board/shortcuts/useBoardShortcutKeydown.ts +49 -49
  125. package/src/client/components/board/useBoardCanvasPanScroll.ts +108 -108
  126. package/src/client/components/board/useBoardTaskContainerDroppableReact.ts +33 -33
  127. package/src/client/components/board/useBoardTaskSortableReact.ts +26 -26
  128. package/src/client/components/multi-select.tsx +1206 -1206
  129. package/src/client/components/routing/BoardPage.tsx +20 -20
  130. package/src/client/components/routing/NavigationRegistrar.tsx +13 -13
  131. package/src/client/components/settings/SettingsPage.tsx +1 -1
  132. package/src/client/components/task/TaskCard.tsx +643 -643
  133. package/src/client/components/ui/badge.tsx +49 -49
  134. package/src/client/components/ui/button.tsx +65 -65
  135. package/src/client/components/ui/command.tsx +193 -193
  136. package/src/client/components/ui/dialog.tsx +163 -163
  137. package/src/client/components/ui/input-group.tsx +155 -155
  138. package/src/client/components/ui/input.tsx +19 -19
  139. package/src/client/components/ui/popover.tsx +87 -87
  140. package/src/client/components/ui/separator.tsx +28 -28
  141. package/src/client/components/ui/textarea.tsx +18 -18
  142. package/src/client/index.css +248 -248
  143. package/src/client/lib/appNavigate.ts +16 -16
  144. package/src/client/lib/taskCardDate.ts +111 -111
  145. package/src/client/lib/utils.ts +6 -6
  146. package/src/server/auth.ts +351 -302
  147. package/src/server/bootstrapDev.ts +11 -2
  148. package/src/server/bootstrapInstalled.ts +6 -1
  149. package/src/server/index.ts +33 -7
  150. package/src/server/migrations/013_cli_policy_and_provenance.ts +2 -2
  151. package/src/server/migrations/019_cli_global_create_board_default_on.ts +14 -0
  152. package/src/server/migrations/registry.ts +43 -41
  153. package/src/server/parseBootstrapProfile.ts +42 -0
  154. package/src/server/storage/cliPolicy.ts +2 -1
  155. package/src/shared/runtimeConfig.ts +256 -237
  156. package/src/shared/runtimeIdentity.test.ts +47 -0
  157. package/src/shared/runtimeIdentity.ts +35 -0
  158. package/src/shared/serverStatus.ts +21 -0
  159. package/src/shared/skillsInstall.ts +70 -0
  160. package/src/shared/terminalColors.ts +24 -0
  161. package/dist/assets/architecture-YZFGNWBL-3h1eIYfB.js +0 -1
  162. package/dist/assets/architectureDiagram-Q4EWVU46-DSQ1_74_.js +0 -36
  163. package/dist/assets/channel-yBmN_ln0.js +0 -1
  164. package/dist/assets/classDiagram-6PBFFD2Q-CotFZI8-.js +0 -1
  165. package/dist/assets/classDiagram-v2-HSJHXN6E-DAPzeDGn.js +0 -1
  166. package/dist/assets/clone-BRQpYu_n.js +0 -1
  167. package/dist/assets/cytoscape.esm-BIYWHPG0.js +0 -321
  168. package/dist/assets/dagre-rhyPjnsQ.js +0 -1
  169. package/dist/assets/diagram-5BDNPKRD-Ky3EXXj0.js +0 -10
  170. package/dist/assets/diagram-G4DWMVQ6-t7LbT0Uz.js +0 -24
  171. package/dist/assets/diagram-MMDJMWI5-CdnLXEMx.js +0 -43
  172. package/dist/assets/diagram-TYMM5635-CnzTqJBM.js +0 -24
  173. package/dist/assets/gitGraph-7Q5UKJZL-CG8f8JF7.js +0 -1
  174. package/dist/assets/graphlib-COiJG5Qv.js +0 -1
  175. package/dist/assets/identity-D4WOnl_h.js +0 -1
  176. package/dist/assets/index-lyyIVcc_.js +0 -304
  177. package/dist/assets/info-OMHHGYJF-C8_SHoRO.js +0 -1
  178. package/dist/assets/infoDiagram-42DDH7IO-BbvTdpSV.js +0 -2
  179. package/dist/assets/mermaid-parser.core-6Tn8epr_.js +0 -4
  180. package/dist/assets/packet-4T2RLAQJ-BvpAX0kJ.js +0 -1
  181. package/dist/assets/pie-ZZUOXDRM-Ow26Yf-E.js +0 -1
  182. package/dist/assets/radar-PYXPWWZC-e_ron5jQ.js +0 -1
  183. package/dist/assets/reduce-BDOBPIXr.js +0 -1
  184. package/dist/assets/stateDiagram-FHFEXIEX-CYfGMoR8.js +0 -1
  185. package/dist/assets/stateDiagram-v2-QKLJ7IA2-CO1W_n55.js +0 -1
  186. package/dist/assets/treeView-SZITEDCU-DsEr3xeq.js +0 -1
  187. package/dist/assets/treemap-W4RFUUIX-DV7nk2AB.js +0 -1
  188. package/dist/assets/vennDiagram-DHZGUBPP-BjTbuhcb.js +0 -34
  189. package/dist/assets/wardley-RL74JXVD-CrrFU9AE.js +0 -1
  190. /package/dist/assets/{chunk-4BX2VUAB-ean5NKtU.js → chunk-4BX2VUAB-C70mcfQR.js} +0 -0
  191. /package/dist/assets/{chunk-55IACEB6-CvSRyJqy.js → chunk-55IACEB6-CWfnqcLM.js} +0 -0
  192. /package/dist/assets/{chunk-BSJP7CBP-D8kBlJsf.js → chunk-BSJP7CBP-B0LrXV9y.js} +0 -0
  193. /package/dist/assets/{chunk-FMBD7UC4-DrNhFt1N.js → chunk-FMBD7UC4-_mV71Mwu.js} +0 -0
  194. /package/dist/assets/{chunk-QZHKN3VN-Csp3OYJY.js → chunk-QZHKN3VN-t2nrsegL.js} +0 -0
  195. /package/dist/assets/{katex-8mXVa4k3.js → katex-B2dtGfSp.js} +0 -0
  196. /package/dist/assets/{rough.esm-DtEqI08j.js → rough.esm-DEh6Frf9.js} +0 -0
@@ -1,374 +1,374 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import type { PaginatedListBody } from "../../shared/pagination";
3
- import type { SearchHit } from "../../shared/models";
4
- import { CLI_ERR } from "../types/errors";
5
- import { syncCliOutputFormatFromGlobals } from "../lib/cliFormat";
6
- import { createTestCliRuntime } from "../lib/runtime";
7
- import { resetCliOutputFormat } from "../lib/output";
8
- import { createDefaultCliContext } from "./context";
9
- import { handleSearch } from "./search";
10
- import type { CliContext } from "./context";
11
-
12
- function mockContext(overrides: Partial<CliContext> = {}): CliContext {
13
- return {
14
- ...createDefaultCliContext(),
15
- resolvePort: () => 3002,
16
- fetchApi: async () => {
17
- throw new Error("fetchApi not stubbed");
18
- },
19
- fetchApiMutate: async () => {
20
- throw new Error("fetchApiMutate not stubbed");
21
- },
22
- fetchApiTrashMutate: async () => {
23
- throw new Error("fetchApiTrashMutate not stubbed");
24
- },
25
- printJson: () => {},
26
- getRuntime: () => createTestCliRuntime({ port: 3002 }),
27
- ...overrides,
28
- };
29
- }
30
-
31
- describe("handleSearch", () => {
32
- afterEach(() => {
33
- resetCliOutputFormat();
34
- });
35
-
36
- test("requires non-empty query", async () => {
37
- const ctx = mockContext();
38
- await expect(handleSearch(ctx, ["", " "], {})).rejects.toMatchObject({
39
- exitCode: 2,
40
- details: expect.objectContaining({ code: CLI_ERR.missingRequired }),
41
- });
42
- });
43
-
44
- test("ndjson output prints one JSON object per hit line", async () => {
45
- const hits: SearchHit[] = [
46
- {
47
- boardId: 1,
48
- boardSlug: "b",
49
- taskId: 9,
50
- boardName: "B",
51
- listId: 2,
52
- listName: "L",
53
- title: "T",
54
- snippet: "s",
55
- score: 0.1,
56
- },
57
- ];
58
- const body: PaginatedListBody<SearchHit> = {
59
- items: hits,
60
- total: hits.length,
61
- limit: 5,
62
- offset: 0,
63
- };
64
- let fetchedPath = "";
65
- const ctx = mockContext({
66
- fetchApi: (async (path) => {
67
- fetchedPath = path;
68
- return body;
69
- }) as CliContext["fetchApi"],
70
- });
71
-
72
- let out = "";
73
- const origWrite = process.stdout.write.bind(process.stdout);
74
- process.stdout.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
75
- out +=
76
- typeof chunk === "string"
77
- ? chunk
78
- : new TextDecoder().decode(chunk as Uint8Array);
79
- void args;
80
- return true;
81
- };
82
- try {
83
- await handleSearch(ctx, ["hello", "world"], {
84
- board: "b1",
85
- limit: "5",
86
- noPrefix: true,
87
- });
88
- } finally {
89
- process.stdout.write = origWrite;
90
- }
91
-
92
- expect(fetchedPath).toContain("/search?");
93
- expect(fetchedPath).toContain("q=hello+world");
94
- expect(fetchedPath).toContain("limit=5");
95
- expect(fetchedPath).toContain("board=b1");
96
- expect(fetchedPath).toContain("prefix=0");
97
- const lines = out.trimEnd().split("\n").filter((l) => l.length > 0);
98
- expect(lines.length).toBe(1);
99
- expect(JSON.parse(lines[0]!)).toEqual(hits[0]);
100
- });
101
-
102
- test("rejects --fields when global format is human", async () => {
103
- syncCliOutputFormatFromGlobals({ format: "human" });
104
- const ctx = mockContext();
105
- await expect(
106
- handleSearch(ctx, ["q"], { fields: "taskId" }),
107
- ).rejects.toMatchObject({
108
- exitCode: 2,
109
- details: expect.objectContaining({ code: CLI_ERR.invalidValue }),
110
- });
111
- });
112
-
113
- test("ndjson --fields projects items", async () => {
114
- const hits: SearchHit[] = [
115
- {
116
- boardId: 1,
117
- boardSlug: "b",
118
- taskId: 9,
119
- boardName: "B",
120
- listId: 2,
121
- listName: "L",
122
- title: "T",
123
- snippet: "s",
124
- score: 0.1,
125
- },
126
- ];
127
- const body: PaginatedListBody<SearchHit> = {
128
- items: hits,
129
- total: 1,
130
- limit: 20,
131
- offset: 0,
132
- };
133
- const ctx = mockContext({
134
- fetchApi: (async () => body) as CliContext["fetchApi"],
135
- });
136
-
137
- let out = "";
138
- const origWrite = process.stdout.write.bind(process.stdout);
139
- process.stdout.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
140
- out +=
141
- typeof chunk === "string"
142
- ? chunk
143
- : new TextDecoder().decode(chunk as Uint8Array);
144
- void args;
145
- return true;
146
- };
147
- try {
148
- await handleSearch(ctx, ["q"], { fields: "taskId,title" });
149
- } finally {
150
- process.stdout.write = origWrite;
151
- }
152
-
153
- const lines = out.trimEnd().split("\n").filter((l) => l.length > 0);
154
- expect(lines.length).toBe(1);
155
- expect(JSON.parse(lines[0]!)).toEqual({ taskId: 9, title: "T" });
156
- });
157
-
158
- test("human format prints a fixed-width table", async () => {
159
- syncCliOutputFormatFromGlobals({ format: "human" });
160
- const hits: SearchHit[] = [
161
- {
162
- boardId: 1,
163
- boardSlug: "b",
164
- taskId: 1,
165
- boardName: "B",
166
- listId: 2,
167
- listName: "L",
168
- title: "T",
169
- snippet: "s",
170
- score: 0.2,
171
- },
172
- ];
173
- const body: PaginatedListBody<SearchHit> = {
174
- items: hits,
175
- total: 1,
176
- limit: 20,
177
- offset: 0,
178
- };
179
- const ctx = mockContext({
180
- fetchApi: (async () => body) as CliContext["fetchApi"],
181
- });
182
-
183
- let out = "";
184
- const origWrite = process.stdout.write.bind(process.stdout);
185
- process.stdout.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
186
- out +=
187
- typeof chunk === "string"
188
- ? chunk
189
- : new TextDecoder().decode(chunk as Uint8Array);
190
- void args;
191
- return true;
192
- };
193
- try {
194
- await handleSearch(ctx, ["q"], {});
195
- } finally {
196
- process.stdout.write = origWrite;
197
- }
198
-
199
- expect(out).toContain("Board");
200
- expect(out).toContain("Snippet");
201
- expect(out).toContain("total 1");
202
- });
203
-
204
- test("omitted --limit sends default limit=20 in request URL (aspect 3 bounded default)", async () => {
205
- let fetchedPath = "";
206
- const emptyBody: PaginatedListBody<SearchHit> = {
207
- items: [],
208
- total: 0,
209
- limit: 20,
210
- offset: 0,
211
- };
212
- const ctx = mockContext({
213
- fetchApi: (async (path) => {
214
- fetchedPath = path;
215
- return emptyBody;
216
- }) as CliContext["fetchApi"],
217
- printJson: () => {},
218
- });
219
-
220
- await handleSearch(ctx, ["term"], {});
221
-
222
- expect(fetchedPath).toContain("limit=20");
223
- });
224
-
225
- test("--quiet — taskId per line", async () => {
226
- syncCliOutputFormatFromGlobals({ format: "ndjson", quiet: true });
227
- const hits: SearchHit[] = [
228
- {
229
- boardId: 1,
230
- boardSlug: "b",
231
- taskId: 77,
232
- boardName: "B",
233
- listId: 1,
234
- listName: "L",
235
- title: "T",
236
- snippet: "s",
237
- score: 0.1,
238
- },
239
- ];
240
- const body: PaginatedListBody<SearchHit> = {
241
- items: hits,
242
- total: 1,
243
- limit: 20,
244
- offset: 0,
245
- };
246
- const ctx = mockContext({
247
- fetchApi: (async () => body) as CliContext["fetchApi"],
248
- });
249
-
250
- let out = "";
251
- const origWrite = process.stdout.write.bind(process.stdout);
252
- process.stdout.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
253
- out +=
254
- typeof chunk === "string"
255
- ? chunk
256
- : new TextDecoder().decode(chunk as Uint8Array);
257
- void args;
258
- return true;
259
- };
260
- try {
261
- await handleSearch(ctx, ["q"], {});
262
- } finally {
263
- process.stdout.write = origWrite;
264
- }
265
-
266
- expect(out.trimEnd()).toBe("77");
267
- });
268
-
269
- test("--limit 50 — URL contains limit=50", async () => {
270
- let fetchedPath = "";
271
- const emptyBody: PaginatedListBody<SearchHit> = {
272
- items: [],
273
- total: 0,
274
- limit: 50,
275
- offset: 0,
276
- };
277
- const ctx = mockContext({
278
- fetchApi: (async (path) => {
279
- fetchedPath = path;
280
- return emptyBody;
281
- }) as CliContext["fetchApi"],
282
- printJson: () => {},
283
- });
284
-
285
- await handleSearch(ctx, ["x"], { limit: "50" });
286
-
287
- expect(fetchedPath).toContain("limit=50");
288
- });
289
-
290
- test("--limit 999 — capped to 500 in URL", async () => {
291
- let fetchedPath = "";
292
- const emptyBody: PaginatedListBody<SearchHit> = {
293
- items: [],
294
- total: 0,
295
- limit: 500,
296
- offset: 0,
297
- };
298
- const ctx = mockContext({
299
- fetchApi: (async (path) => {
300
- fetchedPath = path;
301
- return emptyBody;
302
- }) as CliContext["fetchApi"],
303
- printJson: () => {},
304
- });
305
-
306
- await handleSearch(ctx, ["x"], { limit: "999" });
307
-
308
- expect(fetchedPath).toContain("limit=500");
309
- });
310
-
311
- test("no results — NDJSON empty; human No rows.", async () => {
312
- const emptyBody: PaginatedListBody<SearchHit> = {
313
- items: [],
314
- total: 0,
315
- limit: 20,
316
- offset: 0,
317
- };
318
- syncCliOutputFormatFromGlobals({ format: "ndjson", quiet: false });
319
- const ctxNd = mockContext({
320
- fetchApi: (async () => emptyBody) as CliContext["fetchApi"],
321
- });
322
- let outNd = "";
323
- const orig = process.stdout.write.bind(process.stdout);
324
- process.stdout.write = (c: string | Uint8Array, ...a: unknown[]) => {
325
- outNd += typeof c === "string" ? c : new TextDecoder().decode(c);
326
- void a;
327
- return true;
328
- };
329
- try {
330
- await handleSearch(ctxNd, ["q"], {});
331
- } finally {
332
- process.stdout.write = orig;
333
- }
334
- expect(outNd.trim()).toBe("");
335
-
336
- syncCliOutputFormatFromGlobals({ format: "human", quiet: false });
337
- const ctxHu = mockContext({
338
- fetchApi: (async () => emptyBody) as CliContext["fetchApi"],
339
- });
340
- let outHu = "";
341
- process.stdout.write = (c: string | Uint8Array, ...a: unknown[]) => {
342
- outHu += typeof c === "string" ? c : new TextDecoder().decode(c);
343
- void a;
344
- return true;
345
- };
346
- try {
347
- await handleSearch(ctxHu, ["q"], {});
348
- } finally {
349
- process.stdout.write = orig;
350
- }
351
- expect(outHu).toContain("No rows.");
352
- });
353
-
354
- test("--board appears in search URL", async () => {
355
- let fetchedPath = "";
356
- const emptyBody: PaginatedListBody<SearchHit> = {
357
- items: [],
358
- total: 0,
359
- limit: 20,
360
- offset: 0,
361
- };
362
- const ctx = mockContext({
363
- fetchApi: (async (path) => {
364
- fetchedPath = path;
365
- return emptyBody;
366
- }) as CliContext["fetchApi"],
367
- printJson: () => {},
368
- });
369
-
370
- await handleSearch(ctx, ["term"], { board: "my-slug" });
371
-
372
- expect(fetchedPath).toContain("board=my-slug");
373
- });
374
- });
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import type { PaginatedListBody } from "../../shared/pagination";
3
+ import type { SearchHit } from "../../shared/models";
4
+ import { CLI_ERR } from "../types/errors";
5
+ import { syncCliOutputFormatFromGlobals } from "../lib/cliFormat";
6
+ import { createTestCliRuntime } from "../lib/runtime";
7
+ import { resetCliOutputFormat } from "../lib/output";
8
+ import { createDefaultCliContext } from "./context";
9
+ import { handleSearch } from "./search";
10
+ import type { CliContext } from "./context";
11
+
12
+ function mockContext(overrides: Partial<CliContext> = {}): CliContext {
13
+ return {
14
+ ...createDefaultCliContext(),
15
+ resolvePort: () => 3002,
16
+ fetchApi: async () => {
17
+ throw new Error("fetchApi not stubbed");
18
+ },
19
+ fetchApiMutate: async () => {
20
+ throw new Error("fetchApiMutate not stubbed");
21
+ },
22
+ fetchApiTrashMutate: async () => {
23
+ throw new Error("fetchApiTrashMutate not stubbed");
24
+ },
25
+ printJson: () => {},
26
+ getRuntime: () => createTestCliRuntime({ port: 3002 }),
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ describe("handleSearch", () => {
32
+ afterEach(() => {
33
+ resetCliOutputFormat();
34
+ });
35
+
36
+ test("requires non-empty query", async () => {
37
+ const ctx = mockContext();
38
+ await expect(handleSearch(ctx, ["", " "], {})).rejects.toMatchObject({
39
+ exitCode: 2,
40
+ details: expect.objectContaining({ code: CLI_ERR.missingRequired }),
41
+ });
42
+ });
43
+
44
+ test("ndjson output prints one JSON object per hit line", async () => {
45
+ const hits: SearchHit[] = [
46
+ {
47
+ boardId: 1,
48
+ boardSlug: "b",
49
+ taskId: 9,
50
+ boardName: "B",
51
+ listId: 2,
52
+ listName: "L",
53
+ title: "T",
54
+ snippet: "s",
55
+ score: 0.1,
56
+ },
57
+ ];
58
+ const body: PaginatedListBody<SearchHit> = {
59
+ items: hits,
60
+ total: hits.length,
61
+ limit: 5,
62
+ offset: 0,
63
+ };
64
+ let fetchedPath = "";
65
+ const ctx = mockContext({
66
+ fetchApi: (async (path) => {
67
+ fetchedPath = path;
68
+ return body;
69
+ }) as CliContext["fetchApi"],
70
+ });
71
+
72
+ let out = "";
73
+ const origWrite = process.stdout.write.bind(process.stdout);
74
+ process.stdout.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
75
+ out +=
76
+ typeof chunk === "string"
77
+ ? chunk
78
+ : new TextDecoder().decode(chunk as Uint8Array);
79
+ void args;
80
+ return true;
81
+ };
82
+ try {
83
+ await handleSearch(ctx, ["hello", "world"], {
84
+ board: "b1",
85
+ limit: "5",
86
+ noPrefix: true,
87
+ });
88
+ } finally {
89
+ process.stdout.write = origWrite;
90
+ }
91
+
92
+ expect(fetchedPath).toContain("/search?");
93
+ expect(fetchedPath).toContain("q=hello+world");
94
+ expect(fetchedPath).toContain("limit=5");
95
+ expect(fetchedPath).toContain("board=b1");
96
+ expect(fetchedPath).toContain("prefix=0");
97
+ const lines = out.trimEnd().split("\n").filter((l) => l.length > 0);
98
+ expect(lines.length).toBe(1);
99
+ expect(JSON.parse(lines[0]!)).toEqual(hits[0]);
100
+ });
101
+
102
+ test("rejects --fields when global format is human", async () => {
103
+ syncCliOutputFormatFromGlobals({ format: "human" });
104
+ const ctx = mockContext();
105
+ await expect(
106
+ handleSearch(ctx, ["q"], { fields: "taskId" }),
107
+ ).rejects.toMatchObject({
108
+ exitCode: 2,
109
+ details: expect.objectContaining({ code: CLI_ERR.invalidValue }),
110
+ });
111
+ });
112
+
113
+ test("ndjson --fields projects items", async () => {
114
+ const hits: SearchHit[] = [
115
+ {
116
+ boardId: 1,
117
+ boardSlug: "b",
118
+ taskId: 9,
119
+ boardName: "B",
120
+ listId: 2,
121
+ listName: "L",
122
+ title: "T",
123
+ snippet: "s",
124
+ score: 0.1,
125
+ },
126
+ ];
127
+ const body: PaginatedListBody<SearchHit> = {
128
+ items: hits,
129
+ total: 1,
130
+ limit: 20,
131
+ offset: 0,
132
+ };
133
+ const ctx = mockContext({
134
+ fetchApi: (async () => body) as CliContext["fetchApi"],
135
+ });
136
+
137
+ let out = "";
138
+ const origWrite = process.stdout.write.bind(process.stdout);
139
+ process.stdout.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
140
+ out +=
141
+ typeof chunk === "string"
142
+ ? chunk
143
+ : new TextDecoder().decode(chunk as Uint8Array);
144
+ void args;
145
+ return true;
146
+ };
147
+ try {
148
+ await handleSearch(ctx, ["q"], { fields: "taskId,title" });
149
+ } finally {
150
+ process.stdout.write = origWrite;
151
+ }
152
+
153
+ const lines = out.trimEnd().split("\n").filter((l) => l.length > 0);
154
+ expect(lines.length).toBe(1);
155
+ expect(JSON.parse(lines[0]!)).toEqual({ taskId: 9, title: "T" });
156
+ });
157
+
158
+ test("human format prints a fixed-width table", async () => {
159
+ syncCliOutputFormatFromGlobals({ format: "human" });
160
+ const hits: SearchHit[] = [
161
+ {
162
+ boardId: 1,
163
+ boardSlug: "b",
164
+ taskId: 1,
165
+ boardName: "B",
166
+ listId: 2,
167
+ listName: "L",
168
+ title: "T",
169
+ snippet: "s",
170
+ score: 0.2,
171
+ },
172
+ ];
173
+ const body: PaginatedListBody<SearchHit> = {
174
+ items: hits,
175
+ total: 1,
176
+ limit: 20,
177
+ offset: 0,
178
+ };
179
+ const ctx = mockContext({
180
+ fetchApi: (async () => body) as CliContext["fetchApi"],
181
+ });
182
+
183
+ let out = "";
184
+ const origWrite = process.stdout.write.bind(process.stdout);
185
+ process.stdout.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
186
+ out +=
187
+ typeof chunk === "string"
188
+ ? chunk
189
+ : new TextDecoder().decode(chunk as Uint8Array);
190
+ void args;
191
+ return true;
192
+ };
193
+ try {
194
+ await handleSearch(ctx, ["q"], {});
195
+ } finally {
196
+ process.stdout.write = origWrite;
197
+ }
198
+
199
+ expect(out).toContain("Board");
200
+ expect(out).toContain("Snippet");
201
+ expect(out).toContain("total 1");
202
+ });
203
+
204
+ test("omitted --limit sends default limit=20 in request URL (aspect 3 bounded default)", async () => {
205
+ let fetchedPath = "";
206
+ const emptyBody: PaginatedListBody<SearchHit> = {
207
+ items: [],
208
+ total: 0,
209
+ limit: 20,
210
+ offset: 0,
211
+ };
212
+ const ctx = mockContext({
213
+ fetchApi: (async (path) => {
214
+ fetchedPath = path;
215
+ return emptyBody;
216
+ }) as CliContext["fetchApi"],
217
+ printJson: () => {},
218
+ });
219
+
220
+ await handleSearch(ctx, ["term"], {});
221
+
222
+ expect(fetchedPath).toContain("limit=20");
223
+ });
224
+
225
+ test("--quiet — taskId per line", async () => {
226
+ syncCliOutputFormatFromGlobals({ format: "ndjson", quiet: true });
227
+ const hits: SearchHit[] = [
228
+ {
229
+ boardId: 1,
230
+ boardSlug: "b",
231
+ taskId: 77,
232
+ boardName: "B",
233
+ listId: 1,
234
+ listName: "L",
235
+ title: "T",
236
+ snippet: "s",
237
+ score: 0.1,
238
+ },
239
+ ];
240
+ const body: PaginatedListBody<SearchHit> = {
241
+ items: hits,
242
+ total: 1,
243
+ limit: 20,
244
+ offset: 0,
245
+ };
246
+ const ctx = mockContext({
247
+ fetchApi: (async () => body) as CliContext["fetchApi"],
248
+ });
249
+
250
+ let out = "";
251
+ const origWrite = process.stdout.write.bind(process.stdout);
252
+ process.stdout.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
253
+ out +=
254
+ typeof chunk === "string"
255
+ ? chunk
256
+ : new TextDecoder().decode(chunk as Uint8Array);
257
+ void args;
258
+ return true;
259
+ };
260
+ try {
261
+ await handleSearch(ctx, ["q"], {});
262
+ } finally {
263
+ process.stdout.write = origWrite;
264
+ }
265
+
266
+ expect(out.trimEnd()).toBe("77");
267
+ });
268
+
269
+ test("--limit 50 — URL contains limit=50", async () => {
270
+ let fetchedPath = "";
271
+ const emptyBody: PaginatedListBody<SearchHit> = {
272
+ items: [],
273
+ total: 0,
274
+ limit: 50,
275
+ offset: 0,
276
+ };
277
+ const ctx = mockContext({
278
+ fetchApi: (async (path) => {
279
+ fetchedPath = path;
280
+ return emptyBody;
281
+ }) as CliContext["fetchApi"],
282
+ printJson: () => {},
283
+ });
284
+
285
+ await handleSearch(ctx, ["x"], { limit: "50" });
286
+
287
+ expect(fetchedPath).toContain("limit=50");
288
+ });
289
+
290
+ test("--limit 999 — capped to 500 in URL", async () => {
291
+ let fetchedPath = "";
292
+ const emptyBody: PaginatedListBody<SearchHit> = {
293
+ items: [],
294
+ total: 0,
295
+ limit: 500,
296
+ offset: 0,
297
+ };
298
+ const ctx = mockContext({
299
+ fetchApi: (async (path) => {
300
+ fetchedPath = path;
301
+ return emptyBody;
302
+ }) as CliContext["fetchApi"],
303
+ printJson: () => {},
304
+ });
305
+
306
+ await handleSearch(ctx, ["x"], { limit: "999" });
307
+
308
+ expect(fetchedPath).toContain("limit=500");
309
+ });
310
+
311
+ test("no results — NDJSON empty; human No rows.", async () => {
312
+ const emptyBody: PaginatedListBody<SearchHit> = {
313
+ items: [],
314
+ total: 0,
315
+ limit: 20,
316
+ offset: 0,
317
+ };
318
+ syncCliOutputFormatFromGlobals({ format: "ndjson", quiet: false });
319
+ const ctxNd = mockContext({
320
+ fetchApi: (async () => emptyBody) as CliContext["fetchApi"],
321
+ });
322
+ let outNd = "";
323
+ const orig = process.stdout.write.bind(process.stdout);
324
+ process.stdout.write = (c: string | Uint8Array, ...a: unknown[]) => {
325
+ outNd += typeof c === "string" ? c : new TextDecoder().decode(c);
326
+ void a;
327
+ return true;
328
+ };
329
+ try {
330
+ await handleSearch(ctxNd, ["q"], {});
331
+ } finally {
332
+ process.stdout.write = orig;
333
+ }
334
+ expect(outNd.trim()).toBe("");
335
+
336
+ syncCliOutputFormatFromGlobals({ format: "human", quiet: false });
337
+ const ctxHu = mockContext({
338
+ fetchApi: (async () => emptyBody) as CliContext["fetchApi"],
339
+ });
340
+ let outHu = "";
341
+ process.stdout.write = (c: string | Uint8Array, ...a: unknown[]) => {
342
+ outHu += typeof c === "string" ? c : new TextDecoder().decode(c);
343
+ void a;
344
+ return true;
345
+ };
346
+ try {
347
+ await handleSearch(ctxHu, ["q"], {});
348
+ } finally {
349
+ process.stdout.write = orig;
350
+ }
351
+ expect(outHu).toContain("No rows.");
352
+ });
353
+
354
+ test("--board appears in search URL", async () => {
355
+ let fetchedPath = "";
356
+ const emptyBody: PaginatedListBody<SearchHit> = {
357
+ items: [],
358
+ total: 0,
359
+ limit: 20,
360
+ offset: 0,
361
+ };
362
+ const ctx = mockContext({
363
+ fetchApi: (async (path) => {
364
+ fetchedPath = path;
365
+ return emptyBody;
366
+ }) as CliContext["fetchApi"],
367
+ printJson: () => {},
368
+ });
369
+
370
+ await handleSearch(ctx, ["term"], { board: "my-slug" });
371
+
372
+ expect(fetchedPath).toContain("board=my-slug");
373
+ });
374
+ });