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,804 @@
|
|
|
1
|
+
import { ghGraphQL, getRepoInfo, runGh } from '../../utils/github-cli.js';
|
|
2
|
+
import { logger } from '../../utils/logger.js';
|
|
3
|
+
import { STATUS } from '../../types/common.schema.js';
|
|
4
|
+
import { DeploymentDetectionService } from './DeploymentDetectionService.js';
|
|
5
|
+
/**
|
|
6
|
+
* Service for GitHub Projects V2 operations
|
|
7
|
+
* Single Responsibility: GitHub Projects integration
|
|
8
|
+
*/
|
|
9
|
+
export class ProjectService {
|
|
10
|
+
fieldsCache = new Map();
|
|
11
|
+
/**
|
|
12
|
+
* Ensure GitHub CLI has project scope
|
|
13
|
+
*/
|
|
14
|
+
async ensureProjectScope() {
|
|
15
|
+
logger.info('Checking GitHub CLI project scope...');
|
|
16
|
+
try {
|
|
17
|
+
// Check current scopes
|
|
18
|
+
const status = await runGh(['auth', 'status'], { silent: true });
|
|
19
|
+
if (status.includes('project')) {
|
|
20
|
+
logger.success('Project scope already configured');
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
logger.warning('Project scope not found. Adding...');
|
|
24
|
+
// Add project scope with hostname to avoid interactive prompt
|
|
25
|
+
await runGh([
|
|
26
|
+
'auth',
|
|
27
|
+
'refresh',
|
|
28
|
+
'-s',
|
|
29
|
+
'project',
|
|
30
|
+
'--hostname',
|
|
31
|
+
'github.com',
|
|
32
|
+
]);
|
|
33
|
+
logger.success('Project scope added successfully');
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logger.error('Failed to add project scope');
|
|
38
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Find existing HubLaunch project
|
|
44
|
+
*/
|
|
45
|
+
async findExistingProject(owner) {
|
|
46
|
+
logger.info("Searching for existing 'HubLaunch Issue Tracking' project...");
|
|
47
|
+
try {
|
|
48
|
+
const result = await runGh([
|
|
49
|
+
'project',
|
|
50
|
+
'list',
|
|
51
|
+
'--owner',
|
|
52
|
+
owner,
|
|
53
|
+
'--format',
|
|
54
|
+
'json',
|
|
55
|
+
]);
|
|
56
|
+
const projects = JSON.parse(result);
|
|
57
|
+
const project = projects.projects.find((p) => p.title === 'HubLaunch Issue Tracking');
|
|
58
|
+
if (project) {
|
|
59
|
+
logger.success(`Found existing project: #${project.number}`);
|
|
60
|
+
return project.number;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
logger.error('Could not list projects');
|
|
66
|
+
logger.debug(error instanceof Error ? error.message : String(error));
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Create new HubLaunch project
|
|
72
|
+
*/
|
|
73
|
+
async createProject(owner) {
|
|
74
|
+
logger.info("Creating new 'HubLaunch Issue Tracking' project...");
|
|
75
|
+
try {
|
|
76
|
+
const result = await runGh([
|
|
77
|
+
'project',
|
|
78
|
+
'create',
|
|
79
|
+
'--owner',
|
|
80
|
+
owner,
|
|
81
|
+
'--title',
|
|
82
|
+
'HubLaunch Issue Tracking',
|
|
83
|
+
'--format',
|
|
84
|
+
'json',
|
|
85
|
+
]);
|
|
86
|
+
const data = JSON.parse(result);
|
|
87
|
+
if (!data.number) {
|
|
88
|
+
throw new Error('Could not extract project number from response');
|
|
89
|
+
}
|
|
90
|
+
logger.success(`Created project: #${data.number}`);
|
|
91
|
+
return data.number;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
logger.error('Failed to create project');
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Get or create project
|
|
100
|
+
*/
|
|
101
|
+
async getOrCreateProject(owner) {
|
|
102
|
+
// First check if project exists
|
|
103
|
+
const existingProject = await this.findExistingProject(owner);
|
|
104
|
+
if (existingProject !== null) {
|
|
105
|
+
return existingProject;
|
|
106
|
+
}
|
|
107
|
+
// Create new project
|
|
108
|
+
return this.createProject(owner);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Get project global ID
|
|
112
|
+
*/
|
|
113
|
+
async getProjectId(owner, projectNumber) {
|
|
114
|
+
const query = `
|
|
115
|
+
query($owner: String!, $number: Int!) {
|
|
116
|
+
organization(login: $owner) {
|
|
117
|
+
projectV2(number: $number) {
|
|
118
|
+
id
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
`;
|
|
123
|
+
try {
|
|
124
|
+
const result = await ghGraphQL(query, { owner, number: projectNumber }, { suppressErrors: true });
|
|
125
|
+
// Check if organization query returned data
|
|
126
|
+
if (result.data.organization?.projectV2?.id) {
|
|
127
|
+
return result.data.organization.projectV2.id;
|
|
128
|
+
}
|
|
129
|
+
// Organization query returned null, try user query
|
|
130
|
+
throw new Error('Organization not found or project not accessible');
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// Try user query if organization fails
|
|
134
|
+
logger.debug(`Organization query failed, trying user query for ${owner}`);
|
|
135
|
+
const userQuery = `
|
|
136
|
+
query($owner: String!, $number: Int!) {
|
|
137
|
+
user(login: $owner) {
|
|
138
|
+
projectV2(number: $number) {
|
|
139
|
+
id
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
`;
|
|
144
|
+
try {
|
|
145
|
+
const userResult = await ghGraphQL(userQuery, { owner, number: projectNumber }, { suppressErrors: true });
|
|
146
|
+
if (userResult.data.user?.projectV2?.id) {
|
|
147
|
+
return userResult.data.user.projectV2.id;
|
|
148
|
+
}
|
|
149
|
+
throw new Error('User project not found or not accessible');
|
|
150
|
+
}
|
|
151
|
+
catch (userError) {
|
|
152
|
+
logger.debug(`Both organization and user queries failed for ${owner}/#${projectNumber}`);
|
|
153
|
+
throw new Error(`Could not find project #${projectNumber} for owner "${owner}". Ensure the owner is correct and you have access to the project.`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* List project fields (with caching to avoid rate limits)
|
|
159
|
+
*/
|
|
160
|
+
async listFields(owner, projectNumber) {
|
|
161
|
+
const cacheKey = `${owner}/${projectNumber}`;
|
|
162
|
+
// Check cache first
|
|
163
|
+
if (this.fieldsCache.has(cacheKey)) {
|
|
164
|
+
logger.debug(`Using cached fields for project ${cacheKey}`);
|
|
165
|
+
return this.fieldsCache.get(cacheKey);
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
logger.debug(`Fetching fields for project ${cacheKey}`);
|
|
169
|
+
const result = await runGh([
|
|
170
|
+
'project',
|
|
171
|
+
'field-list',
|
|
172
|
+
projectNumber.toString(),
|
|
173
|
+
'--owner',
|
|
174
|
+
owner,
|
|
175
|
+
'--format',
|
|
176
|
+
'json',
|
|
177
|
+
]);
|
|
178
|
+
const data = JSON.parse(result);
|
|
179
|
+
// Cache the result
|
|
180
|
+
this.fieldsCache.set(cacheKey, data.fields);
|
|
181
|
+
return data.fields;
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
logger.error('Could not list project fields');
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Create a single-select field with options using GraphQL
|
|
190
|
+
*/
|
|
191
|
+
async createSingleSelectField(projectId, fieldName, options) {
|
|
192
|
+
logger.info(`Creating '${fieldName}' field...`);
|
|
193
|
+
const mutation = `
|
|
194
|
+
mutation($projectId: ID!, $name: String!, $dataType: ProjectV2CustomFieldType!, $options: [ProjectV2SingleSelectFieldOptionInput!]!) {
|
|
195
|
+
createProjectV2Field(
|
|
196
|
+
input: {
|
|
197
|
+
projectId: $projectId
|
|
198
|
+
dataType: $dataType
|
|
199
|
+
name: $name
|
|
200
|
+
singleSelectOptions: $options
|
|
201
|
+
}
|
|
202
|
+
) {
|
|
203
|
+
projectV2Field {
|
|
204
|
+
... on ProjectV2SingleSelectField {
|
|
205
|
+
id
|
|
206
|
+
name
|
|
207
|
+
options {
|
|
208
|
+
id
|
|
209
|
+
name
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
`;
|
|
216
|
+
try {
|
|
217
|
+
// Use runCommandWithStdin for complex GraphQL mutations with arrays
|
|
218
|
+
const variables = {
|
|
219
|
+
projectId,
|
|
220
|
+
name: fieldName,
|
|
221
|
+
dataType: 'SINGLE_SELECT',
|
|
222
|
+
options: options.map((opt) => ({
|
|
223
|
+
name: opt.name,
|
|
224
|
+
color: opt.color || 'GRAY',
|
|
225
|
+
description: opt.description || '',
|
|
226
|
+
})),
|
|
227
|
+
};
|
|
228
|
+
const payload = JSON.stringify({
|
|
229
|
+
query: mutation,
|
|
230
|
+
variables,
|
|
231
|
+
});
|
|
232
|
+
// Use runCommandWithStdin to send complex JSON payload
|
|
233
|
+
const { runCommandWithStdin } = await import('../../utils/shell.js');
|
|
234
|
+
const result = await runCommandWithStdin('gh api graphql --input -', payload);
|
|
235
|
+
const parsed = JSON.parse(result);
|
|
236
|
+
const fieldId = parsed.data.createProjectV2Field.projectV2Field.id;
|
|
237
|
+
logger.success(`Created '${fieldName}' field with ${options.length} options`);
|
|
238
|
+
// Clear cache to force refresh
|
|
239
|
+
this.fieldsCache.clear();
|
|
240
|
+
return fieldId;
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
logger.error(`Failed to create '${fieldName}' field`);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Ensure single-select field exists with given options
|
|
249
|
+
*/
|
|
250
|
+
async ensureSingleSelectField(owner, projectNumber, projectId, fieldName, options) {
|
|
251
|
+
logger.info(`Checking for '${fieldName}' field...`);
|
|
252
|
+
const fields = await this.listFields(owner, projectNumber);
|
|
253
|
+
const field = fields.find((f) => f.name === fieldName);
|
|
254
|
+
if (field) {
|
|
255
|
+
logger.success(`Found '${fieldName}' field`);
|
|
256
|
+
return field.id;
|
|
257
|
+
}
|
|
258
|
+
// Create the field with options
|
|
259
|
+
const fieldOptions = options.map((name) => ({ name }));
|
|
260
|
+
return this.createSingleSelectField(projectId, fieldName, fieldOptions);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Ensure tracking name field exists
|
|
264
|
+
*/
|
|
265
|
+
async ensureTrackingNameField(owner, projectNumber) {
|
|
266
|
+
logger.info("Checking for 'Tracking Name' field...");
|
|
267
|
+
const fields = await this.listFields(owner, projectNumber);
|
|
268
|
+
const trackingField = fields.find((f) => f.name === 'Tracking Name');
|
|
269
|
+
if (trackingField) {
|
|
270
|
+
logger.success("Found 'Tracking Name' field");
|
|
271
|
+
return trackingField.id;
|
|
272
|
+
}
|
|
273
|
+
// Create the field
|
|
274
|
+
logger.info("Creating 'Tracking Name' field...");
|
|
275
|
+
try {
|
|
276
|
+
const result = await runGh([
|
|
277
|
+
'project',
|
|
278
|
+
'field-create',
|
|
279
|
+
projectNumber.toString(),
|
|
280
|
+
'--owner',
|
|
281
|
+
owner,
|
|
282
|
+
'--name',
|
|
283
|
+
'Tracking Name',
|
|
284
|
+
'--data-type',
|
|
285
|
+
'TEXT',
|
|
286
|
+
'--format',
|
|
287
|
+
'json',
|
|
288
|
+
]);
|
|
289
|
+
const data = JSON.parse(result);
|
|
290
|
+
if (!data.id) {
|
|
291
|
+
throw new Error('Could not extract field ID from response');
|
|
292
|
+
}
|
|
293
|
+
logger.success("Created 'Tracking Name' field");
|
|
294
|
+
return data.id;
|
|
295
|
+
}
|
|
296
|
+
catch (error) {
|
|
297
|
+
logger.error('Failed to create field');
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Add issue to project
|
|
303
|
+
*/
|
|
304
|
+
async addIssueToProject(issueNumber, projectNumber, owner) {
|
|
305
|
+
const repo = await getRepoInfo();
|
|
306
|
+
const repoOwner = owner ?? repo.owner;
|
|
307
|
+
const issueUrl = `https://github.com/${repo.owner}/${repo.name}/issues/${issueNumber}`;
|
|
308
|
+
logger.info(`Adding issue #${issueNumber} to project...`);
|
|
309
|
+
try {
|
|
310
|
+
const result = await runGh([
|
|
311
|
+
'project',
|
|
312
|
+
'item-add',
|
|
313
|
+
projectNumber.toString(),
|
|
314
|
+
'--owner',
|
|
315
|
+
repoOwner,
|
|
316
|
+
'--url',
|
|
317
|
+
issueUrl,
|
|
318
|
+
'--format',
|
|
319
|
+
'json',
|
|
320
|
+
]);
|
|
321
|
+
const data = JSON.parse(result);
|
|
322
|
+
if (!data.id) {
|
|
323
|
+
throw new Error('Could not extract item ID from response');
|
|
324
|
+
}
|
|
325
|
+
logger.success('Added issue to project');
|
|
326
|
+
return data.id;
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
logger.warning('Could not add issue to project');
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Update text field in project item
|
|
335
|
+
*/
|
|
336
|
+
async updateTextField(projectId, itemId, fieldId, value) {
|
|
337
|
+
const mutation = `
|
|
338
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $value: String!) {
|
|
339
|
+
updateProjectV2ItemFieldValue(
|
|
340
|
+
input: {
|
|
341
|
+
projectId: $projectId
|
|
342
|
+
itemId: $itemId
|
|
343
|
+
fieldId: $fieldId
|
|
344
|
+
value: {
|
|
345
|
+
text: $value
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
) {
|
|
349
|
+
projectV2Item {
|
|
350
|
+
id
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
`;
|
|
355
|
+
await ghGraphQL(mutation, {
|
|
356
|
+
projectId,
|
|
357
|
+
itemId,
|
|
358
|
+
fieldId,
|
|
359
|
+
value,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Update single-select field in project item
|
|
364
|
+
*/
|
|
365
|
+
async updateSingleSelectField(projectId, itemId, fieldId, optionId) {
|
|
366
|
+
const mutation = `
|
|
367
|
+
mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) {
|
|
368
|
+
updateProjectV2ItemFieldValue(
|
|
369
|
+
input: {
|
|
370
|
+
projectId: $projectId
|
|
371
|
+
itemId: $itemId
|
|
372
|
+
fieldId: $fieldId
|
|
373
|
+
value: {
|
|
374
|
+
singleSelectOptionId: $optionId
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
) {
|
|
378
|
+
projectV2Item {
|
|
379
|
+
id
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
`;
|
|
384
|
+
await ghGraphQL(mutation, {
|
|
385
|
+
projectId,
|
|
386
|
+
itemId,
|
|
387
|
+
fieldId,
|
|
388
|
+
optionId,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Set tracking name for project item
|
|
393
|
+
*/
|
|
394
|
+
async setTrackingName(issueNumber, trackingName, projectNumber, projectId, owner) {
|
|
395
|
+
const repo = await getRepoInfo();
|
|
396
|
+
const repoOwner = owner ?? repo.owner;
|
|
397
|
+
// Get or create tracking name field
|
|
398
|
+
const fieldId = await this.ensureTrackingNameField(repoOwner, projectNumber);
|
|
399
|
+
// Add issue to project (or get existing item)
|
|
400
|
+
let itemId;
|
|
401
|
+
try {
|
|
402
|
+
itemId = await this.addIssueToProject(issueNumber, projectNumber, repoOwner);
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
logger.warning('Issue may already be in project, continuing...');
|
|
406
|
+
// If item already exists, we need to find it
|
|
407
|
+
// For now, rethrow the error
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
// Update the tracking name field
|
|
411
|
+
logger.info('Setting tracking name...');
|
|
412
|
+
await this.updateTextField(projectId, itemId, fieldId, trackingName);
|
|
413
|
+
logger.success('Tracking name set successfully');
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Remove issue from project
|
|
417
|
+
*/
|
|
418
|
+
async removeIssueFromProject(itemId, projectNumber, owner) {
|
|
419
|
+
const repo = await getRepoInfo();
|
|
420
|
+
const repoOwner = owner ?? repo.owner;
|
|
421
|
+
logger.info('Removing issue from project...');
|
|
422
|
+
try {
|
|
423
|
+
await runGh([
|
|
424
|
+
'project',
|
|
425
|
+
'item-delete',
|
|
426
|
+
projectNumber.toString(),
|
|
427
|
+
'--owner',
|
|
428
|
+
repoOwner,
|
|
429
|
+
'--id',
|
|
430
|
+
itemId,
|
|
431
|
+
]);
|
|
432
|
+
logger.success('Removed issue from project');
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
logger.warning('Could not remove issue from project');
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Get project info
|
|
441
|
+
*/
|
|
442
|
+
async getProjectInfo(owner, projectNumber) {
|
|
443
|
+
const query = `
|
|
444
|
+
query($owner: String!, $number: Int!) {
|
|
445
|
+
organization(login: $owner) {
|
|
446
|
+
projectV2(number: $number) {
|
|
447
|
+
id
|
|
448
|
+
title
|
|
449
|
+
url
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
`;
|
|
454
|
+
try {
|
|
455
|
+
const result = await ghGraphQL(query, { owner, number: projectNumber }, { suppressErrors: true });
|
|
456
|
+
return {
|
|
457
|
+
number: projectNumber,
|
|
458
|
+
id: result.data.organization.projectV2.id,
|
|
459
|
+
title: result.data.organization.projectV2.title,
|
|
460
|
+
url: result.data.organization.projectV2.url,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
// Try user query if organization fails
|
|
465
|
+
const userQuery = `
|
|
466
|
+
query($owner: String!, $number: Int!) {
|
|
467
|
+
user(login: $owner) {
|
|
468
|
+
projectV2(number: $number) {
|
|
469
|
+
id
|
|
470
|
+
title
|
|
471
|
+
url
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
`;
|
|
476
|
+
try {
|
|
477
|
+
const userResult = await ghGraphQL(userQuery, { owner, number: projectNumber }, { suppressErrors: true });
|
|
478
|
+
return {
|
|
479
|
+
number: projectNumber,
|
|
480
|
+
id: userResult.data.user.projectV2.id,
|
|
481
|
+
title: userResult.data.user.projectV2.title,
|
|
482
|
+
url: userResult.data.user.projectV2.url,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
catch (userError) {
|
|
486
|
+
throw new Error(`Could not find project #${projectNumber} for owner "${owner}"`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Find field by name
|
|
492
|
+
*/
|
|
493
|
+
async findFieldByName(owner, projectNumber, fieldName) {
|
|
494
|
+
const fields = await this.listFields(owner, projectNumber);
|
|
495
|
+
return fields.find((f) => f.name === fieldName) ?? null;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Get option ID for single-select field
|
|
499
|
+
*/
|
|
500
|
+
async getFieldOptionId(owner, projectNumber, fieldName, optionName) {
|
|
501
|
+
const field = await this.findFieldByName(owner, projectNumber, fieldName);
|
|
502
|
+
if (!field?.options) {
|
|
503
|
+
logger.debug(`[getFieldOptionId] Field ${fieldName} has no options`);
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
logger.debug(`[getFieldOptionId] Looking for option "${optionName}" in field "${fieldName}"`);
|
|
507
|
+
logger.debug(`[getFieldOptionId] Available options: ${field.options
|
|
508
|
+
.map((o) => `"${o.name}"`)
|
|
509
|
+
.join(', ')}`);
|
|
510
|
+
const option = field.options.find((o) => o.name === optionName);
|
|
511
|
+
if (option) {
|
|
512
|
+
logger.debug(`[getFieldOptionId] Found option "${optionName}" with id: ${option.id}`);
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
logger.debug(`[getFieldOptionId] Option "${optionName}" not found in field "${fieldName}"`);
|
|
516
|
+
}
|
|
517
|
+
return option?.id ?? null;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Update status field for an issue in project
|
|
521
|
+
*/
|
|
522
|
+
async updateIssueStatus(itemId, status, projectId, owner, projectNumber) {
|
|
523
|
+
const field = await this.findFieldByName(owner, projectNumber, 'trackingStatus');
|
|
524
|
+
if (!field) {
|
|
525
|
+
throw new Error('trackingStatus field not found in project');
|
|
526
|
+
}
|
|
527
|
+
const optionId = await this.getFieldOptionId(owner, projectNumber, 'trackingStatus', status);
|
|
528
|
+
if (!optionId) {
|
|
529
|
+
throw new Error(`Status option "${status}" not found in trackingStatus field`);
|
|
530
|
+
}
|
|
531
|
+
await this.updateSingleSelectField(projectId, itemId, field.id, optionId);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Update priority field for an issue in project
|
|
535
|
+
*/
|
|
536
|
+
async updateIssuePriority(itemId, priority, projectId, owner, projectNumber) {
|
|
537
|
+
const field = await this.findFieldByName(owner, projectNumber, 'priority');
|
|
538
|
+
if (!field) {
|
|
539
|
+
throw new Error('priority field not found in project');
|
|
540
|
+
}
|
|
541
|
+
const optionId = await this.getFieldOptionId(owner, projectNumber, 'priority', priority);
|
|
542
|
+
if (!optionId) {
|
|
543
|
+
throw new Error(`Priority option "${priority}" not found in priority field`);
|
|
544
|
+
}
|
|
545
|
+
await this.updateSingleSelectField(projectId, itemId, field.id, optionId);
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Set issue tracking status (OwnerIsTracking or AssigneeIsFollowing)
|
|
549
|
+
* Determines which field to update based on whether current user is the issue creator
|
|
550
|
+
*/
|
|
551
|
+
async setIssueTracking(itemId, isTracking, projectId, owner, projectNumber) {
|
|
552
|
+
logger.debug(`[setIssueTracking] Starting for itemId: ${itemId}, isTracking: ${isTracking}`);
|
|
553
|
+
// Get the issue to check creator
|
|
554
|
+
const { ghGraphQL: gql } = await import('../../utils/github-cli.js');
|
|
555
|
+
// Get current user
|
|
556
|
+
const userQuery = `
|
|
557
|
+
query {
|
|
558
|
+
viewer {
|
|
559
|
+
login
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
`;
|
|
563
|
+
const userResult = await gql(userQuery, {});
|
|
564
|
+
const currentUser = userResult.data.viewer.login;
|
|
565
|
+
logger.debug(`[setIssueTracking] Current user: ${currentUser}`);
|
|
566
|
+
// Get issue creator from the item
|
|
567
|
+
const itemQuery = `
|
|
568
|
+
query($itemId: ID!) {
|
|
569
|
+
node(id: $itemId) {
|
|
570
|
+
... on ProjectV2Item {
|
|
571
|
+
content {
|
|
572
|
+
... on Issue {
|
|
573
|
+
number
|
|
574
|
+
author {
|
|
575
|
+
login
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
`;
|
|
583
|
+
const itemResult = await gql(itemQuery, { itemId });
|
|
584
|
+
const issueCreator = itemResult.data.node.content.author.login;
|
|
585
|
+
const isOwner = issueCreator === currentUser;
|
|
586
|
+
logger.debug(`[setIssueTracking] Issue creator: ${issueCreator}, isOwner: ${isOwner}`);
|
|
587
|
+
// Determine which field to update
|
|
588
|
+
const fieldName = isOwner ? 'OwnerIsTracking' : 'AssigneeIsFollowing';
|
|
589
|
+
logger.debug(`[setIssueTracking] Will update field: ${fieldName}`);
|
|
590
|
+
const field = await this.findFieldByName(owner, projectNumber, fieldName);
|
|
591
|
+
if (!field) {
|
|
592
|
+
logger.error(`${fieldName} field not found in project`);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
logger.debug(`[setIssueTracking] Found field: ${fieldName}, id: ${field.id}`);
|
|
596
|
+
// Get option ID for "true" or "false"
|
|
597
|
+
const optionValue = isTracking ? 'true' : 'false';
|
|
598
|
+
const optionId = await this.getFieldOptionId(owner, projectNumber, fieldName, optionValue);
|
|
599
|
+
if (!optionId) {
|
|
600
|
+
logger.error(`Option "${optionValue}" not found in ${fieldName} field`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
logger.debug(`[setIssueTracking] Found option: ${optionValue}, id: ${optionId}`);
|
|
604
|
+
await this.updateSingleSelectField(projectId, itemId, field.id, optionId);
|
|
605
|
+
logger.info(`✓ Set ${fieldName} = ${optionValue} for item ${itemId}`);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Get all issues from project with their fields
|
|
609
|
+
*/
|
|
610
|
+
async getProjectIssues(owner, projectNumber) {
|
|
611
|
+
const query = `
|
|
612
|
+
query($owner: String!, $projectNumber: Int!) {
|
|
613
|
+
organization(login: $owner) {
|
|
614
|
+
projectV2(number: $projectNumber) {
|
|
615
|
+
items(first: 100) {
|
|
616
|
+
nodes {
|
|
617
|
+
id
|
|
618
|
+
content {
|
|
619
|
+
... on Issue {
|
|
620
|
+
number
|
|
621
|
+
title
|
|
622
|
+
state
|
|
623
|
+
timelineItems(first: 20, itemTypes: [CROSS_REFERENCED_EVENT]) {
|
|
624
|
+
nodes {
|
|
625
|
+
... on CrossReferencedEvent {
|
|
626
|
+
source {
|
|
627
|
+
... on PullRequest {
|
|
628
|
+
number
|
|
629
|
+
url
|
|
630
|
+
state
|
|
631
|
+
comments(last: 10) {
|
|
632
|
+
nodes {
|
|
633
|
+
author {
|
|
634
|
+
login
|
|
635
|
+
}
|
|
636
|
+
body
|
|
637
|
+
createdAt
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
fieldValues(first: 20) {
|
|
648
|
+
nodes {
|
|
649
|
+
... on ProjectV2ItemFieldTextValue {
|
|
650
|
+
text
|
|
651
|
+
field {
|
|
652
|
+
... on ProjectV2FieldCommon {
|
|
653
|
+
name
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
658
|
+
name
|
|
659
|
+
field {
|
|
660
|
+
... on ProjectV2FieldCommon {
|
|
661
|
+
name
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
`;
|
|
673
|
+
try {
|
|
674
|
+
logger.debug(`Fetching project issues for ${owner}/${projectNumber}`);
|
|
675
|
+
logger.debug(`Trying organization query first...`);
|
|
676
|
+
const result = await ghGraphQL(query, { owner, projectNumber }, { suppressErrors: true });
|
|
677
|
+
logger.debug(`Successfully fetched ${result.data.organization.projectV2.items.nodes.length} items from project`);
|
|
678
|
+
const issues = result.data.organization.projectV2.items.nodes
|
|
679
|
+
.filter((item) => item.content?.number)
|
|
680
|
+
.map((item) => {
|
|
681
|
+
const fields = item.fieldValues.nodes;
|
|
682
|
+
const trackingName = fields.find((f) => f.field?.name === 'Tracking Name')?.text ?? '';
|
|
683
|
+
const status = fields.find((f) => f.field?.name === 'trackingStatus')?.name ??
|
|
684
|
+
STATUS.UNASSIGNED;
|
|
685
|
+
const priority = fields.find((f) => f.field?.name === 'priority')?.name ?? 'Medium';
|
|
686
|
+
// Check for tracking fields - only 'true' means tracked, undefined = false
|
|
687
|
+
const ownerField = fields.find((f) => f.field?.name === 'OwnerIsTracking');
|
|
688
|
+
const assigneeField = fields.find((f) => f.field?.name === 'AssigneeIsFollowing');
|
|
689
|
+
// Only consider tracked if explicitly set to "true" (case-insensitive)
|
|
690
|
+
// undefined or any other value = false
|
|
691
|
+
const ownerIsTracking = ownerField?.name?.toLowerCase() === 'true';
|
|
692
|
+
const assigneeIsFollowing = assigneeField?.name?.toLowerCase() === 'true';
|
|
693
|
+
const githubState = item.content.state === 'OPEN' ? 'open' : 'closed';
|
|
694
|
+
// Find linked PR from timeline events and detect deployment status
|
|
695
|
+
let prNumber;
|
|
696
|
+
let prUrl;
|
|
697
|
+
let deploymentStatus;
|
|
698
|
+
let deploymentTimestamp;
|
|
699
|
+
if (item.content.timelineItems?.nodes) {
|
|
700
|
+
// Find first PR that references this issue
|
|
701
|
+
const prEvent = item.content.timelineItems.nodes.find((node) => node.source?.number && node.source?.url);
|
|
702
|
+
if (prEvent?.source) {
|
|
703
|
+
prNumber = prEvent.source.number;
|
|
704
|
+
prUrl = prEvent.source.url;
|
|
705
|
+
// Detect deployment status from PR comments
|
|
706
|
+
if (prEvent.source.comments?.nodes) {
|
|
707
|
+
const deploymentService = new DeploymentDetectionService();
|
|
708
|
+
const comments = prEvent.source.comments.nodes.map((c) => ({
|
|
709
|
+
author: c.author.login,
|
|
710
|
+
body: c.body,
|
|
711
|
+
createdAt: c.createdAt,
|
|
712
|
+
}));
|
|
713
|
+
const deploymentResult = deploymentService.detectDeploymentStatus(comments);
|
|
714
|
+
deploymentStatus = deploymentResult.status;
|
|
715
|
+
deploymentTimestamp = deploymentResult.timestamp;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
number: item.content.number,
|
|
721
|
+
title: item.content.title,
|
|
722
|
+
trackingName,
|
|
723
|
+
status,
|
|
724
|
+
priority,
|
|
725
|
+
itemId: item.id,
|
|
726
|
+
githubState,
|
|
727
|
+
ownerIsTracking,
|
|
728
|
+
assigneeIsFollowing,
|
|
729
|
+
prNumber,
|
|
730
|
+
prUrl,
|
|
731
|
+
deploymentStatus,
|
|
732
|
+
deploymentTimestamp,
|
|
733
|
+
};
|
|
734
|
+
});
|
|
735
|
+
return issues;
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
// Try user query if organization fails
|
|
739
|
+
logger.debug(`Organization query failed, trying user query...`);
|
|
740
|
+
logger.debug(`Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
741
|
+
const userQuery = query.replace('organization', 'user');
|
|
742
|
+
try {
|
|
743
|
+
const userResult = await ghGraphQL(userQuery, { owner, projectNumber }, { suppressErrors: true });
|
|
744
|
+
const issues = userResult.data.user.projectV2.items.nodes
|
|
745
|
+
.filter((item) => item.content?.number)
|
|
746
|
+
.map((item) => {
|
|
747
|
+
const fields = item.fieldValues.nodes;
|
|
748
|
+
const trackingName = fields.find((f) => f.field?.name === 'Tracking Name')?.text ?? '';
|
|
749
|
+
const status = fields.find((f) => f.field?.name === 'trackingStatus')?.name ??
|
|
750
|
+
STATUS.UNASSIGNED;
|
|
751
|
+
const priority = fields.find((f) => f.field?.name === 'priority')?.name ??
|
|
752
|
+
'Medium';
|
|
753
|
+
// Check for tracking fields - only 'true' means tracked, undefined = false
|
|
754
|
+
const ownerField = fields.find((f) => f.field?.name === 'OwnerIsTracking');
|
|
755
|
+
const assigneeField = fields.find((f) => f.field?.name === 'AssigneeIsFollowing');
|
|
756
|
+
// Only consider tracked if explicitly set to "true" (case-insensitive)
|
|
757
|
+
const ownerIsTracking = ownerField?.name?.toLowerCase() === 'true';
|
|
758
|
+
const assigneeIsFollowing = assigneeField?.name?.toLowerCase() === 'true';
|
|
759
|
+
const githubState = item.content.state === 'OPEN' ? 'open' : 'closed';
|
|
760
|
+
// Find linked PR from timeline events
|
|
761
|
+
let prNumber;
|
|
762
|
+
let prUrl;
|
|
763
|
+
if (item.content.timelineItems?.nodes) {
|
|
764
|
+
// Find first PR that references this issue
|
|
765
|
+
const prEvent = item.content.timelineItems.nodes.find((node) => node.source?.number && node.source?.url);
|
|
766
|
+
if (prEvent?.source) {
|
|
767
|
+
prNumber = prEvent.source.number;
|
|
768
|
+
prUrl = prEvent.source.url;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return {
|
|
772
|
+
number: item.content.number,
|
|
773
|
+
title: item.content.title,
|
|
774
|
+
trackingName,
|
|
775
|
+
status,
|
|
776
|
+
priority,
|
|
777
|
+
itemId: item.id,
|
|
778
|
+
githubState,
|
|
779
|
+
ownerIsTracking,
|
|
780
|
+
assigneeIsFollowing,
|
|
781
|
+
prNumber,
|
|
782
|
+
prUrl,
|
|
783
|
+
};
|
|
784
|
+
});
|
|
785
|
+
return issues;
|
|
786
|
+
}
|
|
787
|
+
catch (userError) {
|
|
788
|
+
logger.debug(`Both organization and user queries failed for project #${projectNumber}`);
|
|
789
|
+
logger.debug(`Original error: ${error instanceof Error ? error.message : String(error)}`);
|
|
790
|
+
logger.debug(`User error: ${userError instanceof Error ? userError.message : String(userError)}`);
|
|
791
|
+
throw new Error(`Could not access project #${projectNumber} for owner "${owner}". Ensure the owner name is correct and you have access.`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Find project item by issue number
|
|
797
|
+
*/
|
|
798
|
+
async findProjectItem(owner, projectNumber, issueNumber) {
|
|
799
|
+
const issues = await this.getProjectIssues(owner, projectNumber);
|
|
800
|
+
const issue = issues.find((i) => i.number === issueNumber);
|
|
801
|
+
return issue?.itemId ?? null;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
//# sourceMappingURL=ProjectService.js.map
|