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,1243 @@
1
+ import { writeFile, readFile, mkdir, copyFile, chmod, rm, symlink, } from 'fs/promises';
2
+ import { existsSync } from 'fs';
3
+ import { join, relative, isAbsolute } from 'path';
4
+ import { logger } from '../utils/logger.js';
5
+ import { getRepoRoot } from '../utils/shell.js';
6
+ import { form, UserCancelledError } from '../utils/prompts.js';
7
+ import { fileURLToPath } from 'url';
8
+ import { dirname } from 'path';
9
+ import { DEFAULT_CONFIG } from '../types/config.schema.js';
10
+ import { DEFAULT_POLL_INTERVAL_SECONDS } from '../types/common.schema.js';
11
+ import { checkGhVersion } from '../utils/github-cli.js';
12
+ import { parseExistingConfig, mergeConfigs } from '../utils/config-parser.js';
13
+ /**
14
+ * Init command - Initialize HubLaunch configuration
15
+ */
16
+ export function initCommand(program, _config) {
17
+ program
18
+ .command('init')
19
+ .description('Initialize HubLaunch configuration (interactive)')
20
+ .option('--force', 'Overwrite existing configuration')
21
+ .action(async () => {
22
+ try {
23
+ await executeInit();
24
+ }
25
+ catch (error) {
26
+ // Handle user cancellation gracefully
27
+ if (error instanceof UserCancelledError) {
28
+ logger.info('\nCancelled.');
29
+ process.exit(0);
30
+ }
31
+ logger.error('Failed to initialize configuration:');
32
+ logger.error(error instanceof Error ? error.message : String(error));
33
+ process.exit(1);
34
+ }
35
+ });
36
+ }
37
+ /**
38
+ * Get the deployment startup script template based on auth provider
39
+ */
40
+ function getDeploymentStartupTemplate(authProvider) {
41
+ const header = `#!/usr/bin/env tsx
42
+ /**
43
+ * Deployment Startup Script
44
+ * Automatically logs in to preview deployment before opening
45
+ *
46
+ * Context passed as first argument (JSON string):
47
+ * {
48
+ * deploymentUrl: string;
49
+ * issueNumber?: number;
50
+ * prNumber?: number;
51
+ * prTitle?: string;
52
+ * prUrl?: string;
53
+ * }
54
+ */
55
+
56
+ import { chromium } from "@playwright/test";
57
+
58
+ interface HookContext {
59
+ deploymentUrl: string;
60
+ issueNumber?: number;
61
+ prNumber?: number;
62
+ }
63
+
64
+ async function main() {
65
+ const contextJson = process.argv[2];
66
+ if (!contextJson) {
67
+ console.error("Error: No context provided");
68
+ process.exit(1);
69
+ }
70
+
71
+ const context: HookContext = JSON.parse(contextJson);
72
+ console.log(\`🚀 Starting preview for: \${context.deploymentUrl}\`);
73
+
74
+ // Browser headless mode: Use env var or default to visible
75
+ const headless = process.env.CI === "true" || process.env.HEADLESS === "true";
76
+
77
+ const browser = await chromium.launch({
78
+ headless,
79
+ slowMo: headless ? 0 : 100,
80
+ });
81
+
82
+ const page = await browser.newPage();
83
+
84
+ try {
85
+ await page.goto(context.deploymentUrl);
86
+ `;
87
+ if (authProvider === 'clerk') {
88
+ return (header +
89
+ `
90
+ // Wait for Clerk authentication widget
91
+ await page.waitForSelector('[data-clerk-id]', { timeout: 5000 });
92
+
93
+ // Click sign-in button
94
+ await page.click('button:has-text("Sign in")');
95
+
96
+ // Fill in credentials from environment
97
+ const email = process.env.TEST_USER_EMAIL;
98
+ const password = process.env.TEST_USER_PASSWORD;
99
+
100
+ if (!email || !password) {
101
+ throw new Error(
102
+ "Required: TEST_USER_EMAIL and TEST_USER_PASSWORD environment variables"
103
+ );
104
+ }
105
+
106
+ // Enter email
107
+ await page.fill('input[name="identifier"]', email);
108
+ await page.click('button:has-text("Continue")');
109
+
110
+ // Enter password
111
+ await page.fill('input[name="password"]', password);
112
+ await page.click('button:has-text("Continue")');
113
+
114
+ // Wait for successful login
115
+ await page.waitForURL(\`\${context.deploymentUrl}/**\`, {
116
+ timeout: 10000
117
+ });
118
+
119
+ console.log("✅ Successfully logged in with Clerk!");
120
+ console.log("Browser will remain open for testing.");
121
+
122
+ // Keep browser open - user closes when done
123
+ // Don't call browser.close()
124
+
125
+ } catch (error) {
126
+ console.error("❌ Login failed:", error);
127
+ await browser.close();
128
+ process.exit(1);
129
+ }
130
+ }
131
+
132
+ main().catch(console.error);
133
+ `);
134
+ }
135
+ if (authProvider === 'auth0') {
136
+ return (header +
137
+ `
138
+ // Wait for Auth0 login form
139
+ await page.waitForSelector('[name="username"]', { timeout: 5000 });
140
+
141
+ // Fill in credentials from environment
142
+ const email = process.env.TEST_USER_EMAIL;
143
+ const password = process.env.TEST_USER_PASSWORD;
144
+
145
+ if (!email || !password) {
146
+ throw new Error(
147
+ "Required: TEST_USER_EMAIL and TEST_USER_PASSWORD environment variables"
148
+ );
149
+ }
150
+
151
+ // Auth0 login flow
152
+ await page.fill('input[name="username"]', email);
153
+ await page.fill('input[name="password"]', password);
154
+ await page.click('button[type="submit"]');
155
+
156
+ // Wait for redirect back to app
157
+ await page.waitForURL(\`\${context.deploymentUrl}/**\`, {
158
+ timeout: 10000
159
+ });
160
+
161
+ console.log("✅ Successfully logged in with Auth0!");
162
+ console.log("Browser will remain open for testing.");
163
+
164
+ } catch (error) {
165
+ console.error("❌ Login failed:", error);
166
+ await browser.close();
167
+ process.exit(1);
168
+ }
169
+ }
170
+
171
+ main().catch(console.error);
172
+ `);
173
+ }
174
+ // Generic template (no auth or custom)
175
+ return (header +
176
+ `
177
+ // Add your custom preview initialization logic here
178
+ //
179
+ // Examples:
180
+ // - Authenticate to deployment
181
+ // - Seed test data via API
182
+ // - Configure browser state
183
+ // - Navigate to specific page
184
+
185
+ console.log("✅ Preview setup complete!");
186
+ console.log("Browser opened at:", context.deploymentUrl);
187
+
188
+ // Optionally close browser after delay
189
+ // await new Promise(resolve => setTimeout(resolve, 30000));
190
+ // await browser.close();
191
+
192
+ } catch (error) {
193
+ console.error("❌ Preview setup failed:", error);
194
+ await browser.close();
195
+ process.exit(1);
196
+ }
197
+ }
198
+
199
+ main().catch(console.error);
200
+ `);
201
+ }
202
+ const HOOKS_README_TEMPLATE = `# HubLaunch Hooks
203
+
204
+ This directory contains project-specific hooks that extend HubLaunch functionality.
205
+
206
+ ## Available Hooks
207
+
208
+ ### \`deploymentStartupScript.ts\`
209
+ Runs before opening a preview deployment. Use for:
210
+ - **Authentication** (Clerk, Auth0, custom login flows)
211
+ - **Browser automation** (Playwright-based interactions)
212
+ - **Test data seeding** (API calls to populate data)
213
+ - **Environment setup** (configure local state)
214
+
215
+ **Context Provided**:
216
+ \`\`\`typescript
217
+ {
218
+ deploymentUrl: string; // Preview deployment URL
219
+ issueNumber?: number; // Associated issue number
220
+ prNumber?: number; // Associated PR number
221
+ prTitle?: string; // PR title
222
+ prUrl?: string; // PR URL
223
+ }
224
+ \`\`\`
225
+
226
+ **Usage**:
227
+ - Configured in \`.hublaunch/hublaunch.config.ts\`
228
+ - Runs automatically when you run \`hula preview\`
229
+ - Runs in **background** (non-blocking)
230
+
231
+ ---
232
+
233
+ ## Creating Custom Hooks
234
+
235
+ All hooks must:
236
+ 1. Be TypeScript files with \`.ts\` extension
237
+ 2. Accept context as first CLI argument (JSON string)
238
+ 3. Handle errors gracefully (try/catch)
239
+ 4. Exit with code 0 on success, non-zero on failure
240
+
241
+ ### Example: Custom Hook
242
+
243
+ \`\`\`typescript
244
+ #!/usr/bin/env tsx
245
+
246
+ interface HookContext {
247
+ [key: string]: unknown;
248
+ }
249
+
250
+ async function main() {
251
+ // Parse context from CLI argument
252
+ const contextJson = process.argv[2];
253
+ const context: HookContext = JSON.parse(contextJson);
254
+
255
+ console.log("Hook executing with context:", context);
256
+
257
+ // Your custom logic here
258
+ // ...
259
+
260
+ console.log("✅ Hook completed successfully!");
261
+ }
262
+
263
+ main().catch((error) => {
264
+ console.error("❌ Hook failed:", error);
265
+ process.exit(1);
266
+ });
267
+ \`\`\`
268
+
269
+ ---
270
+
271
+ ## Configuration
272
+
273
+ Configure hooks in \`.hublaunch/hublaunch.config.ts\`:
274
+
275
+ \`\`\`typescript
276
+ export const config = {
277
+ // ... other config ...
278
+
279
+ hooks: {
280
+ deploymentStartup: ".hublaunch/hooks/deploymentStartupScript.ts",
281
+ beforePreview: ".hublaunch/hooks/beforePreview.ts", // optional
282
+ afterPreview: ".hublaunch/hooks/afterPreview.ts", // optional
283
+ beforeMerge: ".hublaunch/hooks/beforeMerge.ts", // optional
284
+ afterMerge: ".hublaunch/hooks/afterMerge.ts", // optional
285
+ },
286
+ };
287
+ \`\`\`
288
+
289
+ ---
290
+
291
+ ## Environment Variables
292
+
293
+ Store sensitive data in environment variables:
294
+
295
+ \`\`\`bash
296
+ # .env (gitignored)
297
+ TEST_USER_EMAIL=test@example.com
298
+ TEST_USER_PASSWORD=your-test-password
299
+ CUSTOM_API_KEY=your-api-key
300
+ \`\`\`
301
+
302
+ Access in hooks:
303
+ \`\`\`typescript
304
+ const email = process.env.TEST_USER_EMAIL;
305
+ \`\`\`
306
+
307
+ ---
308
+
309
+ ## Debugging Hooks
310
+
311
+ Run hook manually for testing:
312
+
313
+ \`\`\`bash
314
+ # Test with sample context
315
+ tsx .hublaunch/hooks/deploymentStartupScript.ts '{"deploymentUrl":"https://example.com","prNumber":123}'
316
+
317
+ # Enable debug mode
318
+ DEBUG=true tsx .hublaunch/hooks/deploymentStartupScript.ts '{"deploymentUrl":"https://example.com"}'
319
+ \`\`\`
320
+
321
+ ---
322
+
323
+ ## More Information
324
+
325
+ - **Full Documentation**: https://github.com/YizYah/hub-launch/blob/main/docs/hooks-and-plugins.md
326
+ - **Examples**: https://github.com/YizYah/hub-launch/tree/main/examples
327
+ - **Support**: https://github.com/YizYah/hub-launch/issues
328
+ `;
329
+ const ADAPTERS_README_TEMPLATE = `# HubLaunch Adapters
330
+
331
+ This directory contains custom service adapters for extending HubLaunch functionality.
332
+
333
+ ## Directory Structure
334
+
335
+ \`\`\`
336
+ adapters/
337
+ └── deployment/ # Custom deployment service adapters
338
+ └── README.md
339
+ \`\`\`
340
+
341
+ ## What are Adapters?
342
+
343
+ Adapters allow you to integrate custom services with HubLaunch. For example:
344
+ - **Deployment Services**: Netlify, Railway, custom deployment platforms
345
+
346
+ ## Creating Custom Adapters
347
+
348
+ All adapters must:
349
+ 1. Export as default export
350
+ 2. Have a unique \`name\` property
351
+ 3. Implement \`isConfigured()\` method
352
+
353
+ ## Configuration
354
+
355
+ Configure adapters in \`.hublaunch/hublaunch.config.ts\`:
356
+
357
+ \`\`\`typescript
358
+ export const config = {
359
+ services: {
360
+ deployment: {
361
+ provider: "custom",
362
+ customPath: ".hublaunch/adapters/deployment/customDeployment.ts",
363
+ },
364
+ },
365
+ };
366
+ \`\`\`
367
+
368
+ ## More Information
369
+
370
+ - **Full Documentation**: https://github.com/YizYah/hub-launch/blob/main/docs/hooks-and-plugins.md
371
+ - **Examples**: https://github.com/YizYah/hub-launch/tree/main/examples/adapters
372
+ `;
373
+ const DEPLOYMENT_ADAPTER_README_TEMPLATE = `# Custom Deployment Service Adapters
374
+
375
+ Create custom deployment service adapters for your deployment platform.
376
+
377
+ ## Interface
378
+
379
+ Deployment adapters should implement deployment-related functionality.
380
+
381
+ ## Example: Custom Deployment Service
382
+
383
+ \`\`\`typescript
384
+ // customDeployment.ts
385
+ export default class CustomDeploymentService {
386
+ readonly name = "custom";
387
+
388
+ isConfigured(): boolean {
389
+ return Boolean(process.env.CUSTOM_DEPLOY_TOKEN);
390
+ }
391
+
392
+ async getDeploymentUrl(prNumber: number): Promise<string> {
393
+ // Your implementation
394
+ const response = await fetch(
395
+ \`https://your-platform.com/deployments/pr-\${prNumber}\`,
396
+ {
397
+ headers: {
398
+ Authorization: \`Bearer \${process.env.CUSTOM_DEPLOY_TOKEN}\`,
399
+ },
400
+ }
401
+ );
402
+
403
+ const data = await response.json();
404
+ return data.url;
405
+ }
406
+ }
407
+ \`\`\`
408
+
409
+ ## Configuration
410
+
411
+ \`\`\`typescript
412
+ // .hublaunch/hublaunch.config.ts
413
+ export const config = {
414
+ services: {
415
+ deployment: {
416
+ provider: "custom",
417
+ customPath: ".hublaunch/adapters/deployment/customDeployment.ts",
418
+ },
419
+ },
420
+ };
421
+ \`\`\`
422
+ `;
423
+ const TEMPLATES_README_TEMPLATE = `# HubLaunch Templates
424
+
425
+ This directory contains project-specific templates for issues, PRs, and other documents.
426
+
427
+ ## Available Templates
428
+
429
+ ### Issue Templates
430
+ Create custom issue templates for different types of work:
431
+ - \`bug-template.md\`
432
+ - \`feature-template.md\`
433
+ - \`task-template.md\`
434
+
435
+ ### PR Templates
436
+ Create PR description templates:
437
+ - \`pr-template.md\`
438
+
439
+ ## Usage
440
+
441
+ Templates are automatically detected and used by HubLaunch when creating issues and PRs.
442
+
443
+ ## Example: Issue Template
444
+
445
+ \`\`\`markdown
446
+ # [Feature Name]
447
+
448
+ ## Description
449
+ [Brief description of the feature]
450
+
451
+ ## Requirements
452
+ - [ ] Requirement 1
453
+ - [ ] Requirement 2
454
+
455
+ ## Acceptance Criteria
456
+ - [ ] Criteria 1
457
+ - [ ] Criteria 2
458
+
459
+ ## Technical Notes
460
+ [Any technical considerations]
461
+ \`\`\`
462
+ `;
463
+ /**
464
+ * Generate hook templates based on authentication provider
465
+ */
466
+ async function generateHookTemplates(projectRoot, authProvider, overrideStartupScript) {
467
+ const hooksDir = join(projectRoot, '.hublaunch/hooks');
468
+ // Create hooks directory
469
+ await mkdir(hooksDir, { recursive: true });
470
+ logger.success(`Created directory: .hublaunch/hooks/`);
471
+ // Generate deploymentStartupScript.ts template based on auth provider
472
+ // Only create if auth provider is configured (not "none")
473
+ if (authProvider !== 'none') {
474
+ const startupScriptPath = join(hooksDir, 'deploymentStartupScript.ts');
475
+ if (!existsSync(startupScriptPath)) {
476
+ const template = getDeploymentStartupTemplate(authProvider);
477
+ await writeFile(startupScriptPath, template, 'utf-8');
478
+ logger.success(`Created: .hublaunch/hooks/deploymentStartupScript.ts`);
479
+ }
480
+ else if (overrideStartupScript) {
481
+ const template = getDeploymentStartupTemplate(authProvider);
482
+ await writeFile(startupScriptPath, template, 'utf-8');
483
+ logger.success(`Overridden: .hublaunch/hooks/deploymentStartupScript.ts`);
484
+ }
485
+ else {
486
+ logger.info('Keeping existing deploymentStartupScript.ts');
487
+ }
488
+ }
489
+ // Generate README for hooks
490
+ const readmePath = join(hooksDir, 'README.md');
491
+ if (!existsSync(readmePath)) {
492
+ await writeFile(readmePath, HOOKS_README_TEMPLATE, 'utf-8');
493
+ logger.success(`Created: .hublaunch/hooks/README.md`);
494
+ }
495
+ logger.blank();
496
+ logger.info('📖 See .hublaunch/hooks/README.md for hook documentation');
497
+ }
498
+ /**
499
+ * Generate adapter directory structure with documentation
500
+ */
501
+ async function generateAdapterStructure(projectRoot) {
502
+ const adaptersDir = join(projectRoot, '.hublaunch/adapters');
503
+ const deploymentDir = join(adaptersDir, 'deployment');
504
+ // Create adapter directories
505
+ await mkdir(deploymentDir, { recursive: true });
506
+ logger.success(`Created directory: .hublaunch/adapters/`);
507
+ // Generate main adapters README
508
+ const adaptersReadmePath = join(adaptersDir, 'README.md');
509
+ if (!existsSync(adaptersReadmePath)) {
510
+ await writeFile(adaptersReadmePath, ADAPTERS_README_TEMPLATE, 'utf-8');
511
+ logger.success(`Created: .hublaunch/adapters/README.md`);
512
+ }
513
+ // Generate deployment adapter README
514
+ const deploymentReadmePath = join(deploymentDir, 'README.md');
515
+ if (!existsSync(deploymentReadmePath)) {
516
+ await writeFile(deploymentReadmePath, DEPLOYMENT_ADAPTER_README_TEMPLATE, 'utf-8');
517
+ logger.success(`Created: .hublaunch/adapters/deployment/README.md`);
518
+ }
519
+ }
520
+ /**
521
+ * Generate templates directory structure with documentation
522
+ */
523
+ async function generateTemplatesStructure(projectRoot) {
524
+ const templatesDir = join(projectRoot, '.hublaunch/templates');
525
+ // Create templates directory
526
+ await mkdir(templatesDir, { recursive: true });
527
+ logger.success(`Created directory: .hublaunch/templates/`);
528
+ // Generate templates README
529
+ const templatesReadmePath = join(templatesDir, 'README.md');
530
+ if (!existsSync(templatesReadmePath)) {
531
+ await writeFile(templatesReadmePath, TEMPLATES_README_TEMPLATE, 'utf-8');
532
+ logger.success(`Created: .hublaunch/templates/README.md`);
533
+ }
534
+ }
535
+ /**
536
+ * Agent Skills manifest: maps template directory → skill name.
537
+ * Source: src/templates/skills/<templateDir>/SKILL.md
538
+ * Canonical destination: .agents/skills/<skillName>/SKILL.md
539
+ *
540
+ * .claude/commands/<skillName>.md is created as a symlink into .agents/skills/
541
+ * so Claude Code can resolve the same file without any content duplication.
542
+ * Standard: https://agentskills.io/specification
543
+ */
544
+ const AGENT_SKILLS = [
545
+ { templateDir: 'hula-plan', skillName: 'hula-plan' },
546
+ { templateDir: 'hula-confirm', skillName: 'hula-confirm' },
547
+ { templateDir: 'hula-fix', skillName: 'hula-fix' },
548
+ { templateDir: 'hula-create', skillName: 'hula-create' },
549
+ { templateDir: 'hula-verify', skillName: 'hula-verify' },
550
+ { templateDir: 'hula-merge', skillName: 'hula-merge' },
551
+ { templateDir: 'hula-launch', skillName: 'hula-launch' },
552
+ { templateDir: 'hula-upload', skillName: 'hula-upload' },
553
+ { templateDir: 'hula-log', skillName: 'hula-log' },
554
+ ];
555
+ /**
556
+ * Copy planning instruction templates to .hublaunch/.
557
+ * These are used by the hula-plan and hula-confirm skills at runtime.
558
+ */
559
+ async function initializePlanningInstructions(projectRoot, overridePlanningInstructions, overrideProceedInstructions) {
560
+ const __filename = fileURLToPath(import.meta.url);
561
+ const __dirname = dirname(__filename);
562
+ const templatesSourceDir = join(__dirname, '..', 'templates');
563
+ const instructionsTemplatePath = join(templatesSourceDir, 'planning-instructions.md');
564
+ const instructionsDestPath = join(projectRoot, '.hublaunch', 'planning-instructions.md');
565
+ if (!existsSync(instructionsDestPath) || overridePlanningInstructions) {
566
+ await copyFile(instructionsTemplatePath, instructionsDestPath);
567
+ logger.success('Updated: .hublaunch/planning-instructions.md');
568
+ }
569
+ else {
570
+ logger.info('Keeping existing planning instructions');
571
+ }
572
+ const proceedInstructionsTemplatePath = join(templatesSourceDir, 'proceed-instructions.md');
573
+ const proceedInstructionsDestPath = join(projectRoot, '.hublaunch', 'proceed-instructions.md');
574
+ if (!existsSync(proceedInstructionsDestPath) || overrideProceedInstructions) {
575
+ await copyFile(proceedInstructionsTemplatePath, proceedInstructionsDestPath);
576
+ logger.success('Updated: .hublaunch/proceed-instructions.md');
577
+ }
578
+ else {
579
+ logger.info('Keeping existing proceed instructions');
580
+ }
581
+ }
582
+ /**
583
+ * Initialize Agent Skills for AI-assisted hula workflows.
584
+ *
585
+ * Single source of truth: .agents/skills/<name>/SKILL.md (Agent Skills spec).
586
+ *
587
+ * Each harness gets a reference to that canonical file:
588
+ * - GitHub Copilot, Cursor, Codex, and other Agent Skills-compatible tools
589
+ * read directly from .agents/skills/<name>/SKILL.md.
590
+ * - Claude Code reads from .claude/commands/<name>.md, which is created as
591
+ * a symlink pointing into .agents/skills/ (relative path so it works after
592
+ * cloning on any machine). No content is duplicated.
593
+ *
594
+ * Because the symlinks are named <name>.md (not SKILL.md) and live under
595
+ * .claude/commands/ (not .agents/skills/), GitHub Copilot's Agent Skills
596
+ * scanner does not pick them up, so no duplicate commands appear.
597
+ *
598
+ * Any existing .claude/skills/ directory is removed on init to clean up
599
+ * legacy installations that previously wrote full files there.
600
+ *
601
+ * Agent Skills standard: https://agentskills.io/specification
602
+ */
603
+ async function initializeAgentSkills(projectRoot) {
604
+ const __filename = fileURLToPath(import.meta.url);
605
+ const __dirname = dirname(__filename);
606
+ const skillsSourceDir = join(__dirname, '..', 'templates', 'skills');
607
+ // Canonical location — single source of truth.
608
+ const agentsSkillsDir = join(projectRoot, '.agents', 'skills');
609
+ await mkdir(agentsSkillsDir, { recursive: true });
610
+ // Claude Code reads from .claude/commands/.
611
+ const claudeCommandsDir = join(projectRoot, '.claude', 'commands');
612
+ await mkdir(claudeCommandsDir, { recursive: true });
613
+ // Remove .claude/skills/ if it exists — legacy path that caused duplicates.
614
+ const claudeSkillsDir = join(projectRoot, '.claude', 'skills');
615
+ try {
616
+ await rm(claudeSkillsDir, { recursive: true, force: true });
617
+ }
618
+ catch {
619
+ // Does not exist — nothing to clean up.
620
+ }
621
+ for (const { templateDir, skillName } of AGENT_SKILLS) {
622
+ const srcSkillMd = join(skillsSourceDir, templateDir, 'SKILL.md');
623
+ // Write the canonical SKILL.md.
624
+ const agentsDestDir = join(agentsSkillsDir, skillName);
625
+ await mkdir(agentsDestDir, { recursive: true });
626
+ await copyFile(srcSkillMd, join(agentsDestDir, 'SKILL.md'));
627
+ // Create a symlink in .claude/commands/ pointing back to the canonical file.
628
+ // Relative path so the symlink works after cloning on any machine.
629
+ const symlinkPath = join(claudeCommandsDir, `${skillName}.md`);
630
+ const symlinkTarget = join('..', '..', '.agents', 'skills', skillName, 'SKILL.md');
631
+ // Remove any existing file/symlink before (re-)creating.
632
+ await rm(symlinkPath, { force: true });
633
+ await symlink(symlinkTarget, symlinkPath);
634
+ logger.success(`Updated skill: ${skillName}`);
635
+ }
636
+ logger.blank();
637
+ logger.info('🎉 Agent Skills installed — works with Claude Code, GitHub Copilot, Codex, and 30+ AI tools');
638
+ logger.info('💡 Invoke with /hula-plan, /hula-fix, /hula-create, etc. (note: hyphens, not dots)');
639
+ }
640
+ /**
641
+ * List of bundled shell scripts that support the chat participant workflows.
642
+ * These scripts are copied from src/templates/scripts/ to .github/scripts/
643
+ * during `hula init` to reduce terminal approval clicks in VS Code.
644
+ */
645
+ const BUNDLED_SCRIPTS = [
646
+ 'hula-read-config.sh',
647
+ 'hula-fix-setup.sh',
648
+ 'hula-fix-commit.sh',
649
+ 'hula-verify-gather.sh',
650
+ 'hula-verify-post.sh',
651
+ 'hula-merge-local.sh',
652
+ 'hula-merge-remote.sh',
653
+ 'hula-launch-run.sh',
654
+ ];
655
+ /**
656
+ * Initialize bundled shell scripts for chat participant workflows.
657
+ * Copies scripts from src/templates/scripts/ to .github/scripts/ in the target project.
658
+ * These scripts bundle multiple terminal commands into single invocations,
659
+ * reducing VS Code terminal approval popups from ~28 to ~7 per workflow cycle.
660
+ */
661
+ async function initializeScriptFiles(projectRoot) {
662
+ const __filename = fileURLToPath(import.meta.url);
663
+ const __dirname = dirname(__filename);
664
+ const scriptsSourceDir = join(__dirname, '..', 'templates', 'scripts');
665
+ const scriptsDestDir = join(projectRoot, '.github', 'scripts');
666
+ // Create .github/scripts directory
667
+ await mkdir(scriptsDestDir, { recursive: true });
668
+ for (const scriptName of BUNDLED_SCRIPTS) {
669
+ const srcPath = join(scriptsSourceDir, scriptName);
670
+ const destPath = join(scriptsDestDir, scriptName);
671
+ // Always overwrite scripts with latest version (they are deterministic tooling, not user-customized)
672
+ await copyFile(srcPath, destPath);
673
+ await chmod(destPath, 0o755);
674
+ logger.success(`Updated script: .github/scripts/${scriptName}`);
675
+ }
676
+ }
677
+ /**
678
+ * Recommended VS Code auto-approval patterns for hula agent commands.
679
+ * These regex patterns match the terminal commands used by hula workflows
680
+ * so VS Code Copilot Chat won't prompt "Allow" for each one.
681
+ */
682
+ const VSCODE_AUTO_APPROVE_SETTINGS = {
683
+ // Hula CLI commands
684
+ '/^hula\\b/': true,
685
+ // Git read-only commands
686
+ '/^git (status|log|diff|fetch|show|branch|worktree list|rev-parse|remote)\\b/': true,
687
+ // Git write commands used by hula workflows
688
+ '/^git (add|commit|push|checkout|worktree add|worktree remove)\\b/': true,
689
+ // GitHub CLI read commands
690
+ '/^gh (issue view|pr list|pr view|pr diff|repo view)\\b/': true,
691
+ // GitHub CLI write commands used by hula workflows
692
+ '/^gh pr (comment|merge)\\b/': true,
693
+ // Hula bash scripts deployed to .github/scripts/
694
+ '/^bash \\.github\\/scripts\\/hula-/': true,
695
+ // Common safe commands
696
+ '/^(ls|cat|head|tail|wc|echo|mkdir|rm -f|cd|pwd)\\b/': true,
697
+ };
698
+ /**
699
+ * Initialize VS Code auto-approval settings for hula agent commands.
700
+ * Creates or deep-merges `.vscode/settings.json` with the auto-approve config.
701
+ * Preserves all existing settings — only the `chat.tools.terminal.autoApprove` key is touched.
702
+ */
703
+ async function initVsCodeAutoApproval(projectRoot) {
704
+ const vscodeDir = join(projectRoot, '.vscode');
705
+ const settingsPath = join(vscodeDir, 'settings.json');
706
+ // Ensure .vscode/ directory exists
707
+ await mkdir(vscodeDir, { recursive: true });
708
+ let existingSettings = {};
709
+ if (existsSync(settingsPath)) {
710
+ try {
711
+ const raw = await readFile(settingsPath, 'utf-8');
712
+ // Strip JSON comments (// and /* */) before parsing
713
+ const stripped = raw
714
+ .replace(/\/\/.*$/gm, '')
715
+ .replace(/\/\*[\s\S]*?\*\//g, '');
716
+ existingSettings = JSON.parse(stripped);
717
+ }
718
+ catch {
719
+ logger.warning('Could not parse existing .vscode/settings.json — creating fresh file');
720
+ existingSettings = {};
721
+ }
722
+ }
723
+ // Deep-merge: only touch the auto-approve key
724
+ existingSettings['chat.tools.terminal.autoApprove'] =
725
+ VSCODE_AUTO_APPROVE_SETTINGS;
726
+ const content = JSON.stringify(existingSettings, null, 2) + '\n';
727
+ await writeFile(settingsPath, content, 'utf-8');
728
+ logger.success('VS Code auto-approval configured: .vscode/settings.json');
729
+ }
730
+ /**
731
+ * Display current configuration summary
732
+ */
733
+ function displayConfigSummary(config) {
734
+ logger.blank();
735
+ logger.section('Current Configuration');
736
+ logger.blank();
737
+ logger.info('📝 Basic Settings:');
738
+ logger.listItem(`Editor: ${config.editorCommand}`, 1);
739
+ logger.listItem(`Plan Path: ${config.planPath}`, 1);
740
+ logger.listItem(`Tracking File: ${config.trackingFile}`, 1);
741
+ logger.listItem(`Create Issues: ${config.createIssuesPreference}`, 1);
742
+ logger.listItem(`Plan Choices: ${config.planChoices}`, 1);
743
+ logger.listItem(`Skip Untracked Prompt: ${config.skipUntrackedPrompt}`, 1);
744
+ logger.listItem(`Worktree Base Path: ${config.worktreeBasePath ?? '.hula-worktrees'}`, 1);
745
+ if (config.teamMembers && config.teamMembers.length > 0) {
746
+ logger.info('👥 Team Members:');
747
+ config.teamMembers.forEach((member) => logger.listItem(member, 1));
748
+ }
749
+ if (config.services) {
750
+ logger.blank();
751
+ logger.info('⚙️ Services:');
752
+ if (config.services.authentication) {
753
+ logger.listItem(`Auth Provider: ${config.services.authentication.provider}`, 1);
754
+ }
755
+ }
756
+ logger.blank();
757
+ }
758
+ export async function executeInit() {
759
+ logger.section('Initialize HubLaunch Configuration');
760
+ // Validate GitHub CLI version
761
+ logger.info('Checking GitHub CLI version...');
762
+ const versionValid = await checkGhVersion('2.4.0');
763
+ if (!versionValid) {
764
+ logger.error('GitHub CLI version check failed.');
765
+ logger.error('Please upgrade to continue with initialization.');
766
+ process.exit(1);
767
+ }
768
+ logger.success('✓ GitHub CLI version is sufficient');
769
+ logger.blank();
770
+ const repoRoot = await getRepoRoot();
771
+ const configDir = join(repoRoot, '.hublaunch');
772
+ const configPath = join(configDir, 'hublaunch.config.js');
773
+ const configPathTs = join(configDir, 'hublaunch.config.ts');
774
+ // Load existing config if it exists (check .ts first, then .js)
775
+ let existingConfig = null;
776
+ const existingConfigPath = existsSync(configPathTs)
777
+ ? configPathTs
778
+ : existsSync(configPath)
779
+ ? configPath
780
+ : null;
781
+ if (existingConfigPath) {
782
+ logger.info(`📋 Detected existing config: ${relative(repoRoot, existingConfigPath)}`);
783
+ existingConfig = await parseExistingConfig(existingConfigPath);
784
+ if (existingConfig) {
785
+ const preservedKeys = [];
786
+ if (existingConfig.hulaApiKey)
787
+ preservedKeys.push('hulaApiKey');
788
+ if (existingConfig.teamMembers && existingConfig.teamMembers.length > 0)
789
+ preservedKeys.push('teamMembers');
790
+ if (existingConfig.anthropicApiKey)
791
+ preservedKeys.push('anthropicApiKey');
792
+ if (existingConfig.daytonaApiKey)
793
+ preservedKeys.push('daytonaApiKey');
794
+ if (existingConfig.daytonaTier !== undefined)
795
+ preservedKeys.push('daytonaTier');
796
+ if (existingConfig.services)
797
+ preservedKeys.push('services');
798
+ if (existingConfig.hooks)
799
+ preservedKeys.push('hooks');
800
+ if (preservedKeys.length > 0) {
801
+ logger.success(`✓ Preserved settings: ${preservedKeys.join(', ')}`);
802
+ }
803
+ logger.blank();
804
+ }
805
+ }
806
+ // Check if config already exists - proceed with overwrite
807
+ if (existsSync(configPath)) {
808
+ logger.info('Configuration file already exists. Proceeding with reinitialization...');
809
+ }
810
+ // Helper to convert absolute paths back to relative paths for display/editing
811
+ const toRelativePath = (absolutePath, defaultPath) => {
812
+ if (!absolutePath)
813
+ return defaultPath;
814
+ if (!isAbsolute(absolutePath))
815
+ return absolutePath;
816
+ // Convert absolute path to relative by removing repoRoot prefix
817
+ const relativePath = relative(repoRoot, absolutePath);
818
+ return relativePath || defaultPath;
819
+ };
820
+ // Display existing config if present
821
+ // Display existing config if present (for reference)
822
+ if (existingConfig && existingConfig.planPath) {
823
+ displayConfigSummary(existingConfig);
824
+ }
825
+ else {
826
+ logger.info("Let's set up your HubLaunch configuration...");
827
+ }
828
+ logger.blank();
829
+ logger.info('📝 Configure ALL settings (use ↑↓ arrows to navigate, Enter to edit each field):');
830
+ logger.info('💡 Hint: For enum fields, valid values are shown in parentheses');
831
+ logger.info('💡 Leave fields unchanged to keep current values, or edit to update');
832
+ logger.blank();
833
+ // Check if override files exist - only ask about overriding if they exist
834
+ const startupScriptExists = existsSync(join(repoRoot, '.hublaunch/hooks/deploymentStartupScript.ts'));
835
+ // Build form choices dynamically - only include override options for files that exist
836
+ const formChoices = [
837
+ {
838
+ name: 'anthropicApiKey',
839
+ message: 'Anthropic OAuth token (sk-ant-oat01-...) — get it at https://claude.ai/settings',
840
+ initial: existingConfig?.anthropicApiKey || '',
841
+ },
842
+ {
843
+ name: 'daytonaApiKey',
844
+ message: 'Daytona API Key (required for hula launch) — get it at https://app.daytona.io',
845
+ initial: existingConfig?.daytonaApiKey || '',
846
+ },
847
+ {
848
+ name: 'daytonaTier',
849
+ message: 'Daytona Org Tier (1=Starter, 2=Standard, 3=Pro, 4=Enterprise)',
850
+ initial: (existingConfig?.daytonaTier ?? 1).toString(),
851
+ validate: (value) => {
852
+ const n = parseInt(value, 10);
853
+ return ([1, 2, 3, 4].includes(n)) || 'Must be 1, 2, 3, or 4';
854
+ },
855
+ },
856
+ {
857
+ name: 'editorCommand',
858
+ message: 'Editor (cursor|vsCode|other)',
859
+ initial: existingConfig?.editorCommand || DEFAULT_CONFIG.editorCommand,
860
+ validate: (value) => {
861
+ const valid = ['cursor', 'vsCode', 'other'];
862
+ return valid.includes(value) || `Must be one of: ${valid.join(', ')}`;
863
+ },
864
+ },
865
+ {
866
+ name: 'planPath',
867
+ message: 'Plan files directory',
868
+ initial: toRelativePath(existingConfig?.planPath, DEFAULT_CONFIG.planPath),
869
+ },
870
+ {
871
+ name: 'trackingFile',
872
+ message: 'Tracking file path',
873
+ initial: toRelativePath(existingConfig?.trackingFile, DEFAULT_CONFIG.trackingFile),
874
+ },
875
+ {
876
+ name: 'createIssuesPreference',
877
+ message: 'Create issues (alwaysLatest|chooseRecentPlan)',
878
+ initial: existingConfig?.createIssuesPreference ||
879
+ DEFAULT_CONFIG.createIssuesPreference,
880
+ validate: (value) => {
881
+ const valid = ['alwaysLatest', 'chooseRecentPlan'];
882
+ return valid.includes(value) || `Must be one of: ${valid.join(', ')}`;
883
+ },
884
+ },
885
+ {
886
+ name: 'planChoices',
887
+ message: 'Number of recent plans to show',
888
+ initial: (existingConfig?.planChoices || DEFAULT_CONFIG.planChoices).toString(),
889
+ validate: (value) => {
890
+ const num = parseInt(value, 10);
891
+ return (!isNaN(num) && num > 0) || 'Please enter a positive number';
892
+ },
893
+ },
894
+ {
895
+ name: 'skipUntrackedPrompt',
896
+ message: 'Skip untracked prompt (true|false)',
897
+ initial: (existingConfig?.skipUntrackedPrompt ??
898
+ DEFAULT_CONFIG.skipUntrackedPrompt).toString(),
899
+ validate: (value) => {
900
+ const valid = ['true', 'false', 'yes', 'no'];
901
+ return valid.includes(value.toLowerCase()) || 'Must be true or false';
902
+ },
903
+ },
904
+ {
905
+ name: 'teamMembers',
906
+ message: 'Team members (comma-separated)',
907
+ initial: existingConfig?.teamMembers?.join(', ') || '',
908
+ },
909
+ {
910
+ name: 'authProvider',
911
+ message: 'Auth provider (none|clerk|auth0|custom)',
912
+ initial: existingConfig?.services?.authentication?.provider || 'clerk',
913
+ validate: (value) => {
914
+ const valid = ['none', 'clerk', 'auth0', 'custom'];
915
+ return valid.includes(value) || `Must be one of: ${valid.join(', ')}`;
916
+ },
917
+ },
918
+ {
919
+ name: 'configureVsCodeAutoApproval',
920
+ message: 'Configure VS Code auto-approval for hula agent commands? (true|false)',
921
+ initial: 'true',
922
+ validate: (value) => {
923
+ const valid = ['true', 'false', 'yes', 'no'];
924
+ return valid.includes(value.toLowerCase()) || 'Must be true or false';
925
+ },
926
+ },
927
+ ];
928
+ // Only add override options for files that already exist
929
+ if (startupScriptExists) {
930
+ formChoices.push({
931
+ name: 'overrideStartupScript',
932
+ message: 'Override existing deploymentStartupScript.ts? (true|false)',
933
+ initial: 'false',
934
+ validate: (value) => {
935
+ const valid = ['true', 'false', 'yes', 'no'];
936
+ return valid.includes(value.toLowerCase()) || 'Must be true or false';
937
+ },
938
+ });
939
+ }
940
+ // Single Unified Form for ALL fields
941
+ const formData = await form('HubLaunch Configuration', formChoices);
942
+ // Parse form results with fallbacks
943
+ const anthropicApiKey = (formData.anthropicApiKey || '').trim() || undefined;
944
+ const daytonaApiKey = (formData.daytonaApiKey || '').trim() || undefined;
945
+ const daytonaTier = parseInt(formData.daytonaTier || '1', 10);
946
+ const editorCommand = (formData.editorCommand ||
947
+ DEFAULT_CONFIG.editorCommand);
948
+ const planPath = (formData.planPath || DEFAULT_CONFIG.planPath).replace(/\\/g, '/');
949
+ const trackingFile = (formData.trackingFile || DEFAULT_CONFIG.trackingFile).replace(/\\/g, '/');
950
+ const createIssuesPreference = (formData.createIssuesPreference ||
951
+ DEFAULT_CONFIG.createIssuesPreference);
952
+ const planChoices = parseInt(formData.planChoices || DEFAULT_CONFIG.planChoices.toString(), 10);
953
+ const skipUntrackedPrompt = ['true', 'yes'].includes((formData.skipUntrackedPrompt || 'false').toLowerCase());
954
+ const teamMembers = (formData.teamMembers || '')
955
+ .split(',')
956
+ .map((m) => m.trim())
957
+ .filter((m) => m.length > 0);
958
+ const authProvider = (formData.authProvider || 'none');
959
+ // Override defaults to true for new files (they will be created), false for existing
960
+ const overrideStartupScript = startupScriptExists
961
+ ? ['true', 'yes'].includes((formData.overrideStartupScript || 'false').toLowerCase())
962
+ : true;
963
+ // Parse VS Code auto-approval preference
964
+ const configureVsCodeAutoApproval = ['true', 'yes'].includes((formData.configureVsCodeAutoApproval || 'true').toLowerCase());
965
+ // Generate config content
966
+ const baseConfig = {
967
+ editorCommand,
968
+ planPath,
969
+ trackingFile,
970
+ createIssuesPreference,
971
+ planChoices,
972
+ skipUntrackedPrompt,
973
+ teamMembers,
974
+ };
975
+ // Add services if configured
976
+ if (authProvider !== 'none') {
977
+ baseConfig.services = {
978
+ authentication: {
979
+ provider: authProvider,
980
+ },
981
+ };
982
+ }
983
+ // Add hooks if configured
984
+ if (authProvider !== 'none') {
985
+ baseConfig.hooks = {
986
+ deploymentStartup: '.hublaunch/hooks/deploymentStartupScript.ts',
987
+ };
988
+ }
989
+ // Merge with existing config (existing values take precedence)
990
+ const finalConfig = existingConfig
991
+ ? mergeConfigs(existingConfig, baseConfig)
992
+ : baseConfig;
993
+ // mergeConfigs makes existingConfig win for top-level keys, so assigning to
994
+ // baseConfig would not let the user clear a previously-saved value. Assign
995
+ // post-merge so the user's typed value (including cleared) is what gets written.
996
+ finalConfig.anthropicApiKey = anthropicApiKey;
997
+ finalConfig.daytonaApiKey = daytonaApiKey;
998
+ finalConfig.daytonaTier = daytonaTier;
999
+ // Generate hooks config section
1000
+ let hooksConfigSection;
1001
+ if (finalConfig.hooks) {
1002
+ hooksConfigSection = `
1003
+ // Lifecycle hooks
1004
+ hooks: ${JSON.stringify(finalConfig.hooks, null, 2).replace(/\n/g, '\n ')},`;
1005
+ }
1006
+ else {
1007
+ hooksConfigSection = `
1008
+ // Lifecycle hooks (optional)
1009
+ // hooks: {
1010
+ // deploymentStartup: ".hublaunch/hooks/deploymentStartupScript.ts",
1011
+ // },`;
1012
+ }
1013
+ // Generate services config section (without credentials)
1014
+ let servicesConfigSection;
1015
+ if (finalConfig.services) {
1016
+ servicesConfigSection = `
1017
+ // Service configuration
1018
+ services: ${JSON.stringify(finalConfig.services, null, 2).replace(/\n/g, '\n ')},`;
1019
+ }
1020
+ else {
1021
+ servicesConfigSection = `
1022
+ // Service configuration (optional)
1023
+ // services: {
1024
+ // authentication: { provider: "none" },
1025
+ // },`;
1026
+ }
1027
+ // Generate config content with preserved API key
1028
+ const apiKeyLine = finalConfig.hulaApiKey
1029
+ ? `\n hulaApiKey: "${finalConfig.hulaApiKey}",`
1030
+ : '\n // hulaApiKey: "your-api-key-here",';
1031
+ const anthropicKeyLine = finalConfig.anthropicApiKey
1032
+ ? `\n anthropicApiKey: "${finalConfig.anthropicApiKey}",`
1033
+ : '\n // anthropicApiKey: "", // Anthropic OAuth token (sk-ant-oat01-...) — get it at https://claude.ai/settings (requires a paid Claude.ai plan — Pro or Max)';
1034
+ const daytonaKeyLine = finalConfig.daytonaApiKey
1035
+ ? `\n daytonaApiKey: "${finalConfig.daytonaApiKey}",`
1036
+ : '\n // daytonaApiKey: "", // Daytona API key — get your key at https://app.daytona.io (or set DAYTONA_API_KEY env var)';
1037
+ const daytonaTierLine = `\n daytonaTier: ${finalConfig.daytonaTier ?? 1}, // Daytona org tier (1–4). Tier 1=Starter, 2=Standard, 3=Pro, 4=Enterprise. Override with --daytona-tier or DAYTONA_TIER env var.`;
1038
+ const containerResourcesLine = finalConfig.containerResources
1039
+ ? `\n containerResources: { ${[
1040
+ finalConfig.containerResources.cpu !== undefined ? `cpu: ${finalConfig.containerResources.cpu}` : null,
1041
+ finalConfig.containerResources.memory !== undefined ? `memory: ${finalConfig.containerResources.memory}` : null,
1042
+ finalConfig.containerResources.disk !== undefined ? `disk: ${finalConfig.containerResources.disk}` : null,
1043
+ ].filter(Boolean).join(', ')} }, // Per-sandbox resource override (cpu: 1–32, memory: 1–128 GiB, disk: 5–200 GiB). Override per-task with --container-cpu/memory/disk flags.`
1044
+ : '\n containerResources: { cpu: 2, memory: 8, disk: 10 }, // Per-sandbox resource override (cpu: 1–32, memory: 1–128 GiB, disk: 5–200 GiB). Override per-task with --container-cpu/memory/disk flags.';
1045
+ const updateNotificationUrlLine = finalConfig.updateNotificationUrl
1046
+ ? `\n updateNotificationUrl: "${finalConfig.updateNotificationUrl}",`
1047
+ : '\n // updateNotificationUrl: "https://hooks.slack.com/services/T0/B0/secret", // POSTed on every terminal task transition (Slack hosts get { text } body, others get generic JSON). See hula-server API.md.';
1048
+ const configContent = `export const config = {
1049
+ // API Configuration${apiKeyLine}${anthropicKeyLine}${daytonaKeyLine}${daytonaTierLine}${containerResourcesLine}${updateNotificationUrlLine}
1050
+
1051
+ // Editor configuration
1052
+ editorCommand: "${finalConfig.editorCommand || editorCommand}",
1053
+
1054
+ // Plan files configuration
1055
+ planPath: "${finalConfig.planPath || planPath}",
1056
+ trackingFile: "${finalConfig.trackingFile || trackingFile}",
1057
+ createIssuesPreference: "${finalConfig.createIssuesPreference || createIssuesPreference}",
1058
+ planChoices: ${finalConfig.planChoices ?? planChoices},
1059
+
1060
+ // Tracking behavior
1061
+ skipUntrackedPrompt: ${finalConfig.skipUntrackedPrompt ?? skipUntrackedPrompt},
1062
+
1063
+ // Polling / auto-refresh
1064
+ pollingProvider: "${finalConfig.pollingProvider || 'hula-server'}", // "hula-server" | "github"
1065
+ pollIntervalSeconds: ${finalConfig.pollIntervalSeconds ?? DEFAULT_POLL_INTERVAL_SECONDS}, // Minimum 60
1066
+
1067
+ // Team members${finalConfig.teamMembers && finalConfig.teamMembers.length > 0
1068
+ ? `\n teamMembers: ${JSON.stringify(finalConfig.teamMembers)},`
1069
+ : '\n // teamMembers: [],'}
1070
+
1071
+ // Command aliases (optional)
1072
+ // commandAliases: {
1073
+ // primary: ["hula", "hub-launch", "hl"],
1074
+ // },
1075
+
1076
+ // Priority levels (optional)
1077
+ // priorityLevels: [
1078
+ // { name: "Critical", color: "red", emoji: "🔴" },
1079
+ // { name: "High", color: "orange", emoji: "🟠" },
1080
+ // { name: "Medium", color: "yellow", emoji: "🟡" },
1081
+ // { name: "Low", color: "green", emoji: "🟢" },
1082
+ // { name: "Minimal", color: "gray", emoji: "⚪" },
1083
+ // ],
1084
+
1085
+ // Issue sorting (optional)
1086
+ // issueSortOrder: {
1087
+ // groupBy: "priority",
1088
+ // sortBy: "created",
1089
+ // direction: "desc",
1090
+ // },
1091
+ ${servicesConfigSection}
1092
+ ${hooksConfigSection}
1093
+
1094
+ // Browser automation
1095
+ browserHeadless: "auto", // "auto" | "always" | "never"
1096
+
1097
+ // Worktree base path (used by /hula-fix, /hula-merge, /hula-upload)
1098
+ worktreeBasePath: "${(finalConfig.worktreeBasePath || '.hula-worktrees').replace(/\\/g, '\\\\').replace(/"/g, '\\"')}",
1099
+ };
1100
+ `;
1101
+ // Create directory if it doesn't exist
1102
+ if (!existsSync(configDir)) {
1103
+ await mkdir(configDir, { recursive: true });
1104
+ }
1105
+ // Create plans directory if it doesn't exist
1106
+ const plansDir = join(repoRoot, planPath);
1107
+ if (!existsSync(plansDir)) {
1108
+ await mkdir(plansDir, { recursive: true });
1109
+ logger.success(`Created plans directory: ${planPath}`);
1110
+ }
1111
+ // Create worktrees directory if it doesn't exist
1112
+ const worktreeBasePath = finalConfig.worktreeBasePath || '.hula-worktrees';
1113
+ const worktreesDir = isAbsolute(worktreeBasePath)
1114
+ ? worktreeBasePath
1115
+ : join(repoRoot, worktreeBasePath);
1116
+ if (!existsSync(worktreesDir)) {
1117
+ await mkdir(worktreesDir, { recursive: true });
1118
+ logger.success(`Created worktrees directory: ${worktreeBasePath}`);
1119
+ }
1120
+ // Write config file
1121
+ await writeFile(configPath, configContent, 'utf-8');
1122
+ logger.blank();
1123
+ logger.success(`✨ Configuration created: ${configPath}`);
1124
+ // Add hublaunch.config.js to .gitignore
1125
+ const gitignorePath = join(repoRoot, '.gitignore');
1126
+ try {
1127
+ let gitignoreContent = '';
1128
+ if (existsSync(gitignorePath)) {
1129
+ gitignoreContent = await import('fs/promises').then((fs) => fs.readFile(gitignorePath, 'utf-8'));
1130
+ }
1131
+ // Check if hublaunch.config.js is already in .gitignore
1132
+ if (!gitignoreContent.includes('hublaunch.config.js')) {
1133
+ // Add entry with comment
1134
+ const newEntry = gitignoreContent.endsWith('\n')
1135
+ ? '# HubLaunch configuration\nhublaunch.config.js\n'
1136
+ : '\n# HubLaunch configuration\nhublaunch.config.js\n';
1137
+ gitignoreContent += newEntry;
1138
+ await import('fs/promises').then((fs) => fs.writeFile(gitignorePath, gitignoreContent, 'utf-8'));
1139
+ logger.success('✨ Added hublaunch.config.js to .gitignore');
1140
+ }
1141
+ // Check if worktree base path is already in .gitignore (skip absolute paths — they're outside the repo)
1142
+ if (!isAbsolute(worktreeBasePath) &&
1143
+ !gitignoreContent.includes(worktreeBasePath)) {
1144
+ gitignoreContent += `\n# HubLaunch worktrees\n${worktreeBasePath}/\n`;
1145
+ await import('fs/promises').then((fs) => fs.writeFile(gitignorePath, gitignoreContent, 'utf-8'));
1146
+ logger.success(`✨ Added ${worktreeBasePath}/ to .gitignore`);
1147
+ }
1148
+ // Warn if .agents/skills is in .gitignore — it must be committed so all
1149
+ // team members get GitHub Copilot and Claude Code support.
1150
+ if (gitignoreContent.includes('.agents/skills')) {
1151
+ logger.warning('⚠️ .agents/skills is listed in .gitignore — this will prevent GitHub Copilot and Claude Code from finding skills.');
1152
+ logger.warning(' Remove ".agents/skills" from .gitignore to allow the skills directory to be committed.');
1153
+ }
1154
+ }
1155
+ catch (error) {
1156
+ logger.warning(`Could not update .gitignore: ${error instanceof Error ? error.message : String(error)}`);
1157
+ }
1158
+ // Create .env template file with instructions (no credentials) in project root
1159
+ const envPath = join(process.cwd(), '.env');
1160
+ const envLines = [
1161
+ '# HubLaunch Environment Variables',
1162
+ '# This file contains sensitive credentials and should NOT be committed to version control',
1163
+ '# It is automatically added to .gitignore',
1164
+ '',
1165
+ ];
1166
+ // Add credential templates based on selected services
1167
+ if (authProvider !== 'none') {
1168
+ envLines.push('# Authentication credentials for preview automation');
1169
+ envLines.push('TEST_USER_EMAIL=test@example.com');
1170
+ envLines.push('TEST_USER_PASSWORD=your_test_password');
1171
+ envLines.push('');
1172
+ }
1173
+ // Only create .env file if it doesn't exist
1174
+ if (!existsSync(envPath)) {
1175
+ await writeFile(envPath, envLines.join('\n'), 'utf-8');
1176
+ logger.success(`✨ Template .env file created: ${envPath}`);
1177
+ logger.warning('⚠️ Remember: Never commit .env files to version control!');
1178
+ }
1179
+ else {
1180
+ logger.info(`ℹ️ .env file already exists at ${envPath} - skipping to preserve your existing configuration`);
1181
+ logger.info(' Add any required environment variables manually if needed');
1182
+ }
1183
+ // Always generate hooks directory structure (with README and optionally script)
1184
+ logger.blank();
1185
+ logger.info('Generating hooks directory structure...');
1186
+ await generateHookTemplates(repoRoot, authProvider, overrideStartupScript);
1187
+ // Always generate adapters directory structure
1188
+ logger.blank();
1189
+ logger.info('Generating adapter directory structure...');
1190
+ await generateAdapterStructure(repoRoot);
1191
+ // Always generate templates directory structure
1192
+ logger.blank();
1193
+ logger.info('Generating templates directory structure...');
1194
+ await generateTemplatesStructure(repoRoot);
1195
+ // Copy planning instruction templates to .hublaunch/ for skills to reference at runtime
1196
+ logger.blank();
1197
+ logger.info('Updating planning instruction templates...');
1198
+ await initializePlanningInstructions(repoRoot, true, true);
1199
+ // Initialize Agent Skills for AI-assisted planning (works with Copilot, Claude Code, and others)
1200
+ logger.blank();
1201
+ logger.info('Setting up Agent Skills for AI-assisted planning...');
1202
+ await initializeAgentSkills(repoRoot);
1203
+ // Initialize bundled shell scripts for chat participant workflows
1204
+ logger.blank();
1205
+ logger.info('Setting up bundled shell scripts for chat workflows...');
1206
+ await initializeScriptFiles(repoRoot);
1207
+ // Configure VS Code auto-approval for hula agent commands
1208
+ if (configureVsCodeAutoApproval) {
1209
+ logger.blank();
1210
+ logger.info('Configuring VS Code auto-approval for hula agent commands...');
1211
+ await initVsCodeAutoApproval(repoRoot);
1212
+ }
1213
+ logger.blank();
1214
+ logger.blank();
1215
+ logger.info('Next steps:');
1216
+ logger.listItem('Review and customize the configuration if needed', 1);
1217
+ // Show credential setup instructions based on selected services
1218
+ if (authProvider !== 'none') {
1219
+ logger.listItem('⚠️ Configure credentials in .env file at project root:', 1);
1220
+ logger.listItem('- TEST_USER_EMAIL: Test user email for preview automation', 2);
1221
+ logger.listItem('- TEST_USER_PASSWORD: Test user password', 2);
1222
+ logger.blank();
1223
+ }
1224
+ if (authProvider !== 'none') {
1225
+ logger.listItem('Customize .hublaunch/hooks/deploymentStartupScript.ts as needed', 1);
1226
+ }
1227
+ logger.listItem('Create issue plan files in the plans directory', 1);
1228
+ logger.listItem("Run 'hublaunch create' to create your first issue", 1);
1229
+ logger.blank();
1230
+ logger.info('💡 Or use the Agent Skills slash commands in any AI chat:');
1231
+ logger.info(' - /hula-plan <description> - Generate implementation plan');
1232
+ logger.info(' - /hula-confirm - Validate and refine the plan');
1233
+ logger.info(' - /hula-upload - Upload plan to origin/main');
1234
+ logger.info(' - /hula-launch <branch> - Launch issue from plan');
1235
+ logger.info(' - /hula-create <name> [--handoff <user>] [--priority <level>] - Create GitHub issue');
1236
+ logger.info(' - /hula-fix <problem> - Fix issues on PR branch');
1237
+ logger.info(' - /hula-verify [issue] - Verify implementation against plan');
1238
+ logger.info(' - /hula-merge - Merge PR and restore original branch');
1239
+ logger.info(' - /hula-log [issueName] - View job logs in editor');
1240
+ logger.blank();
1241
+ logger.info('💡 These skills work with GitHub Copilot (VS Code), Claude Code, and other Agent Skills-compatible tools.');
1242
+ }
1243
+ //# sourceMappingURL=init.js.map