claude-autopm 2.7.0 → 2.8.2
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/README.md +307 -56
- package/autopm/.claude/.env +158 -0
- package/autopm/.claude/settings.local.json +9 -0
- package/bin/autopm.js +11 -2
- package/bin/commands/epic.js +23 -3
- package/bin/commands/plugin.js +395 -0
- package/bin/commands/team.js +184 -10
- package/install/install.js +223 -4
- package/lib/cli/commands/issue.js +360 -20
- package/lib/plugins/PluginManager.js +1328 -0
- package/lib/plugins/PluginManager.old.js +400 -0
- package/lib/providers/AzureDevOpsProvider.js +575 -0
- package/lib/providers/GitHubProvider.js +475 -0
- package/lib/services/EpicService.js +1092 -3
- package/lib/services/IssueService.js +991 -0
- package/package.json +9 -1
- package/scripts/publish-plugins.sh +166 -0
- package/autopm/.claude/agents/cloud/README.md +0 -55
- package/autopm/.claude/agents/cloud/aws-cloud-architect.md +0 -521
- package/autopm/.claude/agents/cloud/azure-cloud-architect.md +0 -436
- package/autopm/.claude/agents/cloud/gcp-cloud-architect.md +0 -385
- package/autopm/.claude/agents/cloud/gcp-cloud-functions-engineer.md +0 -306
- package/autopm/.claude/agents/cloud/gemini-api-expert.md +0 -880
- package/autopm/.claude/agents/cloud/kubernetes-orchestrator.md +0 -566
- package/autopm/.claude/agents/cloud/openai-python-expert.md +0 -1087
- package/autopm/.claude/agents/cloud/terraform-infrastructure-expert.md +0 -454
- package/autopm/.claude/agents/core/agent-manager.md +0 -296
- package/autopm/.claude/agents/core/code-analyzer.md +0 -131
- package/autopm/.claude/agents/core/file-analyzer.md +0 -162
- package/autopm/.claude/agents/core/test-runner.md +0 -200
- package/autopm/.claude/agents/data/airflow-orchestration-expert.md +0 -52
- package/autopm/.claude/agents/data/kedro-pipeline-expert.md +0 -50
- package/autopm/.claude/agents/data/langgraph-workflow-expert.md +0 -520
- package/autopm/.claude/agents/databases/README.md +0 -50
- package/autopm/.claude/agents/databases/bigquery-expert.md +0 -392
- package/autopm/.claude/agents/databases/cosmosdb-expert.md +0 -368
- package/autopm/.claude/agents/databases/mongodb-expert.md +0 -398
- package/autopm/.claude/agents/databases/postgresql-expert.md +0 -321
- package/autopm/.claude/agents/databases/redis-expert.md +0 -52
- package/autopm/.claude/agents/devops/README.md +0 -52
- package/autopm/.claude/agents/devops/azure-devops-specialist.md +0 -308
- package/autopm/.claude/agents/devops/docker-containerization-expert.md +0 -298
- package/autopm/.claude/agents/devops/github-operations-specialist.md +0 -335
- package/autopm/.claude/agents/devops/mcp-context-manager.md +0 -319
- package/autopm/.claude/agents/devops/observability-engineer.md +0 -574
- package/autopm/.claude/agents/devops/ssh-operations-expert.md +0 -1093
- package/autopm/.claude/agents/devops/traefik-proxy-expert.md +0 -444
- package/autopm/.claude/agents/frameworks/README.md +0 -64
- package/autopm/.claude/agents/frameworks/e2e-test-engineer.md +0 -360
- package/autopm/.claude/agents/frameworks/nats-messaging-expert.md +0 -254
- package/autopm/.claude/agents/frameworks/react-frontend-engineer.md +0 -217
- package/autopm/.claude/agents/frameworks/react-ui-expert.md +0 -226
- package/autopm/.claude/agents/frameworks/tailwindcss-expert.md +0 -770
- package/autopm/.claude/agents/frameworks/ux-design-expert.md +0 -244
- package/autopm/.claude/agents/integration/message-queue-engineer.md +0 -794
- package/autopm/.claude/agents/languages/README.md +0 -50
- package/autopm/.claude/agents/languages/bash-scripting-expert.md +0 -541
- package/autopm/.claude/agents/languages/javascript-frontend-engineer.md +0 -197
- package/autopm/.claude/agents/languages/nodejs-backend-engineer.md +0 -226
- package/autopm/.claude/agents/languages/python-backend-engineer.md +0 -214
- package/autopm/.claude/agents/languages/python-backend-expert.md +0 -289
- package/autopm/.claude/agents/testing/frontend-testing-engineer.md +0 -395
- package/autopm/.claude/commands/ai/langgraph-workflow.md +0 -65
- package/autopm/.claude/commands/ai/openai-chat.md +0 -65
- package/autopm/.claude/commands/azure/COMMANDS.md +0 -107
- package/autopm/.claude/commands/azure/COMMAND_MAPPING.md +0 -252
- package/autopm/.claude/commands/azure/INTEGRATION_FIX.md +0 -103
- package/autopm/.claude/commands/azure/README.md +0 -246
- package/autopm/.claude/commands/azure/active-work.md +0 -198
- package/autopm/.claude/commands/azure/aliases.md +0 -143
- package/autopm/.claude/commands/azure/blocked-items.md +0 -287
- package/autopm/.claude/commands/azure/clean.md +0 -93
- package/autopm/.claude/commands/azure/docs-query.md +0 -48
- package/autopm/.claude/commands/azure/feature-decompose.md +0 -380
- package/autopm/.claude/commands/azure/feature-list.md +0 -61
- package/autopm/.claude/commands/azure/feature-new.md +0 -115
- package/autopm/.claude/commands/azure/feature-show.md +0 -205
- package/autopm/.claude/commands/azure/feature-start.md +0 -130
- package/autopm/.claude/commands/azure/fix-integration-example.md +0 -93
- package/autopm/.claude/commands/azure/help.md +0 -150
- package/autopm/.claude/commands/azure/import-us.md +0 -269
- package/autopm/.claude/commands/azure/init.md +0 -211
- package/autopm/.claude/commands/azure/next-task.md +0 -262
- package/autopm/.claude/commands/azure/search.md +0 -160
- package/autopm/.claude/commands/azure/sprint-status.md +0 -235
- package/autopm/.claude/commands/azure/standup.md +0 -260
- package/autopm/.claude/commands/azure/sync-all.md +0 -99
- package/autopm/.claude/commands/azure/task-analyze.md +0 -186
- package/autopm/.claude/commands/azure/task-close.md +0 -329
- package/autopm/.claude/commands/azure/task-edit.md +0 -145
- package/autopm/.claude/commands/azure/task-list.md +0 -263
- package/autopm/.claude/commands/azure/task-new.md +0 -84
- package/autopm/.claude/commands/azure/task-reopen.md +0 -79
- package/autopm/.claude/commands/azure/task-show.md +0 -126
- package/autopm/.claude/commands/azure/task-start.md +0 -301
- package/autopm/.claude/commands/azure/task-status.md +0 -65
- package/autopm/.claude/commands/azure/task-sync.md +0 -67
- package/autopm/.claude/commands/azure/us-edit.md +0 -164
- package/autopm/.claude/commands/azure/us-list.md +0 -202
- package/autopm/.claude/commands/azure/us-new.md +0 -265
- package/autopm/.claude/commands/azure/us-parse.md +0 -253
- package/autopm/.claude/commands/azure/us-show.md +0 -188
- package/autopm/.claude/commands/azure/us-status.md +0 -320
- package/autopm/.claude/commands/azure/validate.md +0 -86
- package/autopm/.claude/commands/azure/work-item-sync.md +0 -47
- package/autopm/.claude/commands/cloud/infra-deploy.md +0 -38
- package/autopm/.claude/commands/github/workflow-create.md +0 -42
- package/autopm/.claude/commands/infrastructure/ssh-security.md +0 -65
- package/autopm/.claude/commands/infrastructure/traefik-setup.md +0 -65
- package/autopm/.claude/commands/kubernetes/deploy.md +0 -37
- package/autopm/.claude/commands/playwright/test-scaffold.md +0 -38
- package/autopm/.claude/commands/pm/blocked.md +0 -28
- package/autopm/.claude/commands/pm/clean.md +0 -119
- package/autopm/.claude/commands/pm/context-create.md +0 -136
- package/autopm/.claude/commands/pm/context-prime.md +0 -170
- package/autopm/.claude/commands/pm/context-update.md +0 -292
- package/autopm/.claude/commands/pm/context.md +0 -28
- package/autopm/.claude/commands/pm/epic-close.md +0 -86
- package/autopm/.claude/commands/pm/epic-decompose.md +0 -370
- package/autopm/.claude/commands/pm/epic-edit.md +0 -83
- package/autopm/.claude/commands/pm/epic-list.md +0 -30
- package/autopm/.claude/commands/pm/epic-merge.md +0 -222
- package/autopm/.claude/commands/pm/epic-oneshot.md +0 -119
- package/autopm/.claude/commands/pm/epic-refresh.md +0 -119
- package/autopm/.claude/commands/pm/epic-show.md +0 -28
- package/autopm/.claude/commands/pm/epic-split.md +0 -120
- package/autopm/.claude/commands/pm/epic-start.md +0 -195
- package/autopm/.claude/commands/pm/epic-status.md +0 -28
- package/autopm/.claude/commands/pm/epic-sync-modular.md +0 -338
- package/autopm/.claude/commands/pm/epic-sync-original.md +0 -473
- package/autopm/.claude/commands/pm/epic-sync.md +0 -486
- package/autopm/.claude/commands/pm/help.md +0 -28
- package/autopm/.claude/commands/pm/import.md +0 -115
- package/autopm/.claude/commands/pm/in-progress.md +0 -28
- package/autopm/.claude/commands/pm/init.md +0 -28
- package/autopm/.claude/commands/pm/issue-analyze.md +0 -202
- package/autopm/.claude/commands/pm/issue-close.md +0 -119
- package/autopm/.claude/commands/pm/issue-edit.md +0 -93
- package/autopm/.claude/commands/pm/issue-reopen.md +0 -87
- package/autopm/.claude/commands/pm/issue-show.md +0 -41
- package/autopm/.claude/commands/pm/issue-start.md +0 -234
- package/autopm/.claude/commands/pm/issue-status.md +0 -95
- package/autopm/.claude/commands/pm/issue-sync.md +0 -411
- package/autopm/.claude/commands/pm/next.md +0 -28
- package/autopm/.claude/commands/pm/prd-edit.md +0 -82
- package/autopm/.claude/commands/pm/prd-list.md +0 -28
- package/autopm/.claude/commands/pm/prd-new.md +0 -55
- package/autopm/.claude/commands/pm/prd-parse.md +0 -42
- package/autopm/.claude/commands/pm/prd-status.md +0 -28
- package/autopm/.claude/commands/pm/search.md +0 -28
- package/autopm/.claude/commands/pm/standup.md +0 -28
- package/autopm/.claude/commands/pm/status.md +0 -28
- package/autopm/.claude/commands/pm/sync.md +0 -99
- package/autopm/.claude/commands/pm/test-reference-update.md +0 -151
- package/autopm/.claude/commands/pm/validate.md +0 -28
- package/autopm/.claude/commands/pm/what-next.md +0 -28
- package/autopm/.claude/commands/python/api-scaffold.md +0 -50
- package/autopm/.claude/commands/python/docs-query.md +0 -48
- package/autopm/.claude/commands/react/app-scaffold.md +0 -50
- package/autopm/.claude/commands/testing/prime.md +0 -314
- package/autopm/.claude/commands/testing/run.md +0 -125
- package/autopm/.claude/commands/ui/bootstrap-scaffold.md +0 -65
- package/autopm/.claude/commands/ui/tailwind-system.md +0 -64
- package/autopm/.claude/rules/ai-integration-patterns.md +0 -219
- package/autopm/.claude/rules/ci-cd-kubernetes-strategy.md +0 -25
- package/autopm/.claude/rules/database-management-strategy.md +0 -17
- package/autopm/.claude/rules/database-pipeline.md +0 -94
- package/autopm/.claude/rules/devops-troubleshooting-playbook.md +0 -450
- package/autopm/.claude/rules/docker-first-development.md +0 -404
- package/autopm/.claude/rules/infrastructure-pipeline.md +0 -128
- package/autopm/.claude/rules/performance-guidelines.md +0 -403
- package/autopm/.claude/rules/ui-development-standards.md +0 -281
- package/autopm/.claude/rules/ui-framework-rules.md +0 -151
- package/autopm/.claude/rules/ux-design-rules.md +0 -209
- package/autopm/.claude/rules/visual-testing.md +0 -223
- package/autopm/.claude/scripts/azure/README.md +0 -192
- package/autopm/.claude/scripts/azure/active-work.js +0 -524
- package/autopm/.claude/scripts/azure/active-work.sh +0 -20
- package/autopm/.claude/scripts/azure/blocked.js +0 -520
- package/autopm/.claude/scripts/azure/blocked.sh +0 -20
- package/autopm/.claude/scripts/azure/daily.js +0 -533
- package/autopm/.claude/scripts/azure/daily.sh +0 -20
- package/autopm/.claude/scripts/azure/dashboard.js +0 -970
- package/autopm/.claude/scripts/azure/dashboard.sh +0 -20
- package/autopm/.claude/scripts/azure/feature-list.js +0 -254
- package/autopm/.claude/scripts/azure/feature-list.sh +0 -20
- package/autopm/.claude/scripts/azure/feature-show.js +0 -7
- package/autopm/.claude/scripts/azure/feature-show.sh +0 -20
- package/autopm/.claude/scripts/azure/feature-status.js +0 -604
- package/autopm/.claude/scripts/azure/feature-status.sh +0 -20
- package/autopm/.claude/scripts/azure/help.js +0 -342
- package/autopm/.claude/scripts/azure/help.sh +0 -20
- package/autopm/.claude/scripts/azure/next-task.js +0 -508
- package/autopm/.claude/scripts/azure/next-task.sh +0 -20
- package/autopm/.claude/scripts/azure/search.js +0 -469
- package/autopm/.claude/scripts/azure/search.sh +0 -20
- package/autopm/.claude/scripts/azure/setup.js +0 -745
- package/autopm/.claude/scripts/azure/setup.sh +0 -20
- package/autopm/.claude/scripts/azure/sprint-report.js +0 -1012
- package/autopm/.claude/scripts/azure/sprint-report.sh +0 -20
- package/autopm/.claude/scripts/azure/sync.js +0 -563
- package/autopm/.claude/scripts/azure/sync.sh +0 -20
- package/autopm/.claude/scripts/azure/us-list.js +0 -210
- package/autopm/.claude/scripts/azure/us-list.sh +0 -20
- package/autopm/.claude/scripts/azure/us-status.js +0 -238
- package/autopm/.claude/scripts/azure/us-status.sh +0 -20
- package/autopm/.claude/scripts/azure/validate.js +0 -626
- package/autopm/.claude/scripts/azure/validate.sh +0 -20
- package/autopm/.claude/scripts/azure/wrapper-template.sh +0 -20
- package/autopm/.claude/scripts/github/dependency-tracker.js +0 -554
- package/autopm/.claude/scripts/github/dependency-validator.js +0 -545
- package/autopm/.claude/scripts/github/dependency-visualizer.js +0 -477
- package/autopm/.claude/scripts/pm/analytics.js +0 -425
- package/autopm/.claude/scripts/pm/blocked.js +0 -164
- package/autopm/.claude/scripts/pm/blocked.sh +0 -78
- package/autopm/.claude/scripts/pm/clean.js +0 -464
- package/autopm/.claude/scripts/pm/context-create.js +0 -216
- package/autopm/.claude/scripts/pm/context-prime.js +0 -335
- package/autopm/.claude/scripts/pm/context-update.js +0 -344
- package/autopm/.claude/scripts/pm/context.js +0 -338
- package/autopm/.claude/scripts/pm/epic-close.js +0 -347
- package/autopm/.claude/scripts/pm/epic-edit.js +0 -382
- package/autopm/.claude/scripts/pm/epic-list.js +0 -273
- package/autopm/.claude/scripts/pm/epic-list.sh +0 -109
- package/autopm/.claude/scripts/pm/epic-show.js +0 -291
- package/autopm/.claude/scripts/pm/epic-show.sh +0 -105
- package/autopm/.claude/scripts/pm/epic-split.js +0 -522
- package/autopm/.claude/scripts/pm/epic-start/epic-start.js +0 -183
- package/autopm/.claude/scripts/pm/epic-start/epic-start.sh +0 -94
- package/autopm/.claude/scripts/pm/epic-status.js +0 -291
- package/autopm/.claude/scripts/pm/epic-status.sh +0 -104
- package/autopm/.claude/scripts/pm/epic-sync/README.md +0 -208
- package/autopm/.claude/scripts/pm/epic-sync/create-epic-issue.sh +0 -77
- package/autopm/.claude/scripts/pm/epic-sync/create-task-issues.sh +0 -86
- package/autopm/.claude/scripts/pm/epic-sync/update-epic-file.sh +0 -79
- package/autopm/.claude/scripts/pm/epic-sync/update-references.sh +0 -89
- package/autopm/.claude/scripts/pm/epic-sync.sh +0 -137
- package/autopm/.claude/scripts/pm/help.js +0 -92
- package/autopm/.claude/scripts/pm/help.sh +0 -90
- package/autopm/.claude/scripts/pm/in-progress.js +0 -178
- package/autopm/.claude/scripts/pm/in-progress.sh +0 -93
- package/autopm/.claude/scripts/pm/init.js +0 -321
- package/autopm/.claude/scripts/pm/init.sh +0 -178
- package/autopm/.claude/scripts/pm/issue-close.js +0 -232
- package/autopm/.claude/scripts/pm/issue-edit.js +0 -310
- package/autopm/.claude/scripts/pm/issue-show.js +0 -272
- package/autopm/.claude/scripts/pm/issue-start.js +0 -181
- package/autopm/.claude/scripts/pm/issue-sync/format-comment.sh +0 -468
- package/autopm/.claude/scripts/pm/issue-sync/gather-updates.sh +0 -460
- package/autopm/.claude/scripts/pm/issue-sync/post-comment.sh +0 -330
- package/autopm/.claude/scripts/pm/issue-sync/preflight-validation.sh +0 -348
- package/autopm/.claude/scripts/pm/issue-sync/update-frontmatter.sh +0 -387
- package/autopm/.claude/scripts/pm/lib/README.md +0 -85
- package/autopm/.claude/scripts/pm/lib/epic-discovery.js +0 -119
- package/autopm/.claude/scripts/pm/lib/logger.js +0 -78
- package/autopm/.claude/scripts/pm/next.js +0 -189
- package/autopm/.claude/scripts/pm/next.sh +0 -72
- package/autopm/.claude/scripts/pm/optimize.js +0 -407
- package/autopm/.claude/scripts/pm/pr-create.js +0 -337
- package/autopm/.claude/scripts/pm/pr-list.js +0 -257
- package/autopm/.claude/scripts/pm/prd-list.js +0 -242
- package/autopm/.claude/scripts/pm/prd-list.sh +0 -103
- package/autopm/.claude/scripts/pm/prd-new.js +0 -684
- package/autopm/.claude/scripts/pm/prd-parse.js +0 -547
- package/autopm/.claude/scripts/pm/prd-status.js +0 -152
- package/autopm/.claude/scripts/pm/prd-status.sh +0 -63
- package/autopm/.claude/scripts/pm/release.js +0 -460
- package/autopm/.claude/scripts/pm/search.js +0 -192
- package/autopm/.claude/scripts/pm/search.sh +0 -89
- package/autopm/.claude/scripts/pm/standup.js +0 -362
- package/autopm/.claude/scripts/pm/standup.sh +0 -95
- package/autopm/.claude/scripts/pm/status.js +0 -148
- package/autopm/.claude/scripts/pm/status.sh +0 -59
- package/autopm/.claude/scripts/pm/sync-batch.js +0 -337
- package/autopm/.claude/scripts/pm/sync.js +0 -343
- package/autopm/.claude/scripts/pm/template-list.js +0 -141
- package/autopm/.claude/scripts/pm/template-new.js +0 -366
- package/autopm/.claude/scripts/pm/validate.js +0 -274
- package/autopm/.claude/scripts/pm/validate.sh +0 -106
- package/autopm/.claude/scripts/pm/what-next.js +0 -660
- package/bin/node/azure-feature-show.js +0 -7
|
@@ -31,12 +31,24 @@
|
|
|
31
31
|
* - getIssuePath: Get file path for issue
|
|
32
32
|
* - formatIssueDuration: Format time duration
|
|
33
33
|
*
|
|
34
|
+
* 6. GitHub Sync Methods (8 methods):
|
|
35
|
+
* - syncToGitHub: Enhanced push with conflict detection
|
|
36
|
+
* - syncFromGitHub: Enhanced pull with merge
|
|
37
|
+
* - syncBidirectional: Full bidirectional sync
|
|
38
|
+
* - createGitHubIssue: Create new issue on GitHub
|
|
39
|
+
* - updateGitHubIssue: Update existing GitHub issue
|
|
40
|
+
* - detectConflict: Detect sync conflicts
|
|
41
|
+
* - resolveConflict: Resolve sync conflict with strategy
|
|
42
|
+
* - getSyncStatus: Get sync status for issue
|
|
43
|
+
*
|
|
34
44
|
* Documentation Queries:
|
|
35
45
|
* - GitHub Issues API v3 best practices (2025)
|
|
36
46
|
* - Azure DevOps work items REST API patterns
|
|
37
47
|
* - Agile issue tracking workflow best practices
|
|
38
48
|
* - mcp://context7/project-management/issue-tracking - Issue lifecycle management
|
|
39
49
|
* - mcp://context7/markdown/frontmatter - YAML frontmatter patterns
|
|
50
|
+
* - mcp://context7/conflict-resolution/sync - Conflict resolution strategies
|
|
51
|
+
* - mcp://context7/github/sync-patterns - GitHub synchronization patterns
|
|
40
52
|
*/
|
|
41
53
|
|
|
42
54
|
class IssueService {
|
|
@@ -586,6 +598,985 @@ ${issueData.description || ''}
|
|
|
586
598
|
return `${minutes} minute${minutes > 1 ? 's' : ''}`;
|
|
587
599
|
}
|
|
588
600
|
}
|
|
601
|
+
|
|
602
|
+
// ==========================================
|
|
603
|
+
// 6. GITHUB SYNC METHODS (8 NEW METHODS)
|
|
604
|
+
// ==========================================
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Sync local issue to GitHub (enhanced push with conflict detection)
|
|
608
|
+
*
|
|
609
|
+
* @param {number|string} issueNumber - Local issue number
|
|
610
|
+
* @param {Object} [options={}] - Sync options
|
|
611
|
+
* @param {boolean} [options.detectConflicts=false] - Enable conflict detection
|
|
612
|
+
* @returns {Promise<Object>} Result: { success, issueNumber, githubNumber, action, conflict? }
|
|
613
|
+
*/
|
|
614
|
+
async syncToGitHub(issueNumber, options = {}) {
|
|
615
|
+
const syncMap = await this._loadSyncMap();
|
|
616
|
+
const localIssue = await this.getLocalIssue(issueNumber);
|
|
617
|
+
const githubNumber = syncMap['local-to-github'][String(issueNumber)];
|
|
618
|
+
|
|
619
|
+
let result;
|
|
620
|
+
let action;
|
|
621
|
+
|
|
622
|
+
if (githubNumber) {
|
|
623
|
+
// Check for conflicts if enabled
|
|
624
|
+
if (options.detectConflicts) {
|
|
625
|
+
const githubIssue = await this.provider.getIssue(githubNumber);
|
|
626
|
+
const conflict = this.detectConflict(localIssue, githubIssue);
|
|
627
|
+
|
|
628
|
+
if (conflict.hasConflict && conflict.remoteNewer) {
|
|
629
|
+
return {
|
|
630
|
+
success: false,
|
|
631
|
+
issueNumber: String(issueNumber),
|
|
632
|
+
githubNumber: String(githubNumber),
|
|
633
|
+
conflict
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Update existing GitHub issue
|
|
639
|
+
result = await this.updateGitHubIssue(githubNumber, localIssue);
|
|
640
|
+
action = 'updated';
|
|
641
|
+
} else {
|
|
642
|
+
// Create new GitHub issue
|
|
643
|
+
result = await this.createGitHubIssue(localIssue);
|
|
644
|
+
action = 'created';
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Update sync-map
|
|
648
|
+
await this._updateSyncMap(String(issueNumber), String(result.number));
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
success: true,
|
|
652
|
+
issueNumber: String(issueNumber),
|
|
653
|
+
githubNumber: String(result.number),
|
|
654
|
+
action
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Sync GitHub issue to local (enhanced pull with merge)
|
|
660
|
+
*
|
|
661
|
+
* @param {number|string} githubNumber - GitHub issue number
|
|
662
|
+
* @param {Object} [options={}] - Sync options
|
|
663
|
+
* @param {boolean} [options.detectConflicts=false] - Enable conflict detection
|
|
664
|
+
* @param {string} [options.conflictStrategy='newest'] - Conflict resolution strategy
|
|
665
|
+
* @returns {Promise<Object>} Result: { success, localNumber, githubNumber, action, conflict? }
|
|
666
|
+
*/
|
|
667
|
+
async syncFromGitHub(githubNumber, options = {}) {
|
|
668
|
+
const fs = require('fs-extra');
|
|
669
|
+
|
|
670
|
+
const githubIssue = await this.provider.getIssue(githubNumber);
|
|
671
|
+
const syncMap = await this._loadSyncMap();
|
|
672
|
+
const localNumber = syncMap['github-to-local'][String(githubNumber)];
|
|
673
|
+
|
|
674
|
+
let action;
|
|
675
|
+
let finalLocalNumber = localNumber;
|
|
676
|
+
|
|
677
|
+
if (localNumber) {
|
|
678
|
+
// Check for conflicts if enabled
|
|
679
|
+
if (options.detectConflicts) {
|
|
680
|
+
const localIssue = await this.getLocalIssue(localNumber);
|
|
681
|
+
const conflict = this.detectConflict(localIssue, githubIssue);
|
|
682
|
+
|
|
683
|
+
if (conflict.hasConflict && conflict.localNewer) {
|
|
684
|
+
return {
|
|
685
|
+
success: false,
|
|
686
|
+
localNumber: String(localNumber),
|
|
687
|
+
githubNumber: String(githubNumber),
|
|
688
|
+
conflict
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Update existing local issue
|
|
694
|
+
action = 'updated';
|
|
695
|
+
} else {
|
|
696
|
+
// Create new local issue - find next available number
|
|
697
|
+
const issues = await this.listIssues();
|
|
698
|
+
const maxNumber = issues.reduce((max, issue) => {
|
|
699
|
+
const num = parseInt(issue.id || '0');
|
|
700
|
+
return num > max ? num : max;
|
|
701
|
+
}, 0);
|
|
702
|
+
finalLocalNumber = String(maxNumber + 1);
|
|
703
|
+
action = 'created';
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Build issue content with frontmatter
|
|
707
|
+
const labels = githubIssue.labels ? githubIssue.labels.map(l => l.name || l).join(', ') : '';
|
|
708
|
+
const assignee = githubIssue.assignees && githubIssue.assignees.length > 0
|
|
709
|
+
? githubIssue.assignees[0].login || githubIssue.assignees[0]
|
|
710
|
+
: '';
|
|
711
|
+
|
|
712
|
+
const frontmatter = `---
|
|
713
|
+
id: ${finalLocalNumber}
|
|
714
|
+
title: ${githubIssue.title}
|
|
715
|
+
status: ${githubIssue.state === 'closed' ? 'closed' : 'open'}
|
|
716
|
+
${assignee ? `assignee: ${assignee}` : ''}
|
|
717
|
+
${labels ? `labels: ${labels}` : ''}
|
|
718
|
+
created: ${githubIssue.created_at}
|
|
719
|
+
updated: ${githubIssue.updated_at}
|
|
720
|
+
github_number: ${githubNumber}
|
|
721
|
+
---
|
|
722
|
+
|
|
723
|
+
# ${githubIssue.title}
|
|
724
|
+
|
|
725
|
+
${githubIssue.body || ''}
|
|
726
|
+
`;
|
|
727
|
+
|
|
728
|
+
// Write to local file
|
|
729
|
+
const issuePath = this.getIssuePath(finalLocalNumber);
|
|
730
|
+
await fs.writeFile(issuePath, frontmatter);
|
|
731
|
+
|
|
732
|
+
// Update sync-map
|
|
733
|
+
await this._updateSyncMap(String(finalLocalNumber), String(githubNumber));
|
|
734
|
+
|
|
735
|
+
return {
|
|
736
|
+
success: true,
|
|
737
|
+
localNumber: String(finalLocalNumber),
|
|
738
|
+
githubNumber: String(githubNumber),
|
|
739
|
+
action
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Bidirectional sync - sync in the direction of newer changes
|
|
745
|
+
*
|
|
746
|
+
* @param {number|string} issueNumber - Local issue number
|
|
747
|
+
* @param {Object} [options={}] - Sync options
|
|
748
|
+
* @param {string} [options.conflictStrategy='detect'] - How to handle conflicts
|
|
749
|
+
* @returns {Promise<Object>} Result: { success, direction, conflict? }
|
|
750
|
+
*/
|
|
751
|
+
async syncBidirectional(issueNumber, options = {}) {
|
|
752
|
+
const syncMap = await this._loadSyncMap();
|
|
753
|
+
const githubNumber = syncMap['local-to-github'][String(issueNumber)];
|
|
754
|
+
|
|
755
|
+
if (!githubNumber) {
|
|
756
|
+
// No GitHub mapping, push to GitHub
|
|
757
|
+
const result = await this.syncToGitHub(issueNumber);
|
|
758
|
+
return { ...result, direction: 'to-github' };
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const localIssue = await this.getLocalIssue(issueNumber);
|
|
762
|
+
const githubIssue = await this.provider.getIssue(githubNumber);
|
|
763
|
+
|
|
764
|
+
const conflict = this.detectConflict(localIssue, githubIssue);
|
|
765
|
+
|
|
766
|
+
if (conflict.hasConflict) {
|
|
767
|
+
if (options.conflictStrategy === 'detect') {
|
|
768
|
+
return {
|
|
769
|
+
success: false,
|
|
770
|
+
direction: 'conflict',
|
|
771
|
+
conflict
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Auto-resolve based on timestamps
|
|
776
|
+
if (conflict.localNewer) {
|
|
777
|
+
const result = await this.syncToGitHub(issueNumber);
|
|
778
|
+
return { ...result, direction: 'to-github' };
|
|
779
|
+
} else if (conflict.remoteNewer) {
|
|
780
|
+
const result = await this.syncFromGitHub(githubNumber);
|
|
781
|
+
return { ...result, direction: 'from-github' };
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// No conflict or same timestamp - sync local to GitHub
|
|
786
|
+
const result = await this.syncToGitHub(issueNumber);
|
|
787
|
+
return { ...result, direction: 'to-github' };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Create new GitHub issue from local data
|
|
792
|
+
*
|
|
793
|
+
* @param {Object} issueData - Local issue data
|
|
794
|
+
* @returns {Promise<Object>} Created GitHub issue
|
|
795
|
+
*/
|
|
796
|
+
async createGitHubIssue(issueData) {
|
|
797
|
+
const labels = issueData.labels ? issueData.labels.split(',').map(l => l.trim()) : [];
|
|
798
|
+
const assignees = issueData.assignee ? [issueData.assignee] : [];
|
|
799
|
+
|
|
800
|
+
const githubData = {
|
|
801
|
+
title: issueData.title,
|
|
802
|
+
body: issueData.content || '',
|
|
803
|
+
state: this._mapStatusToGitHub(issueData.status),
|
|
804
|
+
labels,
|
|
805
|
+
assignees
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
// Remove empty arrays
|
|
809
|
+
if (githubData.labels.length === 0) delete githubData.labels;
|
|
810
|
+
if (githubData.assignees.length === 0) delete githubData.assignees;
|
|
811
|
+
|
|
812
|
+
const result = await this.provider.createIssue(githubData);
|
|
813
|
+
|
|
814
|
+
// Update sync-map if we have an ID
|
|
815
|
+
if (issueData.id) {
|
|
816
|
+
await this._updateSyncMap(String(issueData.id), String(result.number));
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
return result;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Update existing GitHub issue with local data
|
|
824
|
+
*
|
|
825
|
+
* @param {number|string} githubNumber - GitHub issue number
|
|
826
|
+
* @param {Object} issueData - Local issue data
|
|
827
|
+
* @returns {Promise<Object>} Updated GitHub issue
|
|
828
|
+
*/
|
|
829
|
+
async updateGitHubIssue(githubNumber, issueData) {
|
|
830
|
+
const updateData = {};
|
|
831
|
+
|
|
832
|
+
if (issueData.title) {
|
|
833
|
+
updateData.title = issueData.title;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (issueData.content) {
|
|
837
|
+
updateData.body = issueData.content;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (issueData.status) {
|
|
841
|
+
updateData.state = this._mapStatusToGitHub(issueData.status);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
if (issueData.labels) {
|
|
845
|
+
updateData.labels = issueData.labels.split(',').map(l => l.trim());
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (issueData.assignee) {
|
|
849
|
+
updateData.assignees = [issueData.assignee];
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
return await this.provider.updateIssue(githubNumber, updateData);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Detect sync conflicts between local and GitHub issues
|
|
857
|
+
*
|
|
858
|
+
* @param {Object} localIssue - Local issue data
|
|
859
|
+
* @param {Object} githubIssue - GitHub issue data
|
|
860
|
+
* @returns {Object} Conflict info: { hasConflict, localNewer, remoteNewer, conflictFields }
|
|
861
|
+
*/
|
|
862
|
+
detectConflict(localIssue, githubIssue) {
|
|
863
|
+
const localTime = new Date(localIssue.updated || localIssue.created || 0);
|
|
864
|
+
const githubTime = new Date(githubIssue.updated_at || githubIssue.created_at || 0);
|
|
865
|
+
|
|
866
|
+
const conflictFields = [];
|
|
867
|
+
|
|
868
|
+
// Check field differences
|
|
869
|
+
if (localIssue.title !== githubIssue.title) {
|
|
870
|
+
conflictFields.push('title');
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
const localStatus = this._mapStatusToGitHub(localIssue.status);
|
|
874
|
+
if (localStatus !== githubIssue.state) {
|
|
875
|
+
conflictFields.push('status');
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const hasConflict = localTime.getTime() !== githubTime.getTime();
|
|
879
|
+
const localNewer = localTime > githubTime;
|
|
880
|
+
const remoteNewer = githubTime > localTime;
|
|
881
|
+
|
|
882
|
+
return {
|
|
883
|
+
hasConflict,
|
|
884
|
+
localNewer,
|
|
885
|
+
remoteNewer,
|
|
886
|
+
conflictFields
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Resolve sync conflict using specified strategy
|
|
892
|
+
*
|
|
893
|
+
* @param {number|string} issueNumber - Local issue number
|
|
894
|
+
* @param {string} strategy - Resolution strategy (local|remote|newest|manual|merge)
|
|
895
|
+
* @returns {Promise<Object>} Resolution result: { resolved, appliedStrategy, result }
|
|
896
|
+
* @throws {Error} If strategy is invalid
|
|
897
|
+
*/
|
|
898
|
+
async resolveConflict(issueNumber, strategy) {
|
|
899
|
+
const validStrategies = ['local', 'remote', 'newest', 'manual', 'merge'];
|
|
900
|
+
|
|
901
|
+
if (!validStrategies.includes(strategy)) {
|
|
902
|
+
throw new Error('Invalid conflict resolution strategy');
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
if (strategy === 'manual') {
|
|
906
|
+
return {
|
|
907
|
+
resolved: false,
|
|
908
|
+
appliedStrategy: 'manual',
|
|
909
|
+
requiresManualResolution: true
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const syncMap = await this._loadSyncMap();
|
|
914
|
+
const githubNumber = syncMap['local-to-github'][String(issueNumber)];
|
|
915
|
+
|
|
916
|
+
if (strategy === 'local') {
|
|
917
|
+
// Use local version
|
|
918
|
+
const result = await this.syncToGitHub(issueNumber);
|
|
919
|
+
return {
|
|
920
|
+
resolved: true,
|
|
921
|
+
appliedStrategy: 'local',
|
|
922
|
+
result
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (strategy === 'remote') {
|
|
927
|
+
// Use remote version
|
|
928
|
+
const result = await this.syncFromGitHub(githubNumber);
|
|
929
|
+
return {
|
|
930
|
+
resolved: true,
|
|
931
|
+
appliedStrategy: 'remote',
|
|
932
|
+
result
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (strategy === 'newest') {
|
|
937
|
+
// Use newest version based on timestamp
|
|
938
|
+
const localIssue = await this.getLocalIssue(issueNumber);
|
|
939
|
+
const githubIssue = await this.provider.getIssue(githubNumber);
|
|
940
|
+
|
|
941
|
+
const localTime = new Date(localIssue.updated || localIssue.created || 0);
|
|
942
|
+
const githubTime = new Date(githubIssue.updated_at || githubIssue.created_at || 0);
|
|
943
|
+
|
|
944
|
+
if (localTime >= githubTime) {
|
|
945
|
+
const result = await this.syncToGitHub(issueNumber);
|
|
946
|
+
return {
|
|
947
|
+
resolved: true,
|
|
948
|
+
appliedStrategy: 'newest',
|
|
949
|
+
result
|
|
950
|
+
};
|
|
951
|
+
} else {
|
|
952
|
+
const result = await this.syncFromGitHub(githubNumber);
|
|
953
|
+
return {
|
|
954
|
+
resolved: true,
|
|
955
|
+
appliedStrategy: 'newest',
|
|
956
|
+
result
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// merge strategy would go here (future enhancement)
|
|
962
|
+
return {
|
|
963
|
+
resolved: false,
|
|
964
|
+
appliedStrategy: strategy,
|
|
965
|
+
message: 'Strategy not yet implemented'
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Get sync status for an issue
|
|
971
|
+
*
|
|
972
|
+
* @param {number|string} issueNumber - Local issue number
|
|
973
|
+
* @returns {Promise<Object>} Status: { synced, localNumber, githubNumber, lastSync, status }
|
|
974
|
+
*/
|
|
975
|
+
async getSyncStatus(issueNumber) {
|
|
976
|
+
const syncMap = await this._loadSyncMap();
|
|
977
|
+
const githubNumber = syncMap['local-to-github'][String(issueNumber)];
|
|
978
|
+
|
|
979
|
+
if (!githubNumber) {
|
|
980
|
+
return {
|
|
981
|
+
synced: false,
|
|
982
|
+
localNumber: String(issueNumber),
|
|
983
|
+
githubNumber: null,
|
|
984
|
+
status: 'not-synced'
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const metadata = syncMap.metadata[String(issueNumber)] || {};
|
|
989
|
+
|
|
990
|
+
// Check if out of sync
|
|
991
|
+
try {
|
|
992
|
+
const localIssue = await this.getLocalIssue(issueNumber);
|
|
993
|
+
const githubIssue = await this.provider.getIssue(githubNumber);
|
|
994
|
+
|
|
995
|
+
const localTime = new Date(localIssue.updated || localIssue.created || 0);
|
|
996
|
+
const githubTime = new Date(githubIssue.updated_at || githubIssue.created_at || 0);
|
|
997
|
+
const lastSyncTime = new Date(metadata.lastSync || 0);
|
|
998
|
+
|
|
999
|
+
const isOutOfSync = localTime > lastSyncTime || githubTime > lastSyncTime;
|
|
1000
|
+
|
|
1001
|
+
return {
|
|
1002
|
+
synced: !isOutOfSync,
|
|
1003
|
+
localNumber: String(issueNumber),
|
|
1004
|
+
githubNumber: String(githubNumber),
|
|
1005
|
+
lastSync: metadata.lastSync,
|
|
1006
|
+
status: isOutOfSync ? 'out-of-sync' : 'synced'
|
|
1007
|
+
};
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
return {
|
|
1010
|
+
synced: true,
|
|
1011
|
+
localNumber: String(issueNumber),
|
|
1012
|
+
githubNumber: String(githubNumber),
|
|
1013
|
+
lastSync: metadata.lastSync,
|
|
1014
|
+
status: 'synced'
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// ==========================================
|
|
1020
|
+
// PRIVATE HELPER METHODS FOR SYNC
|
|
1021
|
+
// ==========================================
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Load sync-map from file
|
|
1025
|
+
* @private
|
|
1026
|
+
*/
|
|
1027
|
+
async _loadSyncMap() {
|
|
1028
|
+
const fs = require('fs-extra');
|
|
1029
|
+
const path = require('path');
|
|
1030
|
+
const syncMapPath = path.join(process.cwd(), '.claude/sync-map.json');
|
|
1031
|
+
|
|
1032
|
+
if (await fs.pathExists(syncMapPath)) {
|
|
1033
|
+
return await fs.readJSON(syncMapPath);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return {
|
|
1037
|
+
'local-to-github': {},
|
|
1038
|
+
'github-to-local': {},
|
|
1039
|
+
'metadata': {}
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Save sync-map to file
|
|
1045
|
+
* @private
|
|
1046
|
+
*/
|
|
1047
|
+
async _saveSyncMap(syncMap) {
|
|
1048
|
+
const fs = require('fs-extra');
|
|
1049
|
+
const path = require('path');
|
|
1050
|
+
const syncMapPath = path.join(process.cwd(), '.claude/sync-map.json');
|
|
1051
|
+
await fs.writeJSON(syncMapPath, syncMap, { spaces: 2 });
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Update sync-map with new mapping
|
|
1056
|
+
* @private
|
|
1057
|
+
*/
|
|
1058
|
+
async _updateSyncMap(localNumber, githubNumber) {
|
|
1059
|
+
const syncMap = await this._loadSyncMap();
|
|
1060
|
+
|
|
1061
|
+
syncMap['local-to-github'][String(localNumber)] = String(githubNumber);
|
|
1062
|
+
syncMap['github-to-local'][String(githubNumber)] = String(localNumber);
|
|
1063
|
+
syncMap['metadata'][String(localNumber)] = {
|
|
1064
|
+
lastSync: new Date().toISOString(),
|
|
1065
|
+
githubNumber: String(githubNumber)
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
await this._saveSyncMap(syncMap);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* Map local status to GitHub state
|
|
1073
|
+
* @private
|
|
1074
|
+
*/
|
|
1075
|
+
_mapStatusToGitHub(status) {
|
|
1076
|
+
if (!status) return 'open';
|
|
1077
|
+
|
|
1078
|
+
const lowerStatus = status.toLowerCase();
|
|
1079
|
+
|
|
1080
|
+
if (['closed', 'completed', 'done', 'resolved'].includes(lowerStatus)) {
|
|
1081
|
+
return 'closed';
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return 'open';
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// ==========================================
|
|
1088
|
+
// 7. AZURE DEVOPS SYNC METHODS (8 NEW METHODS)
|
|
1089
|
+
// ==========================================
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Sync local issue to Azure DevOps (enhanced push with conflict detection)
|
|
1093
|
+
*
|
|
1094
|
+
* @param {number|string} issueNumber - Local issue number
|
|
1095
|
+
* @param {Object} [options={}] - Sync options
|
|
1096
|
+
* @param {boolean} [options.detectConflicts=false] - Enable conflict detection
|
|
1097
|
+
* @returns {Promise<Object>} Result: { success, issueNumber, workItemId, action, conflict? }
|
|
1098
|
+
*/
|
|
1099
|
+
async syncToAzure(issueNumber, options = {}) {
|
|
1100
|
+
const syncMap = await this._loadAzureSyncMap();
|
|
1101
|
+
const localIssue = await this.getLocalIssue(issueNumber);
|
|
1102
|
+
const workItemId = syncMap['local-to-azure'][String(issueNumber)];
|
|
1103
|
+
|
|
1104
|
+
let result;
|
|
1105
|
+
let action;
|
|
1106
|
+
|
|
1107
|
+
if (workItemId) {
|
|
1108
|
+
// Check for conflicts if enabled
|
|
1109
|
+
if (options.detectConflicts) {
|
|
1110
|
+
const azureWorkItem = await this.provider.getWorkItem(workItemId);
|
|
1111
|
+
const conflict = this.detectAzureConflict(localIssue, azureWorkItem);
|
|
1112
|
+
|
|
1113
|
+
if (conflict.hasConflict && conflict.remoteNewer) {
|
|
1114
|
+
return {
|
|
1115
|
+
success: false,
|
|
1116
|
+
issueNumber: String(issueNumber),
|
|
1117
|
+
workItemId: String(workItemId),
|
|
1118
|
+
conflict
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// Update existing Azure work item
|
|
1124
|
+
result = await this.updateAzureWorkItem(workItemId, localIssue);
|
|
1125
|
+
action = 'updated';
|
|
1126
|
+
} else {
|
|
1127
|
+
// Create new Azure work item
|
|
1128
|
+
result = await this.createAzureWorkItem(localIssue);
|
|
1129
|
+
action = 'created';
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Update azure-sync-map
|
|
1133
|
+
const workItemType = result.fields['System.WorkItemType'] || 'User Story';
|
|
1134
|
+
await this._updateAzureSyncMap(String(issueNumber), String(result.id), workItemType);
|
|
1135
|
+
|
|
1136
|
+
return {
|
|
1137
|
+
success: true,
|
|
1138
|
+
issueNumber: String(issueNumber),
|
|
1139
|
+
workItemId: String(result.id),
|
|
1140
|
+
action
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Sync Azure work item to local (enhanced pull with merge)
|
|
1146
|
+
*
|
|
1147
|
+
* @param {number|string} workItemId - Azure work item ID
|
|
1148
|
+
* @param {Object} [options={}] - Sync options
|
|
1149
|
+
* @param {boolean} [options.detectConflicts=false] - Enable conflict detection
|
|
1150
|
+
* @returns {Promise<Object>} Result: { success, localNumber, workItemId, action, conflict? }
|
|
1151
|
+
*/
|
|
1152
|
+
async syncFromAzure(workItemId, options = {}) {
|
|
1153
|
+
const fs = require('fs-extra');
|
|
1154
|
+
|
|
1155
|
+
const azureWorkItem = await this.provider.getWorkItem(workItemId);
|
|
1156
|
+
const syncMap = await this._loadAzureSyncMap();
|
|
1157
|
+
const localNumber = syncMap['azure-to-local'][String(workItemId)];
|
|
1158
|
+
|
|
1159
|
+
let action;
|
|
1160
|
+
let finalLocalNumber = localNumber;
|
|
1161
|
+
|
|
1162
|
+
if (localNumber) {
|
|
1163
|
+
// Check for conflicts if enabled
|
|
1164
|
+
if (options.detectConflicts) {
|
|
1165
|
+
const localIssue = await this.getLocalIssue(localNumber);
|
|
1166
|
+
const conflict = this.detectAzureConflict(localIssue, azureWorkItem);
|
|
1167
|
+
|
|
1168
|
+
if (conflict.hasConflict && conflict.localNewer) {
|
|
1169
|
+
return {
|
|
1170
|
+
success: false,
|
|
1171
|
+
localNumber: String(localNumber),
|
|
1172
|
+
workItemId: String(workItemId),
|
|
1173
|
+
conflict
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Update existing local issue
|
|
1179
|
+
action = 'updated';
|
|
1180
|
+
} else {
|
|
1181
|
+
// Create new local issue - find next available number
|
|
1182
|
+
const issues = await this.listIssues();
|
|
1183
|
+
const maxNumber = issues.reduce((max, issue) => {
|
|
1184
|
+
const num = parseInt(issue.id || '0');
|
|
1185
|
+
return num > max ? num : max;
|
|
1186
|
+
}, 0);
|
|
1187
|
+
finalLocalNumber = String(maxNumber + 1);
|
|
1188
|
+
action = 'created';
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Build issue content with frontmatter
|
|
1192
|
+
const tags = azureWorkItem.fields['System.Tags'] || '';
|
|
1193
|
+
const assignee = azureWorkItem.fields['System.AssignedTo']
|
|
1194
|
+
? azureWorkItem.fields['System.AssignedTo'].displayName
|
|
1195
|
+
: '';
|
|
1196
|
+
|
|
1197
|
+
const frontmatter = `---
|
|
1198
|
+
id: ${finalLocalNumber}
|
|
1199
|
+
title: ${azureWorkItem.fields['System.Title']}
|
|
1200
|
+
status: ${this._mapAzureStateToLocal(azureWorkItem.fields['System.State'])}
|
|
1201
|
+
${assignee ? `assignee: ${assignee}` : ''}
|
|
1202
|
+
${tags ? `labels: ${tags}` : ''}
|
|
1203
|
+
created: ${azureWorkItem.fields['System.CreatedDate']}
|
|
1204
|
+
updated: ${azureWorkItem.fields['System.ChangedDate']}
|
|
1205
|
+
azure_work_item_id: ${workItemId}
|
|
1206
|
+
work_item_type: ${azureWorkItem.fields['System.WorkItemType']}
|
|
1207
|
+
---
|
|
1208
|
+
|
|
1209
|
+
# ${azureWorkItem.fields['System.Title']}
|
|
1210
|
+
|
|
1211
|
+
${azureWorkItem.fields['System.Description'] || ''}
|
|
1212
|
+
`;
|
|
1213
|
+
|
|
1214
|
+
// Write to local file
|
|
1215
|
+
const issuePath = this.getIssuePath(finalLocalNumber);
|
|
1216
|
+
await fs.writeFile(issuePath, frontmatter);
|
|
1217
|
+
|
|
1218
|
+
// Update azure-sync-map
|
|
1219
|
+
const workItemType = azureWorkItem.fields['System.WorkItemType'] || 'User Story';
|
|
1220
|
+
await this._updateAzureSyncMap(String(finalLocalNumber), String(workItemId), workItemType);
|
|
1221
|
+
|
|
1222
|
+
return {
|
|
1223
|
+
success: true,
|
|
1224
|
+
localNumber: String(finalLocalNumber),
|
|
1225
|
+
workItemId: String(workItemId),
|
|
1226
|
+
action
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/**
|
|
1231
|
+
* Bidirectional Azure sync - sync in the direction of newer changes
|
|
1232
|
+
*
|
|
1233
|
+
* @param {number|string} issueNumber - Local issue number
|
|
1234
|
+
* @param {Object} [options={}] - Sync options
|
|
1235
|
+
* @param {string} [options.conflictStrategy='detect'] - How to handle conflicts
|
|
1236
|
+
* @returns {Promise<Object>} Result: { success, direction, conflict? }
|
|
1237
|
+
*/
|
|
1238
|
+
async syncBidirectionalAzure(issueNumber, options = {}) {
|
|
1239
|
+
const syncMap = await this._loadAzureSyncMap();
|
|
1240
|
+
const workItemId = syncMap['local-to-azure'][String(issueNumber)];
|
|
1241
|
+
|
|
1242
|
+
if (!workItemId) {
|
|
1243
|
+
// No Azure mapping, push to Azure
|
|
1244
|
+
const result = await this.syncToAzure(issueNumber);
|
|
1245
|
+
return { ...result, direction: 'to-azure' };
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const localIssue = await this.getLocalIssue(issueNumber);
|
|
1249
|
+
const azureWorkItem = await this.provider.getWorkItem(workItemId);
|
|
1250
|
+
|
|
1251
|
+
const conflict = this.detectAzureConflict(localIssue, azureWorkItem);
|
|
1252
|
+
|
|
1253
|
+
if (conflict.hasConflict) {
|
|
1254
|
+
if (options.conflictStrategy === 'detect') {
|
|
1255
|
+
return {
|
|
1256
|
+
success: false,
|
|
1257
|
+
direction: 'conflict',
|
|
1258
|
+
conflict
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Auto-resolve based on timestamps
|
|
1263
|
+
if (conflict.localNewer) {
|
|
1264
|
+
const result = await this.syncToAzure(issueNumber);
|
|
1265
|
+
return { ...result, direction: 'to-azure' };
|
|
1266
|
+
} else if (conflict.remoteNewer) {
|
|
1267
|
+
const result = await this.syncFromAzure(workItemId);
|
|
1268
|
+
return { ...result, direction: 'from-azure' };
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// No conflict or same timestamp - sync local to Azure
|
|
1273
|
+
const result = await this.syncToAzure(issueNumber);
|
|
1274
|
+
return { ...result, direction: 'to-azure' };
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
/**
|
|
1278
|
+
* Create new Azure work item from local data
|
|
1279
|
+
*
|
|
1280
|
+
* @param {Object} issueData - Local issue data
|
|
1281
|
+
* @returns {Promise<Object>} Created Azure work item
|
|
1282
|
+
*/
|
|
1283
|
+
async createAzureWorkItem(issueData) {
|
|
1284
|
+
const workItemType = issueData.work_item_type || 'User Story';
|
|
1285
|
+
const tags = issueData.labels || '';
|
|
1286
|
+
|
|
1287
|
+
const azureData = {
|
|
1288
|
+
title: issueData.title,
|
|
1289
|
+
description: issueData.content || '',
|
|
1290
|
+
state: this._mapStatusToAzure(issueData.status),
|
|
1291
|
+
tags
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
const result = await this.provider.createWorkItem(workItemType, azureData);
|
|
1295
|
+
|
|
1296
|
+
// Update azure-sync-map if we have an ID
|
|
1297
|
+
if (issueData.id) {
|
|
1298
|
+
const resultWorkItemType = result.fields['System.WorkItemType'] || workItemType;
|
|
1299
|
+
await this._updateAzureSyncMap(String(issueData.id), String(result.id), resultWorkItemType);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
return result;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Update existing Azure work item with local data
|
|
1307
|
+
*
|
|
1308
|
+
* @param {number|string} workItemId - Azure work item ID
|
|
1309
|
+
* @param {Object} issueData - Local issue data
|
|
1310
|
+
* @returns {Promise<Object>} Updated Azure work item
|
|
1311
|
+
*/
|
|
1312
|
+
async updateAzureWorkItem(workItemId, issueData) {
|
|
1313
|
+
const updateData = {};
|
|
1314
|
+
|
|
1315
|
+
if (issueData.title) {
|
|
1316
|
+
updateData.title = issueData.title;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
if (issueData.content) {
|
|
1320
|
+
updateData.description = issueData.content;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (issueData.status) {
|
|
1324
|
+
updateData.state = this._mapStatusToAzure(issueData.status);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
return await this.provider.updateWorkItem(workItemId, updateData);
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Detect sync conflicts between local and Azure work items
|
|
1332
|
+
*
|
|
1333
|
+
* @param {Object} localIssue - Local issue data
|
|
1334
|
+
* @param {Object} azureWorkItem - Azure work item data
|
|
1335
|
+
* @returns {Object} Conflict info: { hasConflict, localNewer, remoteNewer, conflictFields }
|
|
1336
|
+
*/
|
|
1337
|
+
detectAzureConflict(localIssue, azureWorkItem) {
|
|
1338
|
+
const localTime = new Date(localIssue.updated || localIssue.created || 0);
|
|
1339
|
+
const azureTime = new Date(azureWorkItem.fields['System.ChangedDate'] || azureWorkItem.fields['System.CreatedDate'] || 0);
|
|
1340
|
+
|
|
1341
|
+
const conflictFields = [];
|
|
1342
|
+
|
|
1343
|
+
// Check field differences
|
|
1344
|
+
if (localIssue.title !== azureWorkItem.fields['System.Title']) {
|
|
1345
|
+
conflictFields.push('title');
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const localStatus = this._mapStatusToAzure(localIssue.status);
|
|
1349
|
+
if (localStatus !== azureWorkItem.fields['System.State']) {
|
|
1350
|
+
conflictFields.push('status');
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const hasConflict = localTime.getTime() !== azureTime.getTime();
|
|
1354
|
+
const localNewer = localTime > azureTime;
|
|
1355
|
+
const remoteNewer = azureTime > localTime;
|
|
1356
|
+
|
|
1357
|
+
return {
|
|
1358
|
+
hasConflict,
|
|
1359
|
+
localNewer,
|
|
1360
|
+
remoteNewer,
|
|
1361
|
+
conflictFields
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
/**
|
|
1366
|
+
* Resolve Azure sync conflict using specified strategy
|
|
1367
|
+
*
|
|
1368
|
+
* @param {number|string} issueNumber - Local issue number
|
|
1369
|
+
* @param {string} strategy - Resolution strategy (local|remote|newest|manual|merge)
|
|
1370
|
+
* @returns {Promise<Object>} Resolution result: { resolved, appliedStrategy, result }
|
|
1371
|
+
* @throws {Error} If strategy is invalid
|
|
1372
|
+
*/
|
|
1373
|
+
async resolveAzureConflict(issueNumber, strategy) {
|
|
1374
|
+
const validStrategies = ['local', 'remote', 'newest', 'manual', 'merge'];
|
|
1375
|
+
|
|
1376
|
+
if (!validStrategies.includes(strategy)) {
|
|
1377
|
+
throw new Error('Invalid conflict resolution strategy');
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (strategy === 'manual') {
|
|
1381
|
+
return {
|
|
1382
|
+
resolved: false,
|
|
1383
|
+
appliedStrategy: 'manual',
|
|
1384
|
+
requiresManualResolution: true
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
const syncMap = await this._loadAzureSyncMap();
|
|
1389
|
+
const workItemId = syncMap['local-to-azure'][String(issueNumber)];
|
|
1390
|
+
|
|
1391
|
+
if (strategy === 'local') {
|
|
1392
|
+
// Use local version
|
|
1393
|
+
const result = await this.syncToAzure(issueNumber);
|
|
1394
|
+
return {
|
|
1395
|
+
resolved: true,
|
|
1396
|
+
appliedStrategy: 'local',
|
|
1397
|
+
result
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
if (strategy === 'remote') {
|
|
1402
|
+
// Use remote version
|
|
1403
|
+
const result = await this.syncFromAzure(workItemId);
|
|
1404
|
+
return {
|
|
1405
|
+
resolved: true,
|
|
1406
|
+
appliedStrategy: 'remote',
|
|
1407
|
+
result
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
if (strategy === 'newest') {
|
|
1412
|
+
// Use newest version based on timestamp
|
|
1413
|
+
const localIssue = await this.getLocalIssue(issueNumber);
|
|
1414
|
+
const azureWorkItem = await this.provider.getWorkItem(workItemId);
|
|
1415
|
+
|
|
1416
|
+
const localTime = new Date(localIssue.updated || localIssue.created || 0);
|
|
1417
|
+
const azureTime = new Date(azureWorkItem.fields['System.ChangedDate'] || azureWorkItem.fields['System.CreatedDate'] || 0);
|
|
1418
|
+
|
|
1419
|
+
if (localTime >= azureTime) {
|
|
1420
|
+
const result = await this.syncToAzure(issueNumber);
|
|
1421
|
+
return {
|
|
1422
|
+
resolved: true,
|
|
1423
|
+
appliedStrategy: 'newest',
|
|
1424
|
+
result
|
|
1425
|
+
};
|
|
1426
|
+
} else {
|
|
1427
|
+
const result = await this.syncFromAzure(workItemId);
|
|
1428
|
+
return {
|
|
1429
|
+
resolved: true,
|
|
1430
|
+
appliedStrategy: 'newest',
|
|
1431
|
+
result
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// merge strategy would go here (future enhancement)
|
|
1437
|
+
return {
|
|
1438
|
+
resolved: false,
|
|
1439
|
+
appliedStrategy: strategy,
|
|
1440
|
+
message: 'Strategy not yet implemented'
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Get Azure sync status for an issue
|
|
1446
|
+
*
|
|
1447
|
+
* @param {number|string} issueNumber - Local issue number
|
|
1448
|
+
* @returns {Promise<Object>} Status: { synced, localNumber, workItemId, lastSync, status }
|
|
1449
|
+
*/
|
|
1450
|
+
async getAzureSyncStatus(issueNumber) {
|
|
1451
|
+
const syncMap = await this._loadAzureSyncMap();
|
|
1452
|
+
const workItemId = syncMap['local-to-azure'][String(issueNumber)];
|
|
1453
|
+
|
|
1454
|
+
if (!workItemId) {
|
|
1455
|
+
return {
|
|
1456
|
+
synced: false,
|
|
1457
|
+
localNumber: String(issueNumber),
|
|
1458
|
+
workItemId: null,
|
|
1459
|
+
status: 'not-synced'
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const metadata = syncMap.metadata[String(issueNumber)] || {};
|
|
1464
|
+
|
|
1465
|
+
// Check if out of sync
|
|
1466
|
+
try {
|
|
1467
|
+
const localIssue = await this.getLocalIssue(issueNumber);
|
|
1468
|
+
const azureWorkItem = await this.provider.getWorkItem(workItemId);
|
|
1469
|
+
|
|
1470
|
+
const localTime = new Date(localIssue.updated || localIssue.created || 0);
|
|
1471
|
+
const azureTime = new Date(azureWorkItem.fields['System.ChangedDate'] || azureWorkItem.fields['System.CreatedDate'] || 0);
|
|
1472
|
+
const lastSyncTime = new Date(metadata.lastSync || 0);
|
|
1473
|
+
|
|
1474
|
+
const isOutOfSync = localTime > lastSyncTime || azureTime > lastSyncTime;
|
|
1475
|
+
|
|
1476
|
+
return {
|
|
1477
|
+
synced: !isOutOfSync,
|
|
1478
|
+
localNumber: String(issueNumber),
|
|
1479
|
+
workItemId: String(workItemId),
|
|
1480
|
+
lastSync: metadata.lastSync,
|
|
1481
|
+
status: isOutOfSync ? 'out-of-sync' : 'synced'
|
|
1482
|
+
};
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
return {
|
|
1485
|
+
synced: true,
|
|
1486
|
+
localNumber: String(issueNumber),
|
|
1487
|
+
workItemId: String(workItemId),
|
|
1488
|
+
lastSync: metadata.lastSync,
|
|
1489
|
+
status: 'synced'
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// ==========================================
|
|
1495
|
+
// PRIVATE HELPER METHODS FOR AZURE SYNC
|
|
1496
|
+
// ==========================================
|
|
1497
|
+
|
|
1498
|
+
/**
|
|
1499
|
+
* Load azure-sync-map from file
|
|
1500
|
+
* @private
|
|
1501
|
+
*/
|
|
1502
|
+
async _loadAzureSyncMap() {
|
|
1503
|
+
const fs = require('fs-extra');
|
|
1504
|
+
const path = require('path');
|
|
1505
|
+
const syncMapPath = path.join(process.cwd(), '.claude/azure-sync-map.json');
|
|
1506
|
+
|
|
1507
|
+
if (await fs.pathExists(syncMapPath)) {
|
|
1508
|
+
return await fs.readJSON(syncMapPath);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
return {
|
|
1512
|
+
'local-to-azure': {},
|
|
1513
|
+
'azure-to-local': {},
|
|
1514
|
+
'metadata': {}
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Save azure-sync-map to file
|
|
1520
|
+
* @private
|
|
1521
|
+
*/
|
|
1522
|
+
async _saveAzureSyncMap(syncMap) {
|
|
1523
|
+
const fs = require('fs-extra');
|
|
1524
|
+
const path = require('path');
|
|
1525
|
+
const syncMapPath = path.join(process.cwd(), '.claude/azure-sync-map.json');
|
|
1526
|
+
await fs.writeJSON(syncMapPath, syncMap, { spaces: 2 });
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
/**
|
|
1530
|
+
* Update azure-sync-map with new mapping
|
|
1531
|
+
* @private
|
|
1532
|
+
*/
|
|
1533
|
+
async _updateAzureSyncMap(localNumber, workItemId, workItemType = 'User Story') {
|
|
1534
|
+
const syncMap = await this._loadAzureSyncMap();
|
|
1535
|
+
|
|
1536
|
+
syncMap['local-to-azure'][String(localNumber)] = String(workItemId);
|
|
1537
|
+
syncMap['azure-to-local'][String(workItemId)] = String(localNumber);
|
|
1538
|
+
syncMap['metadata'][String(localNumber)] = {
|
|
1539
|
+
lastSync: new Date().toISOString(),
|
|
1540
|
+
workItemId: String(workItemId),
|
|
1541
|
+
workItemType
|
|
1542
|
+
};
|
|
1543
|
+
|
|
1544
|
+
await this._saveAzureSyncMap(syncMap);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* Map local status to Azure state
|
|
1549
|
+
* @private
|
|
1550
|
+
*/
|
|
1551
|
+
_mapStatusToAzure(status) {
|
|
1552
|
+
if (!status) return 'New';
|
|
1553
|
+
|
|
1554
|
+
const lowerStatus = status.toLowerCase();
|
|
1555
|
+
const statusMap = {
|
|
1556
|
+
'open': 'New',
|
|
1557
|
+
'in-progress': 'Active',
|
|
1558
|
+
'done': 'Resolved',
|
|
1559
|
+
'completed': 'Resolved',
|
|
1560
|
+
'closed': 'Closed'
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
return statusMap[lowerStatus] || 'New';
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
/**
|
|
1567
|
+
* Map Azure state to local status
|
|
1568
|
+
* @private
|
|
1569
|
+
*/
|
|
1570
|
+
_mapAzureStateToLocal(state) {
|
|
1571
|
+
const stateMap = {
|
|
1572
|
+
'New': 'open',
|
|
1573
|
+
'Active': 'in-progress',
|
|
1574
|
+
'Resolved': 'done',
|
|
1575
|
+
'Closed': 'closed'
|
|
1576
|
+
};
|
|
1577
|
+
|
|
1578
|
+
return stateMap[state] || 'open';
|
|
1579
|
+
}
|
|
589
1580
|
}
|
|
590
1581
|
|
|
591
1582
|
module.exports = IssueService;
|