hub-launch 1.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 (331) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/README.md +195 -0
  3. package/bin/hublaunch +20 -0
  4. package/dist/commands/checkout.d.ts +7 -0
  5. package/dist/commands/checkout.d.ts.map +1 -0
  6. package/dist/commands/checkout.js +164 -0
  7. package/dist/commands/checkout.js.map +1 -0
  8. package/dist/commands/clean.d.ts +7 -0
  9. package/dist/commands/clean.d.ts.map +1 -0
  10. package/dist/commands/clean.js +206 -0
  11. package/dist/commands/clean.js.map +1 -0
  12. package/dist/commands/close.d.ts +7 -0
  13. package/dist/commands/close.d.ts.map +1 -0
  14. package/dist/commands/close.js +109 -0
  15. package/dist/commands/close.js.map +1 -0
  16. package/dist/commands/create.d.ts +7 -0
  17. package/dist/commands/create.d.ts.map +1 -0
  18. package/dist/commands/create.js +367 -0
  19. package/dist/commands/create.js.map +1 -0
  20. package/dist/commands/diff.d.ts +7 -0
  21. package/dist/commands/diff.d.ts.map +1 -0
  22. package/dist/commands/diff.js +98 -0
  23. package/dist/commands/diff.js.map +1 -0
  24. package/dist/commands/execute.d.ts +7 -0
  25. package/dist/commands/execute.d.ts.map +1 -0
  26. package/dist/commands/execute.js +376 -0
  27. package/dist/commands/execute.js.map +1 -0
  28. package/dist/commands/init.d.ts +8 -0
  29. package/dist/commands/init.d.ts.map +1 -0
  30. package/dist/commands/init.js +1243 -0
  31. package/dist/commands/init.js.map +1 -0
  32. package/dist/commands/interactive.d.ts +12 -0
  33. package/dist/commands/interactive.d.ts.map +1 -0
  34. package/dist/commands/interactive.js +1071 -0
  35. package/dist/commands/interactive.js.map +1 -0
  36. package/dist/commands/launch.d.ts +16 -0
  37. package/dist/commands/launch.d.ts.map +1 -0
  38. package/dist/commands/launch.js +684 -0
  39. package/dist/commands/launch.js.map +1 -0
  40. package/dist/commands/login.d.ts +40 -0
  41. package/dist/commands/login.d.ts.map +1 -0
  42. package/dist/commands/login.js +469 -0
  43. package/dist/commands/login.js.map +1 -0
  44. package/dist/commands/logs.d.ts +22 -0
  45. package/dist/commands/logs.d.ts.map +1 -0
  46. package/dist/commands/logs.js +105 -0
  47. package/dist/commands/logs.js.map +1 -0
  48. package/dist/commands/merge.d.ts +7 -0
  49. package/dist/commands/merge.d.ts.map +1 -0
  50. package/dist/commands/merge.js +240 -0
  51. package/dist/commands/merge.js.map +1 -0
  52. package/dist/commands/preview.d.ts +7 -0
  53. package/dist/commands/preview.d.ts.map +1 -0
  54. package/dist/commands/preview.js +160 -0
  55. package/dist/commands/preview.js.map +1 -0
  56. package/dist/commands/project.d.ts +18 -0
  57. package/dist/commands/project.d.ts.map +1 -0
  58. package/dist/commands/project.js +352 -0
  59. package/dist/commands/project.js.map +1 -0
  60. package/dist/commands/respond.d.ts +7 -0
  61. package/dist/commands/respond.d.ts.map +1 -0
  62. package/dist/commands/respond.js +112 -0
  63. package/dist/commands/respond.js.map +1 -0
  64. package/dist/commands/setPriority.d.ts +7 -0
  65. package/dist/commands/setPriority.d.ts.map +1 -0
  66. package/dist/commands/setPriority.js +112 -0
  67. package/dist/commands/setPriority.js.map +1 -0
  68. package/dist/commands/track.d.ts +7 -0
  69. package/dist/commands/track.d.ts.map +1 -0
  70. package/dist/commands/track.js +140 -0
  71. package/dist/commands/track.js.map +1 -0
  72. package/dist/commands/upload.d.ts +21 -0
  73. package/dist/commands/upload.d.ts.map +1 -0
  74. package/dist/commands/upload.js +145 -0
  75. package/dist/commands/upload.js.map +1 -0
  76. package/dist/commands/view.d.ts +7 -0
  77. package/dist/commands/view.d.ts.map +1 -0
  78. package/dist/commands/view.js +168 -0
  79. package/dist/commands/view.js.map +1 -0
  80. package/dist/commands/watch.d.ts +7 -0
  81. package/dist/commands/watch.d.ts.map +1 -0
  82. package/dist/commands/watch.js +438 -0
  83. package/dist/commands/watch.js.map +1 -0
  84. package/dist/commands/worktree.d.ts +7 -0
  85. package/dist/commands/worktree.d.ts.map +1 -0
  86. package/dist/commands/worktree.js +122 -0
  87. package/dist/commands/worktree.js.map +1 -0
  88. package/dist/config/command-names.d.ts +16 -0
  89. package/dist/config/command-names.d.ts.map +1 -0
  90. package/dist/config/command-names.js +17 -0
  91. package/dist/config/command-names.js.map +1 -0
  92. package/dist/config/constants.d.ts +12 -0
  93. package/dist/config/constants.d.ts.map +1 -0
  94. package/dist/config/constants.js +12 -0
  95. package/dist/config/constants.js.map +1 -0
  96. package/dist/config/index.d.ts +47 -0
  97. package/dist/config/index.d.ts.map +1 -0
  98. package/dist/config/index.js +175 -0
  99. package/dist/config/index.js.map +1 -0
  100. package/dist/index.d.ts +3 -0
  101. package/dist/index.d.ts.map +1 -0
  102. package/dist/index.js +148 -0
  103. package/dist/index.js.map +1 -0
  104. package/dist/services/api/HulaApiClient.d.ts +170 -0
  105. package/dist/services/api/HulaApiClient.d.ts.map +1 -0
  106. package/dist/services/api/HulaApiClient.js +233 -0
  107. package/dist/services/api/HulaApiClient.js.map +1 -0
  108. package/dist/services/diff/DiffService.d.ts +44 -0
  109. package/dist/services/diff/DiffService.d.ts.map +1 -0
  110. package/dist/services/diff/DiffService.js +126 -0
  111. package/dist/services/diff/DiffService.js.map +1 -0
  112. package/dist/services/editor/EditorService.d.ts +53 -0
  113. package/dist/services/editor/EditorService.d.ts.map +1 -0
  114. package/dist/services/editor/EditorService.js +161 -0
  115. package/dist/services/editor/EditorService.js.map +1 -0
  116. package/dist/services/git/GitService.d.ts +124 -0
  117. package/dist/services/git/GitService.d.ts.map +1 -0
  118. package/dist/services/git/GitService.js +423 -0
  119. package/dist/services/git/GitService.js.map +1 -0
  120. package/dist/services/git/WorktreeService.d.ts +144 -0
  121. package/dist/services/git/WorktreeService.d.ts.map +1 -0
  122. package/dist/services/git/WorktreeService.js +505 -0
  123. package/dist/services/git/WorktreeService.js.map +1 -0
  124. package/dist/services/github/ActionsService.d.ts +66 -0
  125. package/dist/services/github/ActionsService.d.ts.map +1 -0
  126. package/dist/services/github/ActionsService.js +114 -0
  127. package/dist/services/github/ActionsService.js.map +1 -0
  128. package/dist/services/github/AssetUploadService.d.ts +103 -0
  129. package/dist/services/github/AssetUploadService.d.ts.map +1 -0
  130. package/dist/services/github/AssetUploadService.js +299 -0
  131. package/dist/services/github/AssetUploadService.js.map +1 -0
  132. package/dist/services/github/BatchCopilotDetectionService.d.ts +58 -0
  133. package/dist/services/github/BatchCopilotDetectionService.d.ts.map +1 -0
  134. package/dist/services/github/BatchCopilotDetectionService.js +321 -0
  135. package/dist/services/github/BatchCopilotDetectionService.js.map +1 -0
  136. package/dist/services/github/CopilotDetectionService.d.ts +40 -0
  137. package/dist/services/github/CopilotDetectionService.d.ts.map +1 -0
  138. package/dist/services/github/CopilotDetectionService.js +280 -0
  139. package/dist/services/github/CopilotDetectionService.js.map +1 -0
  140. package/dist/services/github/CopilotService.d.ts +27 -0
  141. package/dist/services/github/CopilotService.d.ts.map +1 -0
  142. package/dist/services/github/CopilotService.js +249 -0
  143. package/dist/services/github/CopilotService.js.map +1 -0
  144. package/dist/services/github/DeploymentDetectionService.d.ts +38 -0
  145. package/dist/services/github/DeploymentDetectionService.d.ts.map +1 -0
  146. package/dist/services/github/DeploymentDetectionService.js +104 -0
  147. package/dist/services/github/DeploymentDetectionService.js.map +1 -0
  148. package/dist/services/github/GithubAppService.d.ts +40 -0
  149. package/dist/services/github/GithubAppService.d.ts.map +1 -0
  150. package/dist/services/github/GithubAppService.js +187 -0
  151. package/dist/services/github/GithubAppService.js.map +1 -0
  152. package/dist/services/github/IssueService.d.ts +65 -0
  153. package/dist/services/github/IssueService.d.ts.map +1 -0
  154. package/dist/services/github/IssueService.js +231 -0
  155. package/dist/services/github/IssueService.js.map +1 -0
  156. package/dist/services/github/LabelService.d.ts +36 -0
  157. package/dist/services/github/LabelService.d.ts.map +1 -0
  158. package/dist/services/github/LabelService.js +122 -0
  159. package/dist/services/github/LabelService.js.map +1 -0
  160. package/dist/services/github/PRService.d.ts +77 -0
  161. package/dist/services/github/PRService.d.ts.map +1 -0
  162. package/dist/services/github/PRService.js +287 -0
  163. package/dist/services/github/PRService.js.map +1 -0
  164. package/dist/services/github/ProjectService.d.ts +144 -0
  165. package/dist/services/github/ProjectService.d.ts.map +1 -0
  166. package/dist/services/github/ProjectService.js +804 -0
  167. package/dist/services/github/ProjectService.js.map +1 -0
  168. package/dist/services/hooks/HookExecutor.d.ts +66 -0
  169. package/dist/services/hooks/HookExecutor.d.ts.map +1 -0
  170. package/dist/services/hooks/HookExecutor.js +227 -0
  171. package/dist/services/hooks/HookExecutor.js.map +1 -0
  172. package/dist/services/hooks/index.d.ts +2 -0
  173. package/dist/services/hooks/index.d.ts.map +1 -0
  174. package/dist/services/hooks/index.js +2 -0
  175. package/dist/services/hooks/index.js.map +1 -0
  176. package/dist/services/logs/LogFormatterService.d.ts +17 -0
  177. package/dist/services/logs/LogFormatterService.d.ts.map +1 -0
  178. package/dist/services/logs/LogFormatterService.js +35 -0
  179. package/dist/services/logs/LogFormatterService.js.map +1 -0
  180. package/dist/services/logs/TempFileService.d.ts +27 -0
  181. package/dist/services/logs/TempFileService.d.ts.map +1 -0
  182. package/dist/services/logs/TempFileService.js +90 -0
  183. package/dist/services/logs/TempFileService.js.map +1 -0
  184. package/dist/services/logs/index.d.ts +3 -0
  185. package/dist/services/logs/index.d.ts.map +1 -0
  186. package/dist/services/logs/index.js +4 -0
  187. package/dist/services/logs/index.js.map +1 -0
  188. package/dist/services/plan/PlanService.d.ts +70 -0
  189. package/dist/services/plan/PlanService.d.ts.map +1 -0
  190. package/dist/services/plan/PlanService.js +211 -0
  191. package/dist/services/plan/PlanService.js.map +1 -0
  192. package/dist/services/polling/GitHubDirectPollingAdapter.d.ts +16 -0
  193. package/dist/services/polling/GitHubDirectPollingAdapter.d.ts.map +1 -0
  194. package/dist/services/polling/GitHubDirectPollingAdapter.js +87 -0
  195. package/dist/services/polling/GitHubDirectPollingAdapter.js.map +1 -0
  196. package/dist/services/polling/HulaServerPollingAdapter.d.ts +23 -0
  197. package/dist/services/polling/HulaServerPollingAdapter.d.ts.map +1 -0
  198. package/dist/services/polling/HulaServerPollingAdapter.js +78 -0
  199. package/dist/services/polling/HulaServerPollingAdapter.js.map +1 -0
  200. package/dist/services/polling/IStatusPollingAdapter.d.ts +29 -0
  201. package/dist/services/polling/IStatusPollingAdapter.d.ts.map +1 -0
  202. package/dist/services/polling/IStatusPollingAdapter.js +2 -0
  203. package/dist/services/polling/IStatusPollingAdapter.js.map +1 -0
  204. package/dist/services/polling/StatusPollingAdapterFactory.d.ts +14 -0
  205. package/dist/services/polling/StatusPollingAdapterFactory.d.ts.map +1 -0
  206. package/dist/services/polling/StatusPollingAdapterFactory.js +41 -0
  207. package/dist/services/polling/StatusPollingAdapterFactory.js.map +1 -0
  208. package/dist/services/polling/index.d.ts +5 -0
  209. package/dist/services/polling/index.d.ts.map +1 -0
  210. package/dist/services/polling/index.js +5 -0
  211. package/dist/services/polling/index.js.map +1 -0
  212. package/dist/services/preview/PreviewService.d.ts +86 -0
  213. package/dist/services/preview/PreviewService.d.ts.map +1 -0
  214. package/dist/services/preview/PreviewService.js +439 -0
  215. package/dist/services/preview/PreviewService.js.map +1 -0
  216. package/dist/services/tracking/ProjectTrackingAdapter.d.ts +41 -0
  217. package/dist/services/tracking/ProjectTrackingAdapter.d.ts.map +1 -0
  218. package/dist/services/tracking/ProjectTrackingAdapter.js +230 -0
  219. package/dist/services/tracking/ProjectTrackingAdapter.js.map +1 -0
  220. package/dist/services/tracking/RemoteTrackingAdapter.d.ts +62 -0
  221. package/dist/services/tracking/RemoteTrackingAdapter.d.ts.map +1 -0
  222. package/dist/services/tracking/RemoteTrackingAdapter.js +242 -0
  223. package/dist/services/tracking/RemoteTrackingAdapter.js.map +1 -0
  224. package/dist/services/tracking/TrackingAdapterFactory.d.ts +25 -0
  225. package/dist/services/tracking/TrackingAdapterFactory.d.ts.map +1 -0
  226. package/dist/services/tracking/TrackingAdapterFactory.js +55 -0
  227. package/dist/services/tracking/TrackingAdapterFactory.js.map +1 -0
  228. package/dist/services/tracking/TrackingService.d.ts +104 -0
  229. package/dist/services/tracking/TrackingService.d.ts.map +1 -0
  230. package/dist/services/tracking/TrackingService.js +256 -0
  231. package/dist/services/tracking/TrackingService.js.map +1 -0
  232. package/dist/services/tracking/UntrackedIssuesService.d.ts +20 -0
  233. package/dist/services/tracking/UntrackedIssuesService.d.ts.map +1 -0
  234. package/dist/services/tracking/UntrackedIssuesService.js +148 -0
  235. package/dist/services/tracking/UntrackedIssuesService.js.map +1 -0
  236. package/dist/services/tracking/local-file-migration.d.ts +16 -0
  237. package/dist/services/tracking/local-file-migration.d.ts.map +1 -0
  238. package/dist/services/tracking/local-file-migration.js +86 -0
  239. package/dist/services/tracking/local-file-migration.js.map +1 -0
  240. package/dist/templates/planning-instructions.md +752 -0
  241. package/dist/templates/proceed-instructions.md +453 -0
  242. package/dist/templates/scripts/hula-fix-commit.sh +110 -0
  243. package/dist/templates/scripts/hula-fix-setup.sh +145 -0
  244. package/dist/templates/scripts/hula-launch-run.sh +89 -0
  245. package/dist/templates/scripts/hula-merge-local.sh +138 -0
  246. package/dist/templates/scripts/hula-merge-remote.sh +74 -0
  247. package/dist/templates/scripts/hula-read-config.sh +38 -0
  248. package/dist/templates/scripts/hula-verify-gather.sh +135 -0
  249. package/dist/templates/scripts/hula-verify-post.sh +84 -0
  250. package/dist/templates/skills/hula-confirm/SKILL.md +361 -0
  251. package/dist/templates/skills/hula-create/SKILL.md +211 -0
  252. package/dist/templates/skills/hula-fix/SKILL.md +224 -0
  253. package/dist/templates/skills/hula-launch/SKILL.md +159 -0
  254. package/dist/templates/skills/hula-log/SKILL.md +105 -0
  255. package/dist/templates/skills/hula-merge/SKILL.md +387 -0
  256. package/dist/templates/skills/hula-plan/SKILL.md +148 -0
  257. package/dist/templates/skills/hula-upload/SKILL.md +115 -0
  258. package/dist/templates/skills/hula-verify/SKILL.md +754 -0
  259. package/dist/types/common.schema.d.ts +46 -0
  260. package/dist/types/common.schema.d.ts.map +1 -0
  261. package/dist/types/common.schema.js +60 -0
  262. package/dist/types/common.schema.js.map +1 -0
  263. package/dist/types/config.schema.d.ts +443 -0
  264. package/dist/types/config.schema.d.ts.map +1 -0
  265. package/dist/types/config.schema.js +126 -0
  266. package/dist/types/config.schema.js.map +1 -0
  267. package/dist/types/index.d.ts +6 -0
  268. package/dist/types/index.d.ts.map +1 -0
  269. package/dist/types/index.js +7 -0
  270. package/dist/types/index.js.map +1 -0
  271. package/dist/types/issue.schema.d.ts +136 -0
  272. package/dist/types/issue.schema.d.ts.map +1 -0
  273. package/dist/types/issue.schema.js +49 -0
  274. package/dist/types/issue.schema.js.map +1 -0
  275. package/dist/types/pr.schema.d.ts +118 -0
  276. package/dist/types/pr.schema.d.ts.map +1 -0
  277. package/dist/types/pr.schema.js +40 -0
  278. package/dist/types/pr.schema.js.map +1 -0
  279. package/dist/types/tracking.schema.d.ts +340 -0
  280. package/dist/types/tracking.schema.d.ts.map +1 -0
  281. package/dist/types/tracking.schema.js +56 -0
  282. package/dist/types/tracking.schema.js.map +1 -0
  283. package/dist/utils/browser-launcher.d.ts +60 -0
  284. package/dist/utils/browser-launcher.d.ts.map +1 -0
  285. package/dist/utils/browser-launcher.js +263 -0
  286. package/dist/utils/browser-launcher.js.map +1 -0
  287. package/dist/utils/cache.d.ts +44 -0
  288. package/dist/utils/cache.d.ts.map +1 -0
  289. package/dist/utils/cache.js +94 -0
  290. package/dist/utils/cache.js.map +1 -0
  291. package/dist/utils/config-parser.d.ts +16 -0
  292. package/dist/utils/config-parser.d.ts.map +1 -0
  293. package/dist/utils/config-parser.js +210 -0
  294. package/dist/utils/config-parser.js.map +1 -0
  295. package/dist/utils/editor.d.ts +21 -0
  296. package/dist/utils/editor.d.ts.map +1 -0
  297. package/dist/utils/editor.js +213 -0
  298. package/dist/utils/editor.js.map +1 -0
  299. package/dist/utils/git-utils.d.ts +5 -0
  300. package/dist/utils/git-utils.d.ts.map +1 -0
  301. package/dist/utils/git-utils.js +18 -0
  302. package/dist/utils/git-utils.js.map +1 -0
  303. package/dist/utils/github-cli.d.ts +62 -0
  304. package/dist/utils/github-cli.d.ts.map +1 -0
  305. package/dist/utils/github-cli.js +492 -0
  306. package/dist/utils/github-cli.js.map +1 -0
  307. package/dist/utils/logger.d.ts +45 -0
  308. package/dist/utils/logger.d.ts.map +1 -0
  309. package/dist/utils/logger.js +118 -0
  310. package/dist/utils/logger.js.map +1 -0
  311. package/dist/utils/open-url.d.ts +19 -0
  312. package/dist/utils/open-url.d.ts.map +1 -0
  313. package/dist/utils/open-url.js +37 -0
  314. package/dist/utils/open-url.js.map +1 -0
  315. package/dist/utils/project-resolver.d.ts +13 -0
  316. package/dist/utils/project-resolver.d.ts.map +1 -0
  317. package/dist/utils/project-resolver.js +35 -0
  318. package/dist/utils/project-resolver.js.map +1 -0
  319. package/dist/utils/prompts.d.ts +80 -0
  320. package/dist/utils/prompts.d.ts.map +1 -0
  321. package/dist/utils/prompts.js +326 -0
  322. package/dist/utils/prompts.js.map +1 -0
  323. package/dist/utils/shell.d.ts +54 -0
  324. package/dist/utils/shell.d.ts.map +1 -0
  325. package/dist/utils/shell.js +152 -0
  326. package/dist/utils/shell.js.map +1 -0
  327. package/dist/utils/validators.d.ts +32 -0
  328. package/dist/utils/validators.d.ts.map +1 -0
  329. package/dist/utils/validators.js +64 -0
  330. package/dist/utils/validators.js.map +1 -0
  331. package/package.json +73 -0
@@ -0,0 +1,1071 @@
1
+ import inquirer from 'inquirer';
2
+ import { logger } from '../utils/logger.js';
3
+ import { TrackingService } from '../services/tracking/TrackingService.js';
4
+ import { TrackingAdapterFactory } from '../services/tracking/TrackingAdapterFactory.js';
5
+ import { PRService } from '../services/github/PRService.js';
6
+ import { StatusPollingAdapterFactory } from '../services/polling/StatusPollingAdapterFactory.js';
7
+ import { MIN_POLL_INTERVAL_SECONDS } from '../types/common.schema.js';
8
+ import { HulaApiClient } from '../services/api/HulaApiClient.js';
9
+ import { executeLogs } from './logs.js';
10
+ import { select, confirm, selectPriority } from '../utils/prompts.js';
11
+ import { DEFAULT_PRIORITY_LEVELS, STATUS } from '../types/common.schema.js';
12
+ import { execSync } from 'child_process';
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import os from 'os';
16
+ import { fileURLToPath } from 'url';
17
+ /**
18
+ * Max listeners for stdin to prevent MaxListenersExceededWarning during background polling.
19
+ * Default Node.js limit is 10, we set to 20 as a safety measure.
20
+ */
21
+ const MAX_STDIN_LISTENERS = 20;
22
+ /**
23
+ * Interactive command - Full interactive TUI mode
24
+ * Not used - kept for backwards compatibility
25
+ */
26
+ export function interactiveCommand(_program, _config) {
27
+ // This is not registered as a command - it's called directly from index.ts
28
+ // when no args are provided
29
+ }
30
+ /**
31
+ * Main entry point for interactive mode
32
+ */
33
+ export async function executeInteractive(config) {
34
+ // Increase stdin max listeners to prevent warnings during background polling
35
+ // Note: This is a fallback - the root cause is fixed by properly cleaning up listeners
36
+ if (process.stdin.setMaxListeners) {
37
+ process.stdin.setMaxListeners(MAX_STDIN_LISTENERS);
38
+ }
39
+ const trackingService = new TrackingService(TrackingAdapterFactory.create(config));
40
+ const state = {
41
+ page: 0,
42
+ pageSize: parseInt(process.env.PAGE_SIZE ?? '10', 10),
43
+ filter: '',
44
+ lastRefreshTime: new Date(),
45
+ lastPollingCheckTime: undefined,
46
+ pendingRefresh: false,
47
+ shouldAutoRefresh: false,
48
+ };
49
+ let running = true;
50
+ // Start background polling for status changes via configurable adapter
51
+ let pollingInterval = null;
52
+ try {
53
+ const pollingAdapter = StatusPollingAdapterFactory.create(config, trackingService);
54
+ const intervalSeconds = Math.max(config.pollIntervalSeconds, MIN_POLL_INTERVAL_SECONDS);
55
+ logger.info(`🔄 Auto-refresh: ${pollingAdapter.name} (every ${intervalSeconds}s)`);
56
+ logger.blank();
57
+ pollingInterval = startStatusPolling(pollingAdapter, trackingService, config, state);
58
+ }
59
+ catch (error) {
60
+ logger.warning(`⚠️ Auto-refresh disabled: ${error instanceof Error ? error.message : String(error)}`);
61
+ logger.blank();
62
+ }
63
+ while (running) {
64
+ // Get all issues
65
+ let allIssues = await trackingService.getAll();
66
+ // Update refresh tracking
67
+ state.lastRefreshTime = new Date();
68
+ state.pendingRefresh = false;
69
+ state.shouldAutoRefresh = false;
70
+ // Filter out closed issues (by actual GitHub state, not tracking status)
71
+ allIssues = allIssues.filter((issue) => issue.githubState !== 'closed');
72
+ // Apply filter if set
73
+ if (state.filter) {
74
+ const filterLower = state.filter.toLowerCase();
75
+ allIssues = allIssues.filter((issue) => issue.name.toLowerCase().includes(filterLower) ||
76
+ (issue.number && issue.number.toString().includes(filterLower)));
77
+ }
78
+ if (allIssues.length === 0) {
79
+ // No issues tracked - show empty state menu
80
+ running = await showEmptyStateMenu(config, trackingService);
81
+ continue;
82
+ }
83
+ // Calculate pagination
84
+ const totalPages = Math.ceil(allIssues.length / state.pageSize);
85
+ const startIdx = state.page * state.pageSize;
86
+ const endIdx = Math.min(startIdx + state.pageSize, allIssues.length);
87
+ const pageIssues = allIssues.slice(startIdx, endIdx);
88
+ // Display issues
89
+ displayIssueList(pageIssues, state, totalPages, allIssues.length, config);
90
+ // Show prompt with available actions (with auto-refresh capability)
91
+ const action = await promptForActionWithAutoRefresh(state, totalPages);
92
+ // Handle action
93
+ const continueLoop = await handleAction(action, pageIssues, state, config, trackingService, totalPages);
94
+ if (!continueLoop) {
95
+ running = false;
96
+ }
97
+ }
98
+ // Stop polling when exiting
99
+ if (pollingInterval) {
100
+ clearInterval(pollingInterval);
101
+ }
102
+ logger.success('Goodbye!');
103
+ }
104
+ /**
105
+ * Display paginated issue list
106
+ */
107
+ function displayIssueList(issues, state, totalPages, totalIssues, config) {
108
+ console.clear();
109
+ logger.section('HubLaunch - Tracked Issues');
110
+ // Show polling status
111
+ const intervalSeconds = Math.max(config.pollIntervalSeconds, MIN_POLL_INTERVAL_SECONDS);
112
+ const intervalText = intervalSeconds < 60
113
+ ? `${intervalSeconds}s`
114
+ : `${Math.floor(intervalSeconds / 60)}m`;
115
+ logger.info(`🔄 Auto-refresh: ${config.pollingProvider ?? 'hula-server'} (every ${intervalText})`);
116
+ logger.blank();
117
+ if (state.filter) {
118
+ logger.info(`🔍 Filter: "${state.filter}"`);
119
+ logger.blank();
120
+ }
121
+ // Display each issue with number, status, priority, and deployment status.
122
+ issues.forEach((issue, idx) => {
123
+ const displayNum = idx + 1;
124
+ const statusEmoji = getStatusEmoji(issue.trackingStatus);
125
+ const priorityEmoji = getPriorityEmoji(issue.priority);
126
+ const deploymentEmoji = getDeploymentEmoji(issue.deploymentStatus);
127
+ logger.info(` ${displayNum}) ${issue.trackingName} (#${issue.number}) [${priorityEmoji} ${issue.priority}] [${statusEmoji} ${issue.trackingStatus}] [${deploymentEmoji} deployment]`);
128
+ });
129
+ logger.blank();
130
+ logger.info(`Page ${state.page + 1} of ${totalPages} (${totalIssues} total issues)`);
131
+ // Show last refresh timestamp
132
+ const refreshTimestamp = state.lastRefreshTime.toLocaleTimeString();
133
+ logger.info(`🔄 Last refreshed: ${refreshTimestamp}`);
134
+ // Show pending refresh indicator
135
+ if (state.pendingRefresh) {
136
+ logger.warning(`⚠️ Updates detected - press Enter to refresh`);
137
+ }
138
+ logger.blank();
139
+ }
140
+ /**
141
+ * Prompt user for action with auto-refresh support
142
+ * Checks periodically if auto-refresh is triggered and returns 'r' automatically
143
+ * Uses native readline for better control over event listener cleanup
144
+ */
145
+ async function promptForActionWithAutoRefresh(state, totalPages) {
146
+ const hints = ['Enter number to select', '/term to search'];
147
+ if (totalPages > 1) {
148
+ if (state.page > 0)
149
+ hints.push('p for prev');
150
+ if (state.page < totalPages - 1)
151
+ hints.push('n for next');
152
+ }
153
+ hints.push('r to refresh', 'a for admin', '? for help', 'q to quit');
154
+ logger.info(`💡 ${hints.join(' · ')}`);
155
+ logger.blank();
156
+ // Import readline for better control over event listeners
157
+ const readline = await import('readline');
158
+ // Track interval ID and readline interface for cleanup
159
+ let checkIntervalId = null;
160
+ let rl = null;
161
+ return new Promise((resolve) => {
162
+ // Create readline interface
163
+ rl = readline.createInterface({
164
+ input: process.stdin,
165
+ output: process.stdout,
166
+ });
167
+ // Display prompt
168
+ rl.setPrompt('Choice: ');
169
+ rl.prompt();
170
+ // Flag to track if already resolved
171
+ let resolved = false;
172
+ // Handle user input
173
+ const lineHandler = (line) => {
174
+ if (!resolved) {
175
+ resolved = true;
176
+ cleanup();
177
+ resolve(line.trim());
178
+ }
179
+ };
180
+ // Handle close events
181
+ const closeHandler = () => {
182
+ if (!resolved) {
183
+ resolved = true;
184
+ cleanup();
185
+ resolve(''); // Return empty string if closed unexpectedly
186
+ }
187
+ };
188
+ // Attach event listeners
189
+ rl.on('line', lineHandler);
190
+ rl.once('close', closeHandler);
191
+ // Start auto-refresh check
192
+ checkIntervalId = setInterval(() => {
193
+ if (state.shouldAutoRefresh && !resolved) {
194
+ resolved = true;
195
+ cleanup();
196
+ resolve('r'); // Simulate user pressing 'r'
197
+ }
198
+ }, 100); // Check every 100ms
199
+ // Cleanup function
200
+ function cleanup() {
201
+ // Clear the interval
202
+ if (checkIntervalId) {
203
+ clearInterval(checkIntervalId);
204
+ checkIntervalId = null;
205
+ }
206
+ // Remove event listeners and close readline
207
+ if (rl) {
208
+ rl.removeListener('line', lineHandler);
209
+ rl.removeListener('close', closeHandler);
210
+ // Attempt to close interface - will fail silently if already closed
211
+ // This prevents ERR_USE_AFTER_CLOSE when user presses Ctrl+C
212
+ try {
213
+ rl.close();
214
+ }
215
+ catch (closeError) {
216
+ // Interface was already closed (e.g., by Ctrl+C), this is expected
217
+ // Silently ignore this error
218
+ }
219
+ rl = null;
220
+ }
221
+ }
222
+ });
223
+ }
224
+ /**
225
+ * Handle user action
226
+ */
227
+ async function handleAction(action, pageIssues, state, config, trackingService, totalPages) {
228
+ // Quit
229
+ if (action.toLowerCase() === 'q') {
230
+ return false;
231
+ }
232
+ // Help
233
+ if (action === '?') {
234
+ showHelp();
235
+ await waitForEnter();
236
+ return true;
237
+ }
238
+ // Admin menu
239
+ if (action.toLowerCase() === 'a') {
240
+ await showAdminMenu(config, trackingService);
241
+ return true;
242
+ }
243
+ // Next page
244
+ if (action.toLowerCase() === 'n' && state.page < totalPages - 1) {
245
+ state.page++;
246
+ return true;
247
+ }
248
+ // Previous page
249
+ if (action.toLowerCase() === 'p' && state.page > 0) {
250
+ state.page--;
251
+ return true;
252
+ }
253
+ // Manual refresh
254
+ if (action.toLowerCase() === 'r' || action.toLowerCase() === 'refresh') {
255
+ // Just return true to continue loop - data will be refreshed at loop start
256
+ // Clear pending flag since user explicitly requested refresh
257
+ state.pendingRefresh = false;
258
+ return true;
259
+ }
260
+ // Clear filter
261
+ if (action === '' && state.filter) {
262
+ state.filter = '';
263
+ state.page = 0;
264
+ return true;
265
+ }
266
+ // Search/filter
267
+ if (action.startsWith('/')) {
268
+ state.filter = action.slice(1);
269
+ state.page = 0;
270
+ return true;
271
+ }
272
+ // Numeric selection
273
+ const num = parseInt(action.replace(/\)$/, ''), 10);
274
+ if (!isNaN(num) && num >= 1 && num <= pageIssues.length) {
275
+ const selectedIssue = pageIssues[num - 1];
276
+ if (selectedIssue) {
277
+ await showIssueActionsMenu(selectedIssue, config, trackingService, state);
278
+ }
279
+ // Clear filter after handling an issue (bash behavior)
280
+ state.filter = '';
281
+ return true;
282
+ }
283
+ // Invalid input
284
+ logger.error('Invalid input. Press ? for help.');
285
+ await waitForEnter();
286
+ return true;
287
+ }
288
+ /**
289
+ * Show empty state menu when no issues are tracked
290
+ */
291
+ async function showEmptyStateMenu(config, _trackingService) {
292
+ console.clear();
293
+ logger.section('HubLaunch - No Tracked Issues');
294
+ logger.blank();
295
+ logger.warning('No tracked issues found.');
296
+ logger.blank();
297
+ const action = await select('What would you like to do?', [
298
+ 'Track existing issues',
299
+ 'Create new issue',
300
+ 'Initialize/configure',
301
+ 'Quit',
302
+ ]);
303
+ switch (action) {
304
+ case 'Track existing issues':
305
+ await runCommand('track', config);
306
+ break;
307
+ case 'Create new issue':
308
+ await runCommand('create', config);
309
+ break;
310
+ case 'Initialize/configure':
311
+ await runCommand('init', config);
312
+ break;
313
+ case 'Quit':
314
+ return false;
315
+ }
316
+ return true;
317
+ }
318
+ /**
319
+ * Show issue actions menu for a selected issue
320
+ */
321
+ async function showIssueActionsMenu(issue, config, trackingService, state) {
322
+ let inMenu = true;
323
+ while (inMenu) {
324
+ // Refresh issue data to get latest status
325
+ const allIssues = await trackingService.getAll();
326
+ const currentIssue = allIssues.find((i) => issue.number != null ? i.number === issue.number : i.name === issue.name) ?? issue;
327
+ console.clear();
328
+ logger.section(`Issue: ${currentIssue.name} (#${currentIssue.number})`);
329
+ logger.blank();
330
+ logger.info('📋 View:');
331
+ logger.info(' i) Open issue in browser');
332
+ logger.info(' p) Open plan file');
333
+ logger.info(' v) Preview deployment');
334
+ logger.info(' d) View code changes (diff)');
335
+ logger.info(' r) View PR in browser');
336
+ logger.info(' l) Latest Copilot comment');
337
+ logger.info(' w) Watch tests');
338
+ logger.info(' g) Get logs by ID');
339
+ // Per-task job-status menu items (`j`/`y`) were removed when the
340
+ // local schema dropped the `job*` fields with the Plan/Project/Task
341
+ // rename.
342
+ logger.blank();
343
+ logger.info('⚙️ Manage:');
344
+ logger.info(' c) Checkout PR branch');
345
+ logger.info(' m) Merge PR');
346
+ logger.info(' s) Set priority');
347
+ // Show assign to Copilot option only for unassigned issues
348
+ if (currentIssue.trackingStatus === STATUS.UNASSIGNED) {
349
+ logger.info(' t) Assign to Copilot');
350
+ }
351
+ logger.info(' u) Untrack issue');
352
+ logger.info(' o) Respond to Copilot');
353
+ logger.info(' x) Close issue & untrack');
354
+ logger.blank();
355
+ logger.info('🔙 Other:');
356
+ logger.info(' b) Back to issue list');
357
+ logger.info(' a) Admin actions');
358
+ logger.info(' ?) Help');
359
+ logger.info(' q) Quit');
360
+ logger.blank();
361
+ // Display current status
362
+ const statusEmoji = getStatusEmoji(currentIssue.trackingStatus);
363
+ const deploymentEmoji = getDeploymentEmoji(currentIssue.deploymentStatus);
364
+ logger.info(`📊 Current Status: ${statusEmoji} ${currentIssue.trackingStatus}`);
365
+ logger.info(`🚀 Deployment: ${deploymentEmoji}`);
366
+ // Per-task job status display was removed with the rename.
367
+ // Show last polling check time if available, otherwise current time
368
+ const lastCheckedTime = state.lastPollingCheckTime
369
+ ? state.lastPollingCheckTime.toLocaleTimeString()
370
+ : new Date().toLocaleTimeString();
371
+ logger.info(`🕐 Last checked: ${lastCheckedTime}`);
372
+ logger.blank();
373
+ // Use native readline for proper cleanup of event listeners
374
+ const readline = await import('readline');
375
+ // Track interval ID and readline interface for cleanup
376
+ let checkIntervalId = null;
377
+ let rl = null;
378
+ const action = await new Promise((resolve) => {
379
+ // Create readline interface
380
+ rl = readline.createInterface({
381
+ input: process.stdin,
382
+ output: process.stdout,
383
+ });
384
+ // Display prompt
385
+ rl.setPrompt('Enter action: ');
386
+ rl.prompt();
387
+ // Flag to track if already resolved
388
+ let resolved = false;
389
+ // Handle user input
390
+ const lineHandler = (line) => {
391
+ if (!resolved) {
392
+ resolved = true;
393
+ cleanup();
394
+ resolve(line.trim().toLowerCase());
395
+ }
396
+ };
397
+ // Handle close events
398
+ const closeHandler = () => {
399
+ if (!resolved) {
400
+ resolved = true;
401
+ cleanup();
402
+ resolve(''); // Return empty string if closed unexpectedly
403
+ }
404
+ };
405
+ // Attach event listeners
406
+ rl.on('line', lineHandler);
407
+ rl.once('close', closeHandler);
408
+ // Start auto-refresh check
409
+ checkIntervalId = setInterval(() => {
410
+ if (state.shouldAutoRefresh && !resolved) {
411
+ resolved = true;
412
+ cleanup();
413
+ resolve('refresh'); // Auto-refresh triggered by background poller
414
+ }
415
+ }, 100); // Check flag every 100ms instead of polling API
416
+ // Cleanup function
417
+ function cleanup() {
418
+ // Clear the interval
419
+ if (checkIntervalId) {
420
+ clearInterval(checkIntervalId);
421
+ checkIntervalId = null;
422
+ }
423
+ // Remove event listeners and close readline
424
+ if (rl) {
425
+ rl.removeListener('line', lineHandler);
426
+ rl.removeListener('close', closeHandler);
427
+ // Attempt to close interface - will fail silently if already closed
428
+ // This prevents ERR_USE_AFTER_CLOSE when user presses Ctrl+C
429
+ try {
430
+ rl.close();
431
+ }
432
+ catch (closeError) {
433
+ // Interface was already closed (e.g., by Ctrl+C), this is expected
434
+ // Silently ignore this error
435
+ }
436
+ rl = null;
437
+ }
438
+ }
439
+ });
440
+ // If auto-refresh triggered, just loop again to redraw
441
+ if (action === 'refresh') {
442
+ continue;
443
+ }
444
+ const continueMenu = await handleIssueAction(action, currentIssue, config, trackingService);
445
+ if (!continueMenu) {
446
+ inMenu = false;
447
+ }
448
+ }
449
+ }
450
+ /**
451
+ * Handle action within issue menu
452
+ */
453
+ async function handleIssueAction(action, issue, config, trackingService) {
454
+ switch (action) {
455
+ case 'i':
456
+ // Open issue in browser
457
+ logger.info(`Opening issue #${issue.number} in browser...`);
458
+ try {
459
+ let owner = issue.projectOwner;
460
+ let repo = issue.projectName;
461
+ // Fallback: try to parse from name if repoOwner/repoName not available
462
+ if (!owner || !repo) {
463
+ // Try parsing from name (owner/repo#number format)
464
+ const parts = issue.name.split('/');
465
+ if (parts.length >= 2) {
466
+ owner = parts[0];
467
+ const repoAndNumber = parts[1]?.split('#');
468
+ repo = repoAndNumber?.[0];
469
+ }
470
+ }
471
+ if (owner && repo) {
472
+ const url = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
473
+ const { openUrlSync } = await import('../utils/open-url.js');
474
+ openUrlSync(url);
475
+ logger.success('✓ Browser opened');
476
+ }
477
+ else {
478
+ logger.error('Could not determine repository. Please open manually.');
479
+ logger.error(`Issue data: name=${issue.name}, number=${issue.number}`);
480
+ logger.error(` projectOwner=${issue.projectOwner}, projectName=${issue.projectName}`);
481
+ logger.error(` Full issue object: ${JSON.stringify(issue, null, 2)}`);
482
+ }
483
+ }
484
+ catch (error) {
485
+ logger.error('Failed to open issue in browser');
486
+ logger.error(error instanceof Error ? error.message : String(error));
487
+ }
488
+ await waitForEnter();
489
+ return true;
490
+ case 'p':
491
+ // Open plan file
492
+ if (issue.planPath) {
493
+ logger.info(`Opening plan file: ${issue.planPath}`);
494
+ try {
495
+ const { EditorService } = await import('../services/editor/EditorService.js');
496
+ const editorService = new EditorService(config.editorCommand);
497
+ await editorService.openFile(issue.planPath);
498
+ logger.success('✓ Plan file opened');
499
+ }
500
+ catch (error) {
501
+ logger.error(`Failed to open plan file: ${error instanceof Error ? error.message : String(error)}`);
502
+ }
503
+ }
504
+ else {
505
+ logger.warning(`No plan file found for issue #${issue.number}`);
506
+ }
507
+ await waitForEnter();
508
+ return true;
509
+ case 'v':
510
+ // Preview deployment
511
+ if (!issue.number) {
512
+ logger.warning('Preview not available for standalone jobs');
513
+ await waitForEnter();
514
+ return true;
515
+ }
516
+ await runCommand('preview', config, [issue.number.toString()]);
517
+ return true;
518
+ case 'd':
519
+ // View diff
520
+ if (!issue.number) {
521
+ logger.warning('Diff not available for standalone jobs');
522
+ await waitForEnter();
523
+ return true;
524
+ }
525
+ await runCommand('diff', config, [issue.number.toString()]);
526
+ await waitForEnter();
527
+ return true;
528
+ case 'r':
529
+ // View PR in browser
530
+ if (!issue.number) {
531
+ logger.warning('PR not available for standalone jobs');
532
+ await waitForEnter();
533
+ return true;
534
+ }
535
+ logger.info(`Opening PR for issue #${issue.number} in browser...`);
536
+ try {
537
+ // Check if issue has a PR URL already
538
+ if (issue.prUrl) {
539
+ const { openUrlSync } = await import('../utils/open-url.js');
540
+ openUrlSync(issue.prUrl);
541
+ logger.success('✓ Browser opened');
542
+ }
543
+ else {
544
+ // Find PR linked to issue using GitHub CLI
545
+ const prOutput = execSync(`gh pr list --search "closes:#${issue.number} OR fixes:#${issue.number}" --json number,url --limit 1`, { encoding: 'utf-8' });
546
+ const prs = JSON.parse(prOutput);
547
+ if (prs.length > 0 && prs[0]) {
548
+ const { openUrlSync } = await import('../utils/open-url.js');
549
+ openUrlSync(prs[0].url);
550
+ logger.success('✓ Browser opened');
551
+ }
552
+ else {
553
+ logger.warning(`No PR found for issue #${issue.number}`);
554
+ }
555
+ }
556
+ }
557
+ catch (error) {
558
+ logger.error('Failed to open PR in browser');
559
+ logger.error(error instanceof Error ? error.message : String(error));
560
+ }
561
+ await waitForEnter();
562
+ return true;
563
+ case 'w':
564
+ // Watch tests
565
+ if (!issue.number) {
566
+ logger.warning('Watch not available for standalone jobs');
567
+ await waitForEnter();
568
+ return true;
569
+ }
570
+ await runCommand('watch', config, [issue.number.toString()]);
571
+ await waitForEnter();
572
+ return true;
573
+ case 'g':
574
+ // Get logs by ID
575
+ await promptAndViewLogs(config);
576
+ await waitForEnter();
577
+ return true;
578
+ case 'j':
579
+ // Job status / job logs UI was removed with the Plan/Project/Task
580
+ // rename (the local schema no longer carries job* fields).
581
+ logger.warning('Per-task job status is no longer available in interactive mode.');
582
+ await waitForEnter();
583
+ return true;
584
+ case 'y':
585
+ logger.warning('Per-task job logs are no longer available in interactive mode.');
586
+ await waitForEnter();
587
+ return true;
588
+ case 'c':
589
+ // Checkout PR branch
590
+ if (!issue.number) {
591
+ logger.warning('Checkout not available for standalone jobs');
592
+ await waitForEnter();
593
+ return true;
594
+ }
595
+ await runCommand('checkout', config, [issue.number.toString()]);
596
+ return true;
597
+ case 'm':
598
+ // Merge PR
599
+ if (!issue.number) {
600
+ logger.warning('Merge not available for standalone jobs');
601
+ await waitForEnter();
602
+ return true;
603
+ }
604
+ await runCommand('merge', config, [issue.number.toString()]);
605
+ return false; // Return to main menu after merge
606
+ case 's':
607
+ // Set priority
608
+ if (!issue.number) {
609
+ logger.warning('Priority update not available for standalone jobs');
610
+ await waitForEnter();
611
+ return true;
612
+ }
613
+ try {
614
+ // Validate config
615
+ if (!config.hulaProjectUrl || !config.hulaApiKey) {
616
+ throw new Error('Hula project URL and API key are required. Run "hula init" to configure.');
617
+ }
618
+ // Show current priority
619
+ if (issue.priority) {
620
+ const currentLevel = DEFAULT_PRIORITY_LEVELS.find((level) => level.name === issue.priority);
621
+ if (currentLevel) {
622
+ logger.info(`Current priority: ${currentLevel.emoji} ${issue.priority}`);
623
+ }
624
+ }
625
+ else {
626
+ logger.info('Current priority: None');
627
+ }
628
+ logger.blank();
629
+ // Select new priority
630
+ const newPriority = await selectPriority('Select new priority:', DEFAULT_PRIORITY_LEVELS);
631
+ const priorityLevel = DEFAULT_PRIORITY_LEVELS.find((level) => level.name === newPriority);
632
+ if (!priorityLevel) {
633
+ throw new Error(`Invalid priority: ${newPriority}`);
634
+ }
635
+ // Update priority via hula-project API
636
+ logger.info(`Setting priority to ${priorityLevel.emoji} ${newPriority}...`);
637
+ const apiClient = new HulaApiClient({
638
+ baseUrl: config.hulaProjectUrl,
639
+ apiKey: config.hulaApiKey,
640
+ });
641
+ // Get the server-side plan ID (paginate to avoid missing plans beyond first page)
642
+ let allServerIssues = [];
643
+ let priorityPage = 1;
644
+ let priorityTotalPages = 1;
645
+ do {
646
+ const resp = await apiClient.listPlans({ page: priorityPage, limit: 100 });
647
+ allServerIssues = allServerIssues.concat(resp.plans);
648
+ priorityTotalPages = resp.totalPages;
649
+ priorityPage++;
650
+ } while (priorityPage <= priorityTotalPages);
651
+ const serverIssue = allServerIssues.find((i) => i.issueNumber === issue.number && i.project.owner === issue.projectOwner && i.project.name === issue.projectName);
652
+ if (!serverIssue) {
653
+ throw new Error(`Plan #${issue.number} not found on server`);
654
+ }
655
+ await apiClient.updatePlanPriority(serverIssue.id, newPriority);
656
+ logger.success(`✓ Priority updated successfully`);
657
+ }
658
+ catch (error) {
659
+ logger.error(`Failed to set priority: ${error instanceof Error ? error.message : String(error)}`);
660
+ }
661
+ await waitForEnter();
662
+ return true;
663
+ case 't':
664
+ // Assign to Copilot
665
+ if (!issue.number) {
666
+ logger.warning('Assign to Copilot not available for standalone jobs');
667
+ await waitForEnter();
668
+ return true;
669
+ }
670
+ logger.info(`Assigning GitHub Copilot to issue #${issue.number}...`);
671
+ try {
672
+ const { CopilotService } = await import('../services/github/CopilotService.js');
673
+ const copilotService = new CopilotService(config);
674
+ const assigned = await copilotService.assignToIssue(issue.number);
675
+ if (assigned) {
676
+ logger.success(`✓ GitHub Copilot assigned to issue #${issue.number}`);
677
+ }
678
+ else {
679
+ logger.warning('Could not assign GitHub Copilot automatically');
680
+ logger.info('You may need to assign Copilot manually in the GitHub UI');
681
+ // Check for debug screenshot
682
+ const { tmpdir } = await import('os');
683
+ const { join } = await import('path');
684
+ const { existsSync } = await import('fs');
685
+ const debugPath = join(tmpdir(), 'github-copilot-not-found.png');
686
+ if (existsSync(debugPath)) {
687
+ logger.info(`📸 Debug screenshot saved to: ${debugPath}`);
688
+ }
689
+ }
690
+ }
691
+ catch (error) {
692
+ logger.error(`Failed to assign Copilot: ${error instanceof Error ? error.message : String(error)}`);
693
+ // Check for debug screenshot on error
694
+ try {
695
+ const { tmpdir } = await import('os');
696
+ const { join } = await import('path');
697
+ const { existsSync } = await import('fs');
698
+ const debugPath = join(tmpdir(), 'github-copilot-not-found.png');
699
+ if (existsSync(debugPath)) {
700
+ logger.info(`📸 Debug screenshot saved to: ${debugPath}`);
701
+ }
702
+ }
703
+ catch {
704
+ // Ignore errors checking for screenshot
705
+ }
706
+ }
707
+ await waitForEnter();
708
+ return true;
709
+ case 'u':
710
+ // Untrack issue
711
+ if (!issue.number) {
712
+ // For standalone jobs, we can still untrack by name
713
+ const confirmUntrackStandalone = await confirm(`Remove standalone job "${issue.trackingName || issue.name}" from tracking?`, false);
714
+ if (confirmUntrackStandalone) {
715
+ await trackingService.removeByName(issue.name);
716
+ logger.success(`Standalone job removed from tracking`);
717
+ await waitForEnter();
718
+ return false;
719
+ }
720
+ return true;
721
+ }
722
+ const confirmUntrack = await confirm(`Remove issue #${issue.number} from tracking? (The issue and PR will remain open in GitHub)`, false);
723
+ if (confirmUntrack) {
724
+ await trackingService.removeByName(issue.name);
725
+ logger.success(`Issue #${issue.number} removed from tracking`);
726
+ await waitForEnter();
727
+ return false; // Return to main menu
728
+ }
729
+ return true;
730
+ case 'o':
731
+ // Respond to Copilot
732
+ if (!issue.number) {
733
+ logger.warning('Respond not available for standalone jobs');
734
+ await waitForEnter();
735
+ return true;
736
+ }
737
+ await runCommand('respond', config, [issue.number.toString()]);
738
+ return true;
739
+ case 'x':
740
+ // Close issue & untrack
741
+ if (!issue.number) {
742
+ logger.warning('Close & untrack not available for standalone jobs');
743
+ await waitForEnter();
744
+ return true;
745
+ }
746
+ const confirmClose = await confirm(`Close issue #${issue.number} and remove from tracking?`, false);
747
+ if (confirmClose) {
748
+ // runCommand('close') spawns a subprocess that closes the issue on GitHub
749
+ // AND removes it from tracking (via trackingService.remove()). Do NOT call
750
+ // removeByName() again — the issue is already untracked and a second remove
751
+ // would throw "Issue not found on server", crashing interactive mode.
752
+ const ok = await runCommand('close', config, [issue.number.toString()]);
753
+ if (ok) {
754
+ logger.success(`Issue #${issue.number} closed and untracked`);
755
+ }
756
+ else {
757
+ logger.warning(`Close command for issue #${issue.number} did not complete cleanly. See output above.`);
758
+ }
759
+ await waitForEnter();
760
+ return false; // Return to main menu
761
+ }
762
+ return true;
763
+ case 'l':
764
+ // Latest Copilot comment
765
+ if (!issue.number) {
766
+ logger.warning('Copilot comments not available for standalone jobs');
767
+ await waitForEnter();
768
+ return true;
769
+ }
770
+ logger.info(`Fetching latest comment for issue #${issue.number}...`);
771
+ try {
772
+ const prService = new PRService();
773
+ const comment = await prService.getLatestCopilotCommentForIssue(issue.number);
774
+ if (comment) {
775
+ const content = `Author: ${comment.author}\nDate: ${comment.date}\n\n${comment.body}`;
776
+ // Write to temp file with unique name
777
+ const timestamp = Date.now();
778
+ const tempFilePath = path.join(os.tmpdir(), `${issue.number}-${timestamp}copilotComment.md`);
779
+ fs.writeFileSync(tempFilePath, content);
780
+ // Make file readonly
781
+ fs.chmodSync(tempFilePath, 0o444);
782
+ // Open in IDE
783
+ const { EditorService } = await import('../services/editor/EditorService.js');
784
+ const editorService = new EditorService(config.editorCommand);
785
+ await editorService.openFile(tempFilePath);
786
+ logger.success('✓ Latest comment opened in editor');
787
+ }
788
+ else {
789
+ logger.warning(`No comments found or no PR linked to issue #${issue.number}`);
790
+ }
791
+ }
792
+ catch (error) {
793
+ logger.error(`Failed to get latest comment: ${error instanceof Error ? error.message : String(error)}`);
794
+ }
795
+ await waitForEnter();
796
+ return true;
797
+ case 'b':
798
+ // Back to list
799
+ return false;
800
+ case 'a':
801
+ // Admin actions
802
+ await showAdminMenu(config, trackingService);
803
+ return true;
804
+ case '?':
805
+ // Help
806
+ showHelp();
807
+ await waitForEnter();
808
+ return true;
809
+ case 'q':
810
+ // Quit entire program
811
+ process.exit(0);
812
+ default:
813
+ logger.error('Invalid action. Press ? for help.');
814
+ await waitForEnter();
815
+ return true;
816
+ }
817
+ }
818
+ /**
819
+ * Show admin actions menu
820
+ */
821
+ async function showAdminMenu(config, _trackingService) {
822
+ console.clear();
823
+ logger.section('Admin Actions');
824
+ logger.blank();
825
+ const action = await select('Select action:', [
826
+ 'Initialize/reconfigure',
827
+ 'Create new issue',
828
+ 'Track existing issues',
829
+ 'Clean closed issues',
830
+ 'Clean browsers',
831
+ 'Back',
832
+ ]);
833
+ switch (action) {
834
+ case 'Initialize/reconfigure':
835
+ await runCommand('init', config);
836
+ break;
837
+ case 'Create new issue':
838
+ await runCommand('create', config);
839
+ break;
840
+ case 'Track existing issues':
841
+ await runCommand('track', config);
842
+ break;
843
+ case 'Clean closed issues':
844
+ await runCommand('clean', config);
845
+ break;
846
+ case 'Clean browsers':
847
+ await runCommand('clean', config, ['--browsers']);
848
+ // Brief pause then return to menu (no key press needed)
849
+ // await new Promise((resolve) => setTimeout(resolve, 500));
850
+ return;
851
+ case 'Back':
852
+ return;
853
+ }
854
+ await waitForEnter();
855
+ }
856
+ /**
857
+ * Run a hublaunch command
858
+ */
859
+ async function runCommand(command, _config, args = []) {
860
+ try {
861
+ // On Windows, we need to use node to run the JS file directly
862
+ // since bin/hublaunch is a shell script
863
+ const distIndexUrl = new URL('../../dist/index.js', import.meta.url);
864
+ const distIndexPath = fileURLToPath(distIndexUrl);
865
+ const cmdArgs = [command, ...args].join(' ');
866
+ logger.blank();
867
+ // Use node to execute the compiled entry point
868
+ execSync(`node "${distIndexPath}" ${cmdArgs}`, { stdio: 'inherit' });
869
+ return true;
870
+ }
871
+ catch (error) {
872
+ logger.error(`Command failed: ${error instanceof Error ? error.message : String(error)}`);
873
+ return false;
874
+ }
875
+ }
876
+ /**
877
+ * Show help text
878
+ */
879
+ function showHelp() {
880
+ console.clear();
881
+ logger.section('HubLaunch Interactive Mode - Help');
882
+ logger.blank();
883
+ logger.info('📋 Main Menu:');
884
+ logger.info(" • Enter a number to select an issue (e.g., '1' or '1)')");
885
+ logger.info(' • Type / and search term to filter issues');
886
+ logger.info(' • Type n for next page, p for previous page');
887
+ logger.info(' • Type r to refresh issue list');
888
+ logger.info(' • Type a for admin actions');
889
+ logger.info(' • Type ? for this help message');
890
+ logger.info(' • Type q to quit');
891
+ logger.blank();
892
+ logger.info('📄 Pagination:');
893
+ logger.info(' • Default: 10 issues per page');
894
+ logger.info(' • Set PAGE_SIZE env var to change (e.g., PAGE_SIZE=15)');
895
+ logger.blank();
896
+ logger.info('⚙️ Issue Actions Menu:');
897
+ logger.info(' • i - Open issue in browser');
898
+ logger.info(' • p - Open plan file');
899
+ logger.info(' • v - Preview deployment');
900
+ logger.info(' • d - View code changes (diff)');
901
+ logger.info(' • r - View PR in browser');
902
+ logger.info(' • w - Watch tests');
903
+ logger.info(' • g - Get logs by ID');
904
+ logger.info(' • c - Checkout PR branch');
905
+ logger.info(' • m - Merge PR');
906
+ logger.info(' • s - Set priority');
907
+ logger.info(' • u - Untrack issue');
908
+ logger.info(' • o - Respond to Copilot');
909
+ logger.info(' • x - Close issue & untrack');
910
+ logger.info(' • l - Latest Copilot comment');
911
+ logger.info(' • b - Back to issue list');
912
+ logger.info(' • a - Admin actions');
913
+ logger.info(' • ? - Help');
914
+ logger.info(' • q - Quit');
915
+ logger.blank();
916
+ logger.info('🔧 Non-Interactive Mode:');
917
+ logger.info(' hula <command> [options]');
918
+ logger.info(' Example: hula merge 123');
919
+ logger.blank();
920
+ }
921
+ /**
922
+ * Wait for user to press Enter
923
+ */
924
+ async function waitForEnter() {
925
+ await inquirer.prompt({
926
+ type: 'input',
927
+ name: 'continue',
928
+ message: 'Press Enter to continue...',
929
+ });
930
+ }
931
+ /**
932
+ * Get status emoji
933
+ */
934
+ function getStatusEmoji(status) {
935
+ const emojiMap = {
936
+ [STATUS.UNASSIGNED]: '⚪', // White - Not assigned to anyone
937
+ [STATUS.INITIAL_WAITING]: '🟡', // Yellow - Copilot assigned, working
938
+ [STATUS.WAITING]: '🟠', // Orange - Waiting for Copilot response
939
+ [STATUS.READY]: '🟢', // Green - Ready for review/merge
940
+ };
941
+ return emojiMap[status] ?? '⚪';
942
+ }
943
+ /**
944
+ * Get priority emoji
945
+ */
946
+ function getPriorityEmoji(priority) {
947
+ const emojiMap = {
948
+ Critical: '🔴',
949
+ High: '🟠',
950
+ Medium: '🟡',
951
+ Low: '🟢',
952
+ Minimal: '⚪',
953
+ };
954
+ return emojiMap[priority] ?? '🟡';
955
+ }
956
+ /**
957
+ * Get deployment status emoji
958
+ */
959
+ function getDeploymentEmoji(status) {
960
+ if (!status)
961
+ return '⏳'; // Default to pending if not set
962
+ const emojiMap = {
963
+ pending: '⏳ pending',
964
+ success: '✅ success',
965
+ failed: '❌ failed',
966
+ };
967
+ return emojiMap[status] ?? '⏳';
968
+ }
969
+ /**
970
+ * Start background polling for status changes via configurable adapter
971
+ */
972
+ function startStatusPolling(pollingAdapter, trackingService, config, state) {
973
+ const pollIntervalMs = Math.max(config.pollIntervalSeconds, MIN_POLL_INTERVAL_SECONDS) * 1000;
974
+ let consecutiveFailures = 0;
975
+ const MAX_CONSECUTIVE_FAILURES = 3;
976
+ let authErrorShown = false;
977
+ const interval = setInterval(() => {
978
+ void (async () => {
979
+ try {
980
+ state.lastPollingCheckTime = new Date();
981
+ const timestamp = new Date().toLocaleTimeString();
982
+ logger.debug(`🔍 [${timestamp}] Polling via ${pollingAdapter.name} adapter...`);
983
+ // Fetch fresh issues — this is what gets passed to the adapter
984
+ const currentIssues = await trackingService.getAll();
985
+ // Delegate change detection to adapter
986
+ const changes = await pollingAdapter.pollForChanges(currentIssues);
987
+ if (changes.length > 0) {
988
+ logger.debug(`🔍 [${timestamp}] ${changes.length} change(s) detected`);
989
+ // Apply tracking status changes
990
+ for (const change of changes) {
991
+ if (change.field === 'trackingStatus' && change.newValue) {
992
+ await trackingService.updateStatus(change.issueNumber, change.newValue);
993
+ }
994
+ if (change.field === 'deploymentStatus' && change.newValue) {
995
+ await trackingService.updateDeploymentStatus(change.issueNumber, change.newValue);
996
+ }
997
+ if (change.field === 'deploymentStatus' && change.newValue === 'failed') {
998
+ await trackingService.markDeploymentNotified(change.issueNumber, new Date().toISOString());
999
+ }
1000
+ }
1001
+ // Trigger automatic refresh of the TUI
1002
+ state.shouldAutoRefresh = true;
1003
+ state.pendingRefresh = true;
1004
+ }
1005
+ consecutiveFailures = 0;
1006
+ authErrorShown = false;
1007
+ }
1008
+ catch (error) {
1009
+ consecutiveFailures++;
1010
+ const errorMessage = error instanceof Error ? error.message : String(error);
1011
+ const isAuthError = errorMessage.includes('authentication') ||
1012
+ errorMessage.includes('Unauthorized') ||
1013
+ errorMessage.includes('HTTP 401') ||
1014
+ errorMessage.includes('gh auth');
1015
+ const isNetworkError = errorMessage.includes('ENOTFOUND') ||
1016
+ errorMessage.includes('ETIMEDOUT') ||
1017
+ errorMessage.includes('ECONNREFUSED') ||
1018
+ errorMessage.includes('ECONNRESET');
1019
+ logger.debug(`[${new Date().toLocaleTimeString()}] Polling error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${errorMessage}`);
1020
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES && !authErrorShown) {
1021
+ logger.blank();
1022
+ if (isAuthError) {
1023
+ logger.warning(`⚠️ Auto-refresh paused: Authentication failed for ${pollingAdapter.name} adapter`);
1024
+ logger.info(pollingAdapter.name === 'hula-server'
1025
+ ? ' Run "hula login" to re-authenticate.'
1026
+ : ' Run "gh auth login" to re-authenticate.');
1027
+ }
1028
+ else if (isNetworkError) {
1029
+ logger.warning('⚠️ Auto-refresh paused: Unable to connect');
1030
+ logger.info(' Check your internet connection. Auto-refresh will resume when connection is restored.');
1031
+ }
1032
+ else {
1033
+ logger.warning('⚠️ Auto-refresh temporarily paused due to errors');
1034
+ }
1035
+ logger.blank();
1036
+ authErrorShown = true;
1037
+ }
1038
+ if (consecutiveFailures > MAX_CONSECUTIVE_FAILURES * 2) {
1039
+ consecutiveFailures = MAX_CONSECUTIVE_FAILURES;
1040
+ }
1041
+ }
1042
+ })();
1043
+ }, pollIntervalMs);
1044
+ return interval;
1045
+ }
1046
+ /**
1047
+ * Removed: viewJobStatus / viewJobLogs / getJobStatusEmoji
1048
+ *
1049
+ * Per-task job status was removed when the local tracking schema
1050
+ * dropped the `job*` fields as part of the Plan/Project/Task rename.
1051
+ * Use the `hublaunch logs <name>` command instead, which still proxies
1052
+ * the server's `/api/v1/logs/<name>` endpoint.
1053
+ */
1054
+ /**
1055
+ * Prompt user for tracking name and view logs
1056
+ */
1057
+ async function promptAndViewLogs(config) {
1058
+ const { trackingName } = await inquirer.prompt({
1059
+ type: 'input',
1060
+ name: 'trackingName',
1061
+ message: 'Enter tracking name:',
1062
+ validate: (input) => {
1063
+ if (!input.trim()) {
1064
+ return 'Tracking name is required';
1065
+ }
1066
+ return true;
1067
+ },
1068
+ });
1069
+ await executeLogs(trackingName.trim(), { lines: '100', raw: false }, config);
1070
+ }
1071
+ //# sourceMappingURL=interactive.js.map