@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,154 +1,161 @@
1
- import { Command } from "commander";
2
- import { CLI_DEFAULTS } from "./constants";
3
- import { CLI_ERR } from "../types/errors";
4
- import { getCliOutputFormat, getCliQuiet } from "./cliFormat";
5
- import { CliError, exitWithError } from "./output";
6
-
7
- /** Shared Commander option helpers — Phase 1 CLI split from monolithic index.ts. */
8
-
9
- /** Help text for read/list `--fields` (subset projection; see `jsonFieldProjection.ts` allowlists). */
10
- export const CLI_FIELDS_OPTION_DESC =
11
- "Comma-separated API keys per row (unknown keys exit 2); use with global --format ndjson, not human tables.";
12
-
13
- /** `boards describe --entities`: list,group,priority,release,status,meta — order controls CLI stdout (and HTTP when subset). */
14
- export const CLI_BOARD_DESCRIBE_ENTITIES_DESC =
15
- "Comma-separated: list,group,priority,release,status,meta (omit for default five sections, no meta). Board + cliPolicy always in JSON; duplicate or unknown tokens exit 2; board is not a token.";
16
-
17
- /** Guarded deletes / structure replaces: paired with `confirmMutableAction` in handlers. */
18
- export const CLI_YES_OPTION_DESC = "Skip the confirmation prompt";
19
-
20
- /** Attach `-y` / `--yes` for commands that call `confirmMutableAction`. */
21
- export function addYesOption(command: Command): Command {
22
- return command.option("-y, --yes", CLI_YES_OPTION_DESC);
23
- }
24
-
25
- /** Human tables cannot apply arbitrary `--fields`; enforce before fetch. */
26
- export function requireNdjsonWhenUsingFields(
27
- fieldKeys: string[] | undefined,
28
- ): void {
29
- if (
30
- fieldKeys != null &&
31
- fieldKeys.length > 0 &&
32
- getCliOutputFormat() === "human"
33
- ) {
34
- throw new CliError("--fields requires global --format ndjson", 2, {
35
- code: CLI_ERR.invalidValue,
36
- });
37
- }
38
- }
39
-
40
- /** Pipe-friendly `--quiet` stdout is not JSON; same constraint as `--fields` vs human tables. */
41
- export function requireNdjsonWhenQuiet(): void {
42
- if (getCliQuiet() && getCliOutputFormat() === "human") {
43
- throw new CliError("--quiet requires global --format ndjson", 2, {
44
- code: CLI_ERR.invalidValue,
45
- });
46
- }
47
- }
48
-
49
- /**
50
- * With global `--quiet`, `--fields` may supply exactly one key (that column per line).
51
- * Multiple keys are ambiguous for single-column output.
52
- */
53
- export function resolveQuietExplicitField(
54
- fieldKeys: string[] | undefined,
55
- ): string | undefined {
56
- if (!getCliQuiet() || fieldKeys == null || fieldKeys.length === 0) {
57
- return undefined;
58
- }
59
- if (fieldKeys.length > 1) {
60
- throw new CliError("--quiet allows at most one --fields key", 2, {
61
- code: CLI_ERR.invalidValue,
62
- });
63
- }
64
- return fieldKeys[0];
65
- }
66
-
67
- /** Attach `--client-name` for commands that call the HTTP API (mutations send the label). */
68
- export function addClientNameOption(command: Command): Command {
69
- return command.option(
70
- "--client-name <name>",
71
- "Human-friendly client label sent with API requests (for notifications)",
72
- );
73
- }
74
-
75
- export function addProfileOption(command: Command): Command {
76
- return command.option(
77
- "--profile <name>",
78
- "Runtime profile name for this command",
79
- );
80
- }
81
-
82
- export function parsePortOption(port: string | undefined): number | undefined {
83
- if (!port?.trim()) return undefined;
84
-
85
- const parsed = Number(port);
86
- if (!Number.isInteger(parsed) || parsed <= 0) {
87
- throw new CliError("Invalid port", 2, { code: CLI_ERR.invalidValue, port });
88
- }
89
-
90
- return parsed;
91
- }
92
-
93
- export function collectMultiValue(
94
- value: string,
95
- previous: string[] = [],
96
- ): string[] {
97
- return [
98
- ...previous,
99
- ...value
100
- .split(",")
101
- .map((entry) => entry.trim())
102
- .filter((entry) => entry.length > 0),
103
- ];
104
- }
105
-
106
- /** Search: default and cap from `CLI_DEFAULTS` (same max as optional list page-all). */
107
- export function parseLimitOption(limit: string | undefined): number {
108
- if (limit == null || limit === "") return CLI_DEFAULTS.DEFAULT_SEARCH_LIMIT;
109
- const n = Number(limit);
110
- if (!Number.isInteger(n) || n < 1) {
111
- throw new CliError("Invalid limit", 2, { code: CLI_ERR.invalidValue, limit });
112
- }
113
- return Math.min(CLI_DEFAULTS.MAX_PAGE_LIMIT, n);
114
- }
115
-
116
- /** Optional list limit for tasks/trash/boards/releases; omit = no `limit` param (one full page). */
117
- export function parseOptionalListLimit(
118
- limit: string | undefined,
119
- ): number | null {
120
- if (limit == null || limit === "") return null;
121
- const n = Number(limit);
122
- if (!Number.isInteger(n) || n < 1) {
123
- throw new CliError("Invalid limit", 2, { code: CLI_ERR.invalidValue, limit });
124
- }
125
- return Math.min(CLI_DEFAULTS.MAX_PAGE_LIMIT, n);
126
- }
127
-
128
- export function parseOptionalOffset(offset: string | undefined): number {
129
- if (offset == null || offset === "") return 0;
130
- const n = Number(offset);
131
- if (!Number.isInteger(n) || n < 0) {
132
- throw new CliError("Invalid offset", 2, { code: CLI_ERR.invalidValue, offset });
133
- }
134
- return n;
135
- }
136
-
137
- /** Wrap handler execution so Commander actions share one exit path (Phase 2). */
138
- export async function withCliErrors(fn: () => Promise<void>): Promise<void> {
139
- try {
140
- await fn();
141
- } catch (error) {
142
- exitWithError(error);
143
- }
144
- }
145
-
146
- /**
147
- * Commander `.action()` helper: runs the handler inside {@link withCliErrors} so
148
- * subcommands cannot forget the shared exit path (see cli-architecture-review item 14).
149
- */
150
- export function cliAction<A extends unknown[]>(
151
- fn: (...args: A) => Promise<void>,
152
- ): (...args: A) => Promise<void> {
153
- return (...args: A) => withCliErrors(() => fn(...args));
154
- }
1
+ import { Command } from "commander";
2
+ import { CLI_DEFAULTS } from "./constants";
3
+ import { CLI_ERR } from "../types/errors";
4
+ import { getCliOutputFormat, getCliQuiet } from "./cliFormat";
5
+ import { CliError, exitWithError } from "./output";
6
+
7
+ /** Shared Commander option helpers — Phase 1 CLI split from monolithic index.ts. */
8
+
9
+ /** Help text for read/list `--fields` (subset projection; see `jsonFieldProjection.ts` allowlists). */
10
+ export const CLI_FIELDS_OPTION_DESC =
11
+ "Comma-separated API keys per row (unknown keys exit 2); use with global --format ndjson, not human tables.";
12
+
13
+ /** `boards describe --entities`: list,group,priority,release,status,meta — order controls CLI stdout (and HTTP when subset). */
14
+ export const CLI_BOARD_DESCRIBE_ENTITIES_DESC =
15
+ "Comma-separated: list,group,priority,release,status,meta (omit for default five sections, no meta). Board + cliPolicy always in JSON; duplicate or unknown tokens exit 2; board is not a token.";
16
+
17
+ /** Guarded deletes / structure replaces: paired with `confirmMutableAction` in handlers. */
18
+ export const CLI_YES_OPTION_DESC = "Skip the confirmation prompt";
19
+
20
+ /** Attach `-y` / `--yes` for commands that call `confirmMutableAction`. */
21
+ export function addYesOption(command: Command): Command {
22
+ return command.option("-y, --yes", CLI_YES_OPTION_DESC);
23
+ }
24
+
25
+ /** Human tables cannot apply arbitrary `--fields`; enforce before fetch. */
26
+ export function requireNdjsonWhenUsingFields(
27
+ fieldKeys: string[] | undefined,
28
+ ): void {
29
+ if (
30
+ fieldKeys != null &&
31
+ fieldKeys.length > 0 &&
32
+ getCliOutputFormat() === "human"
33
+ ) {
34
+ throw new CliError("--fields requires global --format ndjson", 2, {
35
+ code: CLI_ERR.invalidValue,
36
+ });
37
+ }
38
+ }
39
+
40
+ /** Pipe-friendly `--quiet` stdout is not JSON; same constraint as `--fields` vs human tables. */
41
+ export function requireNdjsonWhenQuiet(): void {
42
+ if (getCliQuiet() && getCliOutputFormat() === "human") {
43
+ throw new CliError("--quiet requires global --format ndjson", 2, {
44
+ code: CLI_ERR.invalidValue,
45
+ });
46
+ }
47
+ }
48
+
49
+ /**
50
+ * With global `--quiet`, `--fields` may supply exactly one key (that column per line).
51
+ * Multiple keys are ambiguous for single-column output.
52
+ */
53
+ export function resolveQuietExplicitField(
54
+ fieldKeys: string[] | undefined,
55
+ ): string | undefined {
56
+ if (!getCliQuiet() || fieldKeys == null || fieldKeys.length === 0) {
57
+ return undefined;
58
+ }
59
+ if (fieldKeys.length > 1) {
60
+ throw new CliError("--quiet allows at most one --fields key", 2, {
61
+ code: CLI_ERR.invalidValue,
62
+ });
63
+ }
64
+ return fieldKeys[0];
65
+ }
66
+
67
+ /** Attach `--client-name` for commands that call the HTTP API (mutations send the label). */
68
+ export function addClientNameOption(command: Command): Command {
69
+ return command.option(
70
+ "--client-name <name>",
71
+ "Human-friendly client label sent with API requests (for notifications)",
72
+ );
73
+ }
74
+
75
+ export function addProfileOption(command: Command): Command {
76
+ return command.option(
77
+ "--profile <name>",
78
+ "Runtime profile name for this command",
79
+ );
80
+ }
81
+
82
+ export function addDevOption(command: Command): Command {
83
+ return command.option(
84
+ "--dev",
85
+ "Run in dev mode (API-only with dev CORS, port 3002 default)",
86
+ );
87
+ }
88
+
89
+ export function parsePortOption(port: string | undefined): number | undefined {
90
+ if (!port?.trim()) return undefined;
91
+
92
+ const parsed = Number(port);
93
+ if (!Number.isInteger(parsed) || parsed <= 0) {
94
+ throw new CliError("Invalid port", 2, { code: CLI_ERR.invalidValue, port });
95
+ }
96
+
97
+ return parsed;
98
+ }
99
+
100
+ export function collectMultiValue(
101
+ value: string,
102
+ previous: string[] = [],
103
+ ): string[] {
104
+ return [
105
+ ...previous,
106
+ ...value
107
+ .split(",")
108
+ .map((entry) => entry.trim())
109
+ .filter((entry) => entry.length > 0),
110
+ ];
111
+ }
112
+
113
+ /** Search: default and cap from `CLI_DEFAULTS` (same max as optional list page-all). */
114
+ export function parseLimitOption(limit: string | undefined): number {
115
+ if (limit == null || limit === "") return CLI_DEFAULTS.DEFAULT_SEARCH_LIMIT;
116
+ const n = Number(limit);
117
+ if (!Number.isInteger(n) || n < 1) {
118
+ throw new CliError("Invalid limit", 2, { code: CLI_ERR.invalidValue, limit });
119
+ }
120
+ return Math.min(CLI_DEFAULTS.MAX_PAGE_LIMIT, n);
121
+ }
122
+
123
+ /** Optional list limit for tasks/trash/boards/releases; omit = no `limit` param (one full page). */
124
+ export function parseOptionalListLimit(
125
+ limit: string | undefined,
126
+ ): number | null {
127
+ if (limit == null || limit === "") return null;
128
+ const n = Number(limit);
129
+ if (!Number.isInteger(n) || n < 1) {
130
+ throw new CliError("Invalid limit", 2, { code: CLI_ERR.invalidValue, limit });
131
+ }
132
+ return Math.min(CLI_DEFAULTS.MAX_PAGE_LIMIT, n);
133
+ }
134
+
135
+ export function parseOptionalOffset(offset: string | undefined): number {
136
+ if (offset == null || offset === "") return 0;
137
+ const n = Number(offset);
138
+ if (!Number.isInteger(n) || n < 0) {
139
+ throw new CliError("Invalid offset", 2, { code: CLI_ERR.invalidValue, offset });
140
+ }
141
+ return n;
142
+ }
143
+
144
+ /** Wrap handler execution so Commander actions share one exit path (Phase 2). */
145
+ export async function withCliErrors(fn: () => Promise<void>): Promise<void> {
146
+ try {
147
+ await fn();
148
+ } catch (error) {
149
+ exitWithError(error);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Commander `.action()` helper: runs the handler inside {@link withCliErrors} so
155
+ * subcommands cannot forget the shared exit path (see cli-architecture-review item 14).
156
+ */
157
+ export function cliAction<A extends unknown[]>(
158
+ fn: (...args: A) => Promise<void>,
159
+ ): (...args: A) => Promise<void> {
160
+ return (...args: A) => withCliErrors(() => fn(...args));
161
+ }
@@ -28,6 +28,10 @@ export function setRuntimeProfile(profile: string | undefined): void {
28
28
  setRuntimeConfigSelection({ profile });
29
29
  }
30
30
 
31
+ export function setRuntimeCliPort(port: number | undefined): void {
32
+ setRuntimeConfigSelection({ port });
33
+ }
34
+
31
35
  export function setRuntimeKind(kind: RuntimeKind | undefined): void {
32
36
  setRuntimeConfigSelection({ kind });
33
37
  }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * hirotaskmanager interactive setup: compact prompts, inline spinners, and colored values.
3
+ * Uses ANSI only when stdout is a TTY and NO_COLOR is unset.
4
+ */
5
+ import { existsSync } from "node:fs";
6
+ import path from "node:path";
7
+ import { ansi, paint } from "../../shared/terminalColors";
8
+
9
+ const out = process.stdout;
10
+ const SPINNER_FRAMES = ["-", "\\", "|", "/"] as const;
11
+ const SPINNER_INTERVAL_MS = 100;
12
+ const DEFAULT_SPINNER_MS = 900;
13
+
14
+ function line(text = ""): void {
15
+ console.log(text);
16
+ }
17
+
18
+ function clearInline(): void {
19
+ if (typeof out.clearLine === "function" && typeof out.cursorTo === "function") {
20
+ out.clearLine(0);
21
+ out.cursorTo(0);
22
+ }
23
+ }
24
+
25
+ function writeInline(text: string): void {
26
+ clearInline();
27
+ out.write(text);
28
+ }
29
+
30
+ export function paintValue(value: string | number | boolean): string {
31
+ return paint(out, String(value), ansi.bold + ansi.cyan);
32
+ }
33
+
34
+ function paintSuccess(text: string): string {
35
+ return paint(out, text, ansi.bold + ansi.green);
36
+ }
37
+
38
+ function paintWarning(text: string): string {
39
+ return paint(out, text, ansi.bold + ansi.yellow);
40
+ }
41
+
42
+ export type SpinnerHandle = {
43
+ stop: (finalText?: string | null) => void;
44
+ };
45
+
46
+ export function startInlineSpinner(message: string): SpinnerHandle {
47
+ if (!out.isTTY) {
48
+ line(message);
49
+ return {
50
+ stop(finalText?: string | null): void {
51
+ if (finalText) line(finalText);
52
+ },
53
+ };
54
+ }
55
+
56
+ let frameIndex = 0;
57
+ let stopped = false;
58
+ writeInline(`${message} ${paint(out, SPINNER_FRAMES[frameIndex], ansi.dim)}`);
59
+ const timer = setInterval(() => {
60
+ frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
61
+ writeInline(`${message} ${paint(out, SPINNER_FRAMES[frameIndex], ansi.dim)}`);
62
+ }, SPINNER_INTERVAL_MS);
63
+
64
+ return {
65
+ stop(finalText?: string | null): void {
66
+ if (stopped) return;
67
+ stopped = true;
68
+ clearInterval(timer);
69
+ clearInline();
70
+ if (finalText) out.write(`${finalText}\n`);
71
+ },
72
+ };
73
+ }
74
+
75
+ export async function spinForMoment(
76
+ message: string,
77
+ finalText = message,
78
+ durationMs = DEFAULT_SPINNER_MS,
79
+ ): Promise<void> {
80
+ const spinner = startInlineSpinner(message);
81
+ await Bun.sleep(durationMs);
82
+ spinner.stop(finalText);
83
+ }
84
+
85
+ export function formatTextPrompt(label: string, defaultValue: string): string {
86
+ return `${label}: ${paintValue(`[${defaultValue}]`)}`;
87
+ }
88
+
89
+ export function formatBooleanPrompt(
90
+ label: string,
91
+ defaultValue: boolean,
92
+ ): string {
93
+ return `${label} ${paintValue(`[${defaultValue ? "Y/n" : "y/N"}]`)}`;
94
+ }
95
+
96
+ export function printInteractiveSetupHeader(opts: {
97
+ profileName: string;
98
+ firstProfileOnMachine: boolean;
99
+ }): void {
100
+ // Keep the setup header to one line so first run stays dense and scannable.
101
+ line(
102
+ paint(
103
+ out,
104
+ opts.firstProfileOnMachine
105
+ ? "Hiro Task Manager - First-time Setup..."
106
+ : `Hiro Task Manager - Profile Setup: ${opts.profileName}`,
107
+ ansi.bold + ansi.cyan,
108
+ ),
109
+ );
110
+ }
111
+
112
+ export function printSavedProfileSummary(opts: {
113
+ created: boolean;
114
+ profileName: string;
115
+ appUrl: string;
116
+ dataDir: string;
117
+ openBrowser: boolean;
118
+ }): void {
119
+ line(
120
+ `${opts.created ? paintSuccess("Profile Created:") : paintSuccess("Profile Saved:")} ${paintValue(opts.profileName)}`,
121
+ );
122
+ line(` ${paintWarning("App URL:")} ${paintValue(opts.appUrl)}`);
123
+ line(` ${paintWarning("Data Path:")} ${paintValue(opts.dataDir)}`);
124
+ line(
125
+ ` ${paintWarning("Open Browser:")} ${paintValue(opts.openBrowser ? "Yes" : "No")}`,
126
+ );
127
+ }
128
+
129
+ export async function printPassphraseHint(): Promise<void> {
130
+ // Keep the browser handoff to one line because the recovery key prints later from the server.
131
+ const text =
132
+ "Create your passphrase in the browser. Your recovery key prints here once.";
133
+ await spinForMoment(text, text);
134
+ }
135
+
136
+ export function printRecoveryKey(recoveryKey: string): void {
137
+ line();
138
+ line(paintWarning("Recovery Key:"));
139
+ line(paintValue(recoveryKey));
140
+ line(
141
+ paint(
142
+ out,
143
+ "Store it on a separate device. It will never show again.",
144
+ ansi.dim,
145
+ ),
146
+ );
147
+ line(
148
+ paint(
149
+ out,
150
+ "Use it to recover your passphrase and access your server/data.",
151
+ ansi.dim,
152
+ ),
153
+ );
154
+ line();
155
+ }
156
+
157
+ export function printRecoveryKeyExitHint(appUrl: string): void {
158
+ // Explain why Enter returns to the shell without implying the server is stopping.
159
+ line(
160
+ `After you copy the recovery key, press Enter to close this launcher. TaskManager stays running at ${paintValue(appUrl)}.`,
161
+ );
162
+ }
163
+
164
+ export function isAuthInitialized(authDir: string): boolean {
165
+ return existsSync(path.join(authDir, "auth.json"));
166
+ }
@@ -52,7 +52,14 @@ describe("process.ts server lifecycle (mock fetch)", () => {
52
52
  setMockFetch(async (input: RequestInfo | URL) => {
53
53
  const url = typeof input === "string" ? input : (input as Request).url;
54
54
  expect(url).toContain("/api/health");
55
- return new Response(JSON.stringify({ ok: true }), {
55
+ return new Response(JSON.stringify({
56
+ pid: 1001,
57
+ port: 17_001,
58
+ running: true,
59
+ runtime: "installed",
60
+ source: "installed",
61
+ url: "http://127.0.0.1:17001",
62
+ }), {
56
63
  status: 200,
57
64
  headers: { "content-type": "application/json" },
58
65
  });
@@ -60,15 +67,18 @@ describe("process.ts server lifecycle (mock fetch)", () => {
60
67
 
61
68
  const status = await readServerStatus({ port: 17_001 });
62
69
  expect(status).toEqual({
70
+ pid: 1001,
63
71
  running: true,
64
72
  port: 17_001,
73
+ runtime: "installed",
74
+ source: "installed",
65
75
  url: "http://127.0.0.1:17001",
66
76
  });
67
77
  });
68
78
 
69
79
  test("readServerStatus — no health, no pid file", async () => {
70
80
  setMockFetch(async () =>
71
- new Response(JSON.stringify({ ok: false }), {
81
+ new Response(JSON.stringify({ running: false }), {
72
82
  status: 200,
73
83
  headers: { "content-type": "application/json" },
74
84
  }),
@@ -93,7 +103,7 @@ describe("process.ts server lifecycle (mock fetch)", () => {
93
103
  expect(existsSync(pidPath)).toBe(true);
94
104
 
95
105
  setMockFetch(async () =>
96
- new Response(JSON.stringify({ ok: false }), {
106
+ new Response(JSON.stringify({ running: false }), {
97
107
  status: 200,
98
108
  headers: { "content-type": "application/json" },
99
109
  }),
@@ -120,16 +130,25 @@ describe("process.ts server lifecycle (mock fetch)", () => {
120
130
  setMockFetch(async (input: RequestInfo | URL) => {
121
131
  const url = typeof input === "string" ? input : (input as Request).url;
122
132
  expect(url).toContain("/api/health");
123
- return new Response(JSON.stringify({ ok: true }), {
133
+ return new Response(JSON.stringify({
134
+ pid: process.pid,
135
+ port,
136
+ running: true,
137
+ runtime: "dev",
138
+ source: "repo",
139
+ url: `http://127.0.0.1:${port}`,
140
+ }), {
124
141
  status: 200,
125
142
  headers: { "content-type": "application/json" },
126
143
  });
127
144
  });
128
145
 
129
146
  const status = await readServerStatus({ port });
130
- expect(status.running).toBe(true);
147
+ if (!status.running) expect.unreachable();
131
148
  expect(status.port).toBe(port);
132
149
  expect(status.pid).toBe(process.pid);
150
+ expect(status.runtime).toBe("dev");
151
+ expect(status.source).toBe("repo");
133
152
  expect(status.url).toBe(`http://127.0.0.1:${port}`);
134
153
  });
135
154