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