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.
- package/CHANGELOG.md +144 -0
- package/README.md +195 -0
- package/bin/hublaunch +20 -0
- package/dist/commands/checkout.d.ts +7 -0
- package/dist/commands/checkout.d.ts.map +1 -0
- package/dist/commands/checkout.js +164 -0
- package/dist/commands/checkout.js.map +1 -0
- package/dist/commands/clean.d.ts +7 -0
- package/dist/commands/clean.d.ts.map +1 -0
- package/dist/commands/clean.js +206 -0
- package/dist/commands/clean.js.map +1 -0
- package/dist/commands/close.d.ts +7 -0
- package/dist/commands/close.d.ts.map +1 -0
- package/dist/commands/close.js +109 -0
- package/dist/commands/close.js.map +1 -0
- package/dist/commands/create.d.ts +7 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +367 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/diff.d.ts +7 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +98 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/execute.d.ts +7 -0
- package/dist/commands/execute.d.ts.map +1 -0
- package/dist/commands/execute.js +376 -0
- package/dist/commands/execute.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +1243 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/interactive.d.ts +12 -0
- package/dist/commands/interactive.d.ts.map +1 -0
- package/dist/commands/interactive.js +1071 -0
- package/dist/commands/interactive.js.map +1 -0
- package/dist/commands/launch.d.ts +16 -0
- package/dist/commands/launch.d.ts.map +1 -0
- package/dist/commands/launch.js +684 -0
- package/dist/commands/launch.js.map +1 -0
- package/dist/commands/login.d.ts +40 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +469 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logs.d.ts +22 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +105 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/merge.d.ts +7 -0
- package/dist/commands/merge.d.ts.map +1 -0
- package/dist/commands/merge.js +240 -0
- package/dist/commands/merge.js.map +1 -0
- package/dist/commands/preview.d.ts +7 -0
- package/dist/commands/preview.d.ts.map +1 -0
- package/dist/commands/preview.js +160 -0
- package/dist/commands/preview.js.map +1 -0
- package/dist/commands/project.d.ts +18 -0
- package/dist/commands/project.d.ts.map +1 -0
- package/dist/commands/project.js +352 -0
- package/dist/commands/project.js.map +1 -0
- package/dist/commands/respond.d.ts +7 -0
- package/dist/commands/respond.d.ts.map +1 -0
- package/dist/commands/respond.js +112 -0
- package/dist/commands/respond.js.map +1 -0
- package/dist/commands/setPriority.d.ts +7 -0
- package/dist/commands/setPriority.d.ts.map +1 -0
- package/dist/commands/setPriority.js +112 -0
- package/dist/commands/setPriority.js.map +1 -0
- package/dist/commands/track.d.ts +7 -0
- package/dist/commands/track.d.ts.map +1 -0
- package/dist/commands/track.js +140 -0
- package/dist/commands/track.js.map +1 -0
- package/dist/commands/upload.d.ts +21 -0
- package/dist/commands/upload.d.ts.map +1 -0
- package/dist/commands/upload.js +145 -0
- package/dist/commands/upload.js.map +1 -0
- package/dist/commands/view.d.ts +7 -0
- package/dist/commands/view.d.ts.map +1 -0
- package/dist/commands/view.js +168 -0
- package/dist/commands/view.js.map +1 -0
- package/dist/commands/watch.d.ts +7 -0
- package/dist/commands/watch.d.ts.map +1 -0
- package/dist/commands/watch.js +438 -0
- package/dist/commands/watch.js.map +1 -0
- package/dist/commands/worktree.d.ts +7 -0
- package/dist/commands/worktree.d.ts.map +1 -0
- package/dist/commands/worktree.js +122 -0
- package/dist/commands/worktree.js.map +1 -0
- package/dist/config/command-names.d.ts +16 -0
- package/dist/config/command-names.d.ts.map +1 -0
- package/dist/config/command-names.js +17 -0
- package/dist/config/command-names.js.map +1 -0
- package/dist/config/constants.d.ts +12 -0
- package/dist/config/constants.d.ts.map +1 -0
- package/dist/config/constants.js +12 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/index.d.ts +47 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +175 -0
- package/dist/config/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -0
- package/dist/services/api/HulaApiClient.d.ts +170 -0
- package/dist/services/api/HulaApiClient.d.ts.map +1 -0
- package/dist/services/api/HulaApiClient.js +233 -0
- package/dist/services/api/HulaApiClient.js.map +1 -0
- package/dist/services/diff/DiffService.d.ts +44 -0
- package/dist/services/diff/DiffService.d.ts.map +1 -0
- package/dist/services/diff/DiffService.js +126 -0
- package/dist/services/diff/DiffService.js.map +1 -0
- package/dist/services/editor/EditorService.d.ts +53 -0
- package/dist/services/editor/EditorService.d.ts.map +1 -0
- package/dist/services/editor/EditorService.js +161 -0
- package/dist/services/editor/EditorService.js.map +1 -0
- package/dist/services/git/GitService.d.ts +124 -0
- package/dist/services/git/GitService.d.ts.map +1 -0
- package/dist/services/git/GitService.js +423 -0
- package/dist/services/git/GitService.js.map +1 -0
- package/dist/services/git/WorktreeService.d.ts +144 -0
- package/dist/services/git/WorktreeService.d.ts.map +1 -0
- package/dist/services/git/WorktreeService.js +505 -0
- package/dist/services/git/WorktreeService.js.map +1 -0
- package/dist/services/github/ActionsService.d.ts +66 -0
- package/dist/services/github/ActionsService.d.ts.map +1 -0
- package/dist/services/github/ActionsService.js +114 -0
- package/dist/services/github/ActionsService.js.map +1 -0
- package/dist/services/github/AssetUploadService.d.ts +103 -0
- package/dist/services/github/AssetUploadService.d.ts.map +1 -0
- package/dist/services/github/AssetUploadService.js +299 -0
- package/dist/services/github/AssetUploadService.js.map +1 -0
- package/dist/services/github/BatchCopilotDetectionService.d.ts +58 -0
- package/dist/services/github/BatchCopilotDetectionService.d.ts.map +1 -0
- package/dist/services/github/BatchCopilotDetectionService.js +321 -0
- package/dist/services/github/BatchCopilotDetectionService.js.map +1 -0
- package/dist/services/github/CopilotDetectionService.d.ts +40 -0
- package/dist/services/github/CopilotDetectionService.d.ts.map +1 -0
- package/dist/services/github/CopilotDetectionService.js +280 -0
- package/dist/services/github/CopilotDetectionService.js.map +1 -0
- package/dist/services/github/CopilotService.d.ts +27 -0
- package/dist/services/github/CopilotService.d.ts.map +1 -0
- package/dist/services/github/CopilotService.js +249 -0
- package/dist/services/github/CopilotService.js.map +1 -0
- package/dist/services/github/DeploymentDetectionService.d.ts +38 -0
- package/dist/services/github/DeploymentDetectionService.d.ts.map +1 -0
- package/dist/services/github/DeploymentDetectionService.js +104 -0
- package/dist/services/github/DeploymentDetectionService.js.map +1 -0
- package/dist/services/github/GithubAppService.d.ts +40 -0
- package/dist/services/github/GithubAppService.d.ts.map +1 -0
- package/dist/services/github/GithubAppService.js +187 -0
- package/dist/services/github/GithubAppService.js.map +1 -0
- package/dist/services/github/IssueService.d.ts +65 -0
- package/dist/services/github/IssueService.d.ts.map +1 -0
- package/dist/services/github/IssueService.js +231 -0
- package/dist/services/github/IssueService.js.map +1 -0
- package/dist/services/github/LabelService.d.ts +36 -0
- package/dist/services/github/LabelService.d.ts.map +1 -0
- package/dist/services/github/LabelService.js +122 -0
- package/dist/services/github/LabelService.js.map +1 -0
- package/dist/services/github/PRService.d.ts +77 -0
- package/dist/services/github/PRService.d.ts.map +1 -0
- package/dist/services/github/PRService.js +287 -0
- package/dist/services/github/PRService.js.map +1 -0
- package/dist/services/github/ProjectService.d.ts +144 -0
- package/dist/services/github/ProjectService.d.ts.map +1 -0
- package/dist/services/github/ProjectService.js +804 -0
- package/dist/services/github/ProjectService.js.map +1 -0
- package/dist/services/hooks/HookExecutor.d.ts +66 -0
- package/dist/services/hooks/HookExecutor.d.ts.map +1 -0
- package/dist/services/hooks/HookExecutor.js +227 -0
- package/dist/services/hooks/HookExecutor.js.map +1 -0
- package/dist/services/hooks/index.d.ts +2 -0
- package/dist/services/hooks/index.d.ts.map +1 -0
- package/dist/services/hooks/index.js +2 -0
- package/dist/services/hooks/index.js.map +1 -0
- package/dist/services/logs/LogFormatterService.d.ts +17 -0
- package/dist/services/logs/LogFormatterService.d.ts.map +1 -0
- package/dist/services/logs/LogFormatterService.js +35 -0
- package/dist/services/logs/LogFormatterService.js.map +1 -0
- package/dist/services/logs/TempFileService.d.ts +27 -0
- package/dist/services/logs/TempFileService.d.ts.map +1 -0
- package/dist/services/logs/TempFileService.js +90 -0
- package/dist/services/logs/TempFileService.js.map +1 -0
- package/dist/services/logs/index.d.ts +3 -0
- package/dist/services/logs/index.d.ts.map +1 -0
- package/dist/services/logs/index.js +4 -0
- package/dist/services/logs/index.js.map +1 -0
- package/dist/services/plan/PlanService.d.ts +70 -0
- package/dist/services/plan/PlanService.d.ts.map +1 -0
- package/dist/services/plan/PlanService.js +211 -0
- package/dist/services/plan/PlanService.js.map +1 -0
- package/dist/services/polling/GitHubDirectPollingAdapter.d.ts +16 -0
- package/dist/services/polling/GitHubDirectPollingAdapter.d.ts.map +1 -0
- package/dist/services/polling/GitHubDirectPollingAdapter.js +87 -0
- package/dist/services/polling/GitHubDirectPollingAdapter.js.map +1 -0
- package/dist/services/polling/HulaServerPollingAdapter.d.ts +23 -0
- package/dist/services/polling/HulaServerPollingAdapter.d.ts.map +1 -0
- package/dist/services/polling/HulaServerPollingAdapter.js +78 -0
- package/dist/services/polling/HulaServerPollingAdapter.js.map +1 -0
- package/dist/services/polling/IStatusPollingAdapter.d.ts +29 -0
- package/dist/services/polling/IStatusPollingAdapter.d.ts.map +1 -0
- package/dist/services/polling/IStatusPollingAdapter.js +2 -0
- package/dist/services/polling/IStatusPollingAdapter.js.map +1 -0
- package/dist/services/polling/StatusPollingAdapterFactory.d.ts +14 -0
- package/dist/services/polling/StatusPollingAdapterFactory.d.ts.map +1 -0
- package/dist/services/polling/StatusPollingAdapterFactory.js +41 -0
- package/dist/services/polling/StatusPollingAdapterFactory.js.map +1 -0
- package/dist/services/polling/index.d.ts +5 -0
- package/dist/services/polling/index.d.ts.map +1 -0
- package/dist/services/polling/index.js +5 -0
- package/dist/services/polling/index.js.map +1 -0
- package/dist/services/preview/PreviewService.d.ts +86 -0
- package/dist/services/preview/PreviewService.d.ts.map +1 -0
- package/dist/services/preview/PreviewService.js +439 -0
- package/dist/services/preview/PreviewService.js.map +1 -0
- package/dist/services/tracking/ProjectTrackingAdapter.d.ts +41 -0
- package/dist/services/tracking/ProjectTrackingAdapter.d.ts.map +1 -0
- package/dist/services/tracking/ProjectTrackingAdapter.js +230 -0
- package/dist/services/tracking/ProjectTrackingAdapter.js.map +1 -0
- package/dist/services/tracking/RemoteTrackingAdapter.d.ts +62 -0
- package/dist/services/tracking/RemoteTrackingAdapter.d.ts.map +1 -0
- package/dist/services/tracking/RemoteTrackingAdapter.js +242 -0
- package/dist/services/tracking/RemoteTrackingAdapter.js.map +1 -0
- package/dist/services/tracking/TrackingAdapterFactory.d.ts +25 -0
- package/dist/services/tracking/TrackingAdapterFactory.d.ts.map +1 -0
- package/dist/services/tracking/TrackingAdapterFactory.js +55 -0
- package/dist/services/tracking/TrackingAdapterFactory.js.map +1 -0
- package/dist/services/tracking/TrackingService.d.ts +104 -0
- package/dist/services/tracking/TrackingService.d.ts.map +1 -0
- package/dist/services/tracking/TrackingService.js +256 -0
- package/dist/services/tracking/TrackingService.js.map +1 -0
- package/dist/services/tracking/UntrackedIssuesService.d.ts +20 -0
- package/dist/services/tracking/UntrackedIssuesService.d.ts.map +1 -0
- package/dist/services/tracking/UntrackedIssuesService.js +148 -0
- package/dist/services/tracking/UntrackedIssuesService.js.map +1 -0
- package/dist/services/tracking/local-file-migration.d.ts +16 -0
- package/dist/services/tracking/local-file-migration.d.ts.map +1 -0
- package/dist/services/tracking/local-file-migration.js +86 -0
- package/dist/services/tracking/local-file-migration.js.map +1 -0
- package/dist/templates/planning-instructions.md +752 -0
- package/dist/templates/proceed-instructions.md +453 -0
- package/dist/templates/scripts/hula-fix-commit.sh +110 -0
- package/dist/templates/scripts/hula-fix-setup.sh +145 -0
- package/dist/templates/scripts/hula-launch-run.sh +89 -0
- package/dist/templates/scripts/hula-merge-local.sh +138 -0
- package/dist/templates/scripts/hula-merge-remote.sh +74 -0
- package/dist/templates/scripts/hula-read-config.sh +38 -0
- package/dist/templates/scripts/hula-verify-gather.sh +135 -0
- package/dist/templates/scripts/hula-verify-post.sh +84 -0
- package/dist/templates/skills/hula-confirm/SKILL.md +361 -0
- package/dist/templates/skills/hula-create/SKILL.md +211 -0
- package/dist/templates/skills/hula-fix/SKILL.md +224 -0
- package/dist/templates/skills/hula-launch/SKILL.md +159 -0
- package/dist/templates/skills/hula-log/SKILL.md +105 -0
- package/dist/templates/skills/hula-merge/SKILL.md +387 -0
- package/dist/templates/skills/hula-plan/SKILL.md +148 -0
- package/dist/templates/skills/hula-upload/SKILL.md +115 -0
- package/dist/templates/skills/hula-verify/SKILL.md +754 -0
- package/dist/types/common.schema.d.ts +46 -0
- package/dist/types/common.schema.d.ts.map +1 -0
- package/dist/types/common.schema.js +60 -0
- package/dist/types/common.schema.js.map +1 -0
- package/dist/types/config.schema.d.ts +443 -0
- package/dist/types/config.schema.d.ts.map +1 -0
- package/dist/types/config.schema.js +126 -0
- package/dist/types/config.schema.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/issue.schema.d.ts +136 -0
- package/dist/types/issue.schema.d.ts.map +1 -0
- package/dist/types/issue.schema.js +49 -0
- package/dist/types/issue.schema.js.map +1 -0
- package/dist/types/pr.schema.d.ts +118 -0
- package/dist/types/pr.schema.d.ts.map +1 -0
- package/dist/types/pr.schema.js +40 -0
- package/dist/types/pr.schema.js.map +1 -0
- package/dist/types/tracking.schema.d.ts +340 -0
- package/dist/types/tracking.schema.d.ts.map +1 -0
- package/dist/types/tracking.schema.js +56 -0
- package/dist/types/tracking.schema.js.map +1 -0
- package/dist/utils/browser-launcher.d.ts +60 -0
- package/dist/utils/browser-launcher.d.ts.map +1 -0
- package/dist/utils/browser-launcher.js +263 -0
- package/dist/utils/browser-launcher.js.map +1 -0
- package/dist/utils/cache.d.ts +44 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +94 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/config-parser.d.ts +16 -0
- package/dist/utils/config-parser.d.ts.map +1 -0
- package/dist/utils/config-parser.js +210 -0
- package/dist/utils/config-parser.js.map +1 -0
- package/dist/utils/editor.d.ts +21 -0
- package/dist/utils/editor.d.ts.map +1 -0
- package/dist/utils/editor.js +213 -0
- package/dist/utils/editor.js.map +1 -0
- package/dist/utils/git-utils.d.ts +5 -0
- package/dist/utils/git-utils.d.ts.map +1 -0
- package/dist/utils/git-utils.js +18 -0
- package/dist/utils/git-utils.js.map +1 -0
- package/dist/utils/github-cli.d.ts +62 -0
- package/dist/utils/github-cli.d.ts.map +1 -0
- package/dist/utils/github-cli.js +492 -0
- package/dist/utils/github-cli.js.map +1 -0
- package/dist/utils/logger.d.ts +45 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +118 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/open-url.d.ts +19 -0
- package/dist/utils/open-url.d.ts.map +1 -0
- package/dist/utils/open-url.js +37 -0
- package/dist/utils/open-url.js.map +1 -0
- package/dist/utils/project-resolver.d.ts +13 -0
- package/dist/utils/project-resolver.d.ts.map +1 -0
- package/dist/utils/project-resolver.js +35 -0
- package/dist/utils/project-resolver.js.map +1 -0
- package/dist/utils/prompts.d.ts +80 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +326 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/shell.d.ts +54 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +152 -0
- package/dist/utils/shell.js.map +1 -0
- package/dist/utils/validators.d.ts +32 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +64 -0
- package/dist/utils/validators.js.map +1 -0
- 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
|