claude-autopm 1.27.0 ā 1.29.0
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 +72 -0
- package/autopm/.claude/scripts/pm/analytics.js +425 -0
- package/autopm/.claude/scripts/pm/prd-new.js +292 -2
- package/autopm/.claude/scripts/pm/sync-batch.js +337 -0
- package/autopm/.claude/scripts/pm/template-list.js +119 -0
- package/autopm/.claude/scripts/pm/template-new.js +344 -0
- package/autopm/.claude/templates/prds/README.md +334 -0
- package/autopm/.claude/templates/prds/api-feature.md +306 -0
- package/autopm/.claude/templates/prds/bug-fix.md +413 -0
- package/autopm/.claude/templates/prds/data-migration.md +483 -0
- package/autopm/.claude/templates/prds/documentation.md +439 -0
- package/autopm/.claude/templates/prds/ui-feature.md +365 -0
- package/lib/README-FILTER-SEARCH.md +285 -0
- package/lib/analytics-engine.js +689 -0
- package/lib/batch-processor-integration.js +366 -0
- package/lib/batch-processor.js +278 -0
- package/lib/burndown-chart.js +415 -0
- package/lib/dependency-analyzer.js +466 -0
- package/lib/filter-engine.js +414 -0
- package/lib/query-parser.js +322 -0
- package/lib/template-engine.js +347 -0
- package/package.json +5 -4
|
@@ -1,16 +1,282 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* PRD New - Launch brainstorming for new product requirement
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* - Template-based creation (--template flag)
|
|
7
|
+
* - Interactive template selection
|
|
8
|
+
* - Traditional brainstorming mode (backwards compatible)
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
const fs = require('fs');
|
|
7
12
|
const path = require('path');
|
|
8
13
|
const readline = require('readline');
|
|
9
14
|
|
|
15
|
+
// Dynamically resolve template engine path
|
|
16
|
+
// This works both in installed projects and during testing
|
|
17
|
+
let TemplateEngine;
|
|
18
|
+
try {
|
|
19
|
+
// Try relative path from .claude/scripts/pm/
|
|
20
|
+
TemplateEngine = require(path.join(__dirname, '..', '..', '..', '..', 'lib', 'template-engine'));
|
|
21
|
+
} catch (err) {
|
|
22
|
+
try {
|
|
23
|
+
// Try from project root
|
|
24
|
+
TemplateEngine = require(path.join(process.cwd(), 'lib', 'template-engine'));
|
|
25
|
+
} catch (err2) {
|
|
26
|
+
// Fallback to relative
|
|
27
|
+
TemplateEngine = require('../../../../lib/template-engine');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
10
31
|
class PrdCreator {
|
|
11
32
|
constructor() {
|
|
12
33
|
this.prdsDir = path.join('.claude', 'prds');
|
|
13
34
|
this.templatesDir = path.join(__dirname, '..', '..', 'templates');
|
|
35
|
+
this.templateEngine = new TemplateEngine();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create PRD from template
|
|
40
|
+
*/
|
|
41
|
+
async createPrdFromTemplate(prdName, templateName) {
|
|
42
|
+
console.log(`\nš Creating PRD from Template: ${templateName}`);
|
|
43
|
+
console.log(`${'ā'.repeat(50)}\n`);
|
|
44
|
+
|
|
45
|
+
// Ensure PRDs directory exists
|
|
46
|
+
if (!fs.existsSync(this.prdsDir)) {
|
|
47
|
+
fs.mkdirSync(this.prdsDir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if PRD already exists
|
|
51
|
+
const prdFile = path.join(this.prdsDir, `${prdName}.md`);
|
|
52
|
+
if (fs.existsSync(prdFile)) {
|
|
53
|
+
console.error(`ā PRD already exists: ${prdName}`);
|
|
54
|
+
console.log(`š” Edit file: .claude/prds/${prdName}.md`);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Find template
|
|
59
|
+
const templatePath = this.templateEngine.findTemplate('prds', templateName);
|
|
60
|
+
if (!templatePath) {
|
|
61
|
+
console.error(`ā Template not found: ${templateName}`);
|
|
62
|
+
console.log(`š” List available templates: autopm template:list`);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Read template to find required variables
|
|
67
|
+
const templateContent = fs.readFileSync(templatePath, 'utf8');
|
|
68
|
+
const requiredVars = this.extractTemplateVariables(templateContent);
|
|
69
|
+
|
|
70
|
+
// Prompt for variables
|
|
71
|
+
const rl = readline.createInterface({
|
|
72
|
+
input: process.stdin,
|
|
73
|
+
output: process.stdout
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const prompt = (question) => new Promise((resolve) => {
|
|
77
|
+
rl.question(question, resolve);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
console.log(`š Template: ${templateName}`);
|
|
82
|
+
console.log(`Fill in the following details:\n`);
|
|
83
|
+
|
|
84
|
+
const variables = {
|
|
85
|
+
title: prdName.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
86
|
+
type: 'prd'
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Prompt for common variables
|
|
90
|
+
const title = await prompt(`Title [${variables.title}]: `);
|
|
91
|
+
if (title) variables.title = title;
|
|
92
|
+
|
|
93
|
+
const priority = await prompt('Priority (P0/P1/P2/P3) [P2]: ');
|
|
94
|
+
variables.priority = priority || 'P2';
|
|
95
|
+
|
|
96
|
+
const timeline = await prompt('Timeline [TBD]: ');
|
|
97
|
+
variables.timeline = timeline || 'TBD';
|
|
98
|
+
|
|
99
|
+
// Prompt for template-specific variables
|
|
100
|
+
const templateSpecific = this.getTemplateSpecificPrompts(templateName);
|
|
101
|
+
for (const varName of templateSpecific) {
|
|
102
|
+
if (!variables[varName]) {
|
|
103
|
+
const value = await prompt(`${varName.replace(/_/g, ' ')}: `);
|
|
104
|
+
variables[varName] = value || '';
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Render template
|
|
109
|
+
const rendered = this.templateEngine.renderFile(templatePath, variables);
|
|
110
|
+
|
|
111
|
+
// Write PRD file
|
|
112
|
+
fs.writeFileSync(prdFile, rendered);
|
|
113
|
+
|
|
114
|
+
console.log('\nā
PRD created successfully!');
|
|
115
|
+
console.log(`š File: ${prdFile}`);
|
|
116
|
+
|
|
117
|
+
// Show next steps
|
|
118
|
+
this.showNextSteps(prdName);
|
|
119
|
+
|
|
120
|
+
return true;
|
|
121
|
+
} finally {
|
|
122
|
+
rl.close();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Extract variables from template
|
|
128
|
+
*/
|
|
129
|
+
extractTemplateVariables(template) {
|
|
130
|
+
const varRegex = /\{\{(\w+)\}\}/g;
|
|
131
|
+
const vars = new Set();
|
|
132
|
+
let match;
|
|
133
|
+
|
|
134
|
+
while ((match = varRegex.exec(template)) !== null) {
|
|
135
|
+
vars.add(match[1]);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Remove auto-generated variables
|
|
139
|
+
vars.delete('id');
|
|
140
|
+
vars.delete('timestamp');
|
|
141
|
+
vars.delete('date');
|
|
142
|
+
vars.delete('author');
|
|
143
|
+
|
|
144
|
+
return Array.from(vars);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Get template-specific prompts
|
|
149
|
+
*/
|
|
150
|
+
getTemplateSpecificPrompts(templateName) {
|
|
151
|
+
const prompts = {
|
|
152
|
+
'api-feature': [
|
|
153
|
+
'api_purpose',
|
|
154
|
+
'problem',
|
|
155
|
+
'business_value',
|
|
156
|
+
'http_method',
|
|
157
|
+
'api_endpoint',
|
|
158
|
+
'auth_method',
|
|
159
|
+
'rate_limit',
|
|
160
|
+
'user_role',
|
|
161
|
+
'api_action',
|
|
162
|
+
'user_benefit'
|
|
163
|
+
],
|
|
164
|
+
'ui-feature': [
|
|
165
|
+
'component_type',
|
|
166
|
+
'feature_purpose',
|
|
167
|
+
'problem',
|
|
168
|
+
'user_need',
|
|
169
|
+
'user_goal',
|
|
170
|
+
'user_role',
|
|
171
|
+
'user_action',
|
|
172
|
+
'user_benefit'
|
|
173
|
+
],
|
|
174
|
+
'bug-fix': [
|
|
175
|
+
'bug_summary',
|
|
176
|
+
'severity',
|
|
177
|
+
'user_impact',
|
|
178
|
+
'step_1',
|
|
179
|
+
'step_2',
|
|
180
|
+
'step_3',
|
|
181
|
+
'expected_behavior',
|
|
182
|
+
'actual_behavior',
|
|
183
|
+
'root_cause',
|
|
184
|
+
'solution_approach'
|
|
185
|
+
],
|
|
186
|
+
'data-migration': [
|
|
187
|
+
'migration_purpose',
|
|
188
|
+
'current_state',
|
|
189
|
+
'desired_state',
|
|
190
|
+
'affected_tables',
|
|
191
|
+
'data_volume',
|
|
192
|
+
'migration_strategy'
|
|
193
|
+
],
|
|
194
|
+
'documentation': [
|
|
195
|
+
'doc_type',
|
|
196
|
+
'target_audience',
|
|
197
|
+
'documentation_scope',
|
|
198
|
+
'current_gaps'
|
|
199
|
+
]
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return prompts[templateName] || [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Show interactive template selection
|
|
207
|
+
*/
|
|
208
|
+
async selectTemplate() {
|
|
209
|
+
const templates = this.templateEngine.listTemplates('prds');
|
|
210
|
+
|
|
211
|
+
if (templates.length === 0) {
|
|
212
|
+
console.log('No templates available');
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.log('\nš Available Templates:');
|
|
217
|
+
|
|
218
|
+
const builtIn = templates.filter(t => !t.custom);
|
|
219
|
+
const custom = templates.filter(t => t.custom);
|
|
220
|
+
|
|
221
|
+
let index = 1;
|
|
222
|
+
const options = [];
|
|
223
|
+
|
|
224
|
+
builtIn.forEach(t => {
|
|
225
|
+
const description = this.getTemplateDescription(t.name);
|
|
226
|
+
console.log(`${index}. ${t.name.padEnd(20)} - ${description}`);
|
|
227
|
+
options.push(t.name);
|
|
228
|
+
index++;
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (custom.length > 0) {
|
|
232
|
+
console.log('\nCustom Templates:');
|
|
233
|
+
custom.forEach(t => {
|
|
234
|
+
console.log(`${index}. ${t.name.padEnd(20)} - [Custom]`);
|
|
235
|
+
options.push(t.name);
|
|
236
|
+
index++;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log(`${index}. none${' '.repeat(20)} - Create empty PRD\n`);
|
|
241
|
+
options.push('none');
|
|
242
|
+
|
|
243
|
+
const rl = readline.createInterface({
|
|
244
|
+
input: process.stdin,
|
|
245
|
+
output: process.stdout
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const prompt = (question) => new Promise((resolve) => {
|
|
249
|
+
rl.question(question, resolve);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
const selection = await prompt(`Select template (1-${options.length}): `);
|
|
254
|
+
const selectionNum = parseInt(selection, 10);
|
|
255
|
+
|
|
256
|
+
if (selectionNum < 1 || selectionNum > options.length) {
|
|
257
|
+
console.error('Invalid selection');
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return options[selectionNum - 1];
|
|
262
|
+
} finally {
|
|
263
|
+
rl.close();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get template description
|
|
269
|
+
*/
|
|
270
|
+
getTemplateDescription(templateName) {
|
|
271
|
+
const descriptions = {
|
|
272
|
+
'api-feature': 'REST/GraphQL API development',
|
|
273
|
+
'ui-feature': 'Frontend component/page',
|
|
274
|
+
'bug-fix': 'Bug resolution workflow',
|
|
275
|
+
'data-migration': 'Database schema changes',
|
|
276
|
+
'documentation': 'Documentation updates'
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return descriptions[templateName] || 'Template';
|
|
14
280
|
}
|
|
15
281
|
|
|
16
282
|
async createPrd(prdName) {
|
|
@@ -319,7 +585,19 @@ ${data.technical || 'Technical requirements to be specified...'}
|
|
|
319
585
|
}
|
|
320
586
|
|
|
321
587
|
async run(args) {
|
|
322
|
-
|
|
588
|
+
// Parse arguments
|
|
589
|
+
let prdName = null;
|
|
590
|
+
let templateName = null;
|
|
591
|
+
|
|
592
|
+
// Check for --template or -t flag
|
|
593
|
+
for (let i = 0; i < args.length; i++) {
|
|
594
|
+
if (args[i] === '--template' || args[i] === '-t') {
|
|
595
|
+
templateName = args[i + 1];
|
|
596
|
+
i++; // Skip next arg
|
|
597
|
+
} else if (!prdName) {
|
|
598
|
+
prdName = args[i];
|
|
599
|
+
}
|
|
600
|
+
}
|
|
323
601
|
|
|
324
602
|
if (!prdName) {
|
|
325
603
|
// Interactive mode
|
|
@@ -345,12 +623,24 @@ ${data.technical || 'Technical requirements to be specified...'}
|
|
|
345
623
|
console.error('ā Error: PRD name required');
|
|
346
624
|
process.exit(1);
|
|
347
625
|
}
|
|
626
|
+
|
|
627
|
+
// Ask for template if not provided
|
|
628
|
+
if (!templateName) {
|
|
629
|
+
templateName = await this.selectTemplate();
|
|
630
|
+
}
|
|
348
631
|
}
|
|
349
632
|
|
|
350
633
|
// Sanitize PRD name
|
|
351
634
|
prdName = prdName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
352
635
|
|
|
353
|
-
|
|
636
|
+
// Create PRD from template or traditional mode
|
|
637
|
+
let success;
|
|
638
|
+
if (templateName && templateName !== 'none') {
|
|
639
|
+
success = await this.createPrdFromTemplate(prdName, templateName);
|
|
640
|
+
} else {
|
|
641
|
+
success = await this.createPrd(prdName);
|
|
642
|
+
}
|
|
643
|
+
|
|
354
644
|
process.exit(success ? 0 : 1);
|
|
355
645
|
}
|
|
356
646
|
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Batch Sync Command
|
|
4
|
+
*
|
|
5
|
+
* Synchronize multiple PRDs/Epics/Tasks to GitHub in batch operations
|
|
6
|
+
* with parallel processing, rate limiting, and progress tracking.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* autopm sync:batch # Sync all items
|
|
10
|
+
* autopm sync:batch --type prd # Sync only PRDs
|
|
11
|
+
* autopm sync:batch --type epic # Sync only Epics
|
|
12
|
+
* autopm sync:batch --type task # Sync only Tasks
|
|
13
|
+
* autopm sync:batch --dry-run # Preview without syncing
|
|
14
|
+
* autopm sync:batch --concurrent 5 # Limit concurrency
|
|
15
|
+
*
|
|
16
|
+
* Features:
|
|
17
|
+
* - Parallel processing (default: 10 concurrent)
|
|
18
|
+
* - GitHub API rate limiting with backoff
|
|
19
|
+
* - Real-time progress tracking
|
|
20
|
+
* - Error recovery (continues on failures)
|
|
21
|
+
* - Dry run mode
|
|
22
|
+
*
|
|
23
|
+
* Performance:
|
|
24
|
+
* - 1000 items: < 30 seconds
|
|
25
|
+
* - Respects GitHub API limits (5000 req/hour)
|
|
26
|
+
* - Memory efficient: < 100MB for 1000 items
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs').promises;
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const { Octokit } = require('@octokit/rest');
|
|
32
|
+
const { batchSyncAll, batchSyncPRDs, batchSyncEpics, batchSyncTasks } = require('../../../../lib/batch-processor-integration');
|
|
33
|
+
|
|
34
|
+
class SyncBatchCommand {
|
|
35
|
+
constructor() {
|
|
36
|
+
this.basePath = '.claude';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get GitHub repository info from git remote
|
|
41
|
+
*/
|
|
42
|
+
async getRepoInfo() {
|
|
43
|
+
const { execSync } = require('child_process');
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf8' }).trim();
|
|
47
|
+
|
|
48
|
+
// Parse GitHub URL: https://github.com/owner/repo.git or git@github.com:owner/repo.git
|
|
49
|
+
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/(.+?)(\.git)?$/);
|
|
50
|
+
|
|
51
|
+
if (!match) {
|
|
52
|
+
throw new Error('Could not parse GitHub repository URL');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
owner: match[1],
|
|
57
|
+
repo: match[2]
|
|
58
|
+
};
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new Error('Failed to get repository info. Are you in a Git repository with GitHub remote?');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Show help message
|
|
66
|
+
*/
|
|
67
|
+
showHelp() {
|
|
68
|
+
console.log(`
|
|
69
|
+
š¦ Batch Sync Command
|
|
70
|
+
|
|
71
|
+
Synchronize multiple PRDs/Epics/Tasks to GitHub in parallel.
|
|
72
|
+
|
|
73
|
+
Usage:
|
|
74
|
+
autopm sync:batch [options]
|
|
75
|
+
|
|
76
|
+
Options:
|
|
77
|
+
--type <type> Type to sync: prd|epic|task|all (default: all)
|
|
78
|
+
--dry-run Preview without syncing
|
|
79
|
+
--concurrent <n> Max concurrent uploads (default: 10)
|
|
80
|
+
--help Show this help
|
|
81
|
+
|
|
82
|
+
Examples:
|
|
83
|
+
autopm sync:batch # Sync all items
|
|
84
|
+
autopm sync:batch --type prd # Sync only PRDs
|
|
85
|
+
autopm sync:batch --dry-run # Preview changes
|
|
86
|
+
autopm sync:batch --concurrent 5 # Limit to 5 parallel
|
|
87
|
+
|
|
88
|
+
Performance:
|
|
89
|
+
- 1000 items: ~30 seconds
|
|
90
|
+
- Rate limiting: Automatic backoff
|
|
91
|
+
- Memory: < 100MB for 1000 items
|
|
92
|
+
|
|
93
|
+
Environment:
|
|
94
|
+
GITHUB_TOKEN GitHub personal access token (required)
|
|
95
|
+
`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse command line arguments
|
|
100
|
+
*/
|
|
101
|
+
parseArgs(args) {
|
|
102
|
+
const options = {
|
|
103
|
+
type: 'all',
|
|
104
|
+
dryRun: false,
|
|
105
|
+
maxConcurrent: 10
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < args.length; i++) {
|
|
109
|
+
const arg = args[i];
|
|
110
|
+
|
|
111
|
+
if (arg === '--help' || arg === '-h') {
|
|
112
|
+
this.showHelp();
|
|
113
|
+
process.exit(0);
|
|
114
|
+
} else if (arg === '--type' || arg === '-t') {
|
|
115
|
+
options.type = args[++i];
|
|
116
|
+
if (!['prd', 'epic', 'task', 'all'].includes(options.type)) {
|
|
117
|
+
throw new Error(`Invalid type: ${options.type}. Must be: prd|epic|task|all`);
|
|
118
|
+
}
|
|
119
|
+
} else if (arg === '--dry-run' || arg === '-d') {
|
|
120
|
+
options.dryRun = true;
|
|
121
|
+
} else if (arg === '--concurrent' || arg === '-c') {
|
|
122
|
+
options.maxConcurrent = parseInt(args[++i], 10);
|
|
123
|
+
if (isNaN(options.maxConcurrent) || options.maxConcurrent < 1) {
|
|
124
|
+
throw new Error('--concurrent must be a positive number');
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
throw new Error(`Unknown option: ${arg}. Use --help for usage.`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return options;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Format duration in human-readable form
|
|
136
|
+
*/
|
|
137
|
+
formatDuration(ms) {
|
|
138
|
+
if (ms < 1000) {
|
|
139
|
+
return `${ms}ms`;
|
|
140
|
+
}
|
|
141
|
+
const seconds = Math.floor(ms / 1000);
|
|
142
|
+
const minutes = Math.floor(seconds / 60);
|
|
143
|
+
|
|
144
|
+
if (minutes > 0) {
|
|
145
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
146
|
+
}
|
|
147
|
+
return `${seconds}s`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Print progress bar
|
|
152
|
+
*/
|
|
153
|
+
printProgress(type, current, total) {
|
|
154
|
+
const percentage = Math.floor((current / total) * 100);
|
|
155
|
+
const barLength = 30;
|
|
156
|
+
const filledLength = Math.floor((current / total) * barLength);
|
|
157
|
+
const bar = 'ā'.repeat(filledLength) + 'ā'.repeat(barLength - filledLength);
|
|
158
|
+
|
|
159
|
+
process.stdout.write(`\r [${type.toUpperCase().padEnd(5)}] ${bar} ${percentage}% (${current}/${total})`);
|
|
160
|
+
|
|
161
|
+
if (current === total) {
|
|
162
|
+
console.log(); // New line when complete
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Run batch sync
|
|
168
|
+
*/
|
|
169
|
+
async run(args) {
|
|
170
|
+
try {
|
|
171
|
+
// Parse arguments
|
|
172
|
+
const options = this.parseArgs(args);
|
|
173
|
+
|
|
174
|
+
// Check for GitHub token
|
|
175
|
+
const githubToken = process.env.GITHUB_TOKEN;
|
|
176
|
+
if (!githubToken) {
|
|
177
|
+
console.error('ā Error: GITHUB_TOKEN environment variable not set');
|
|
178
|
+
console.error('\nSet your GitHub token:');
|
|
179
|
+
console.error(' export GITHUB_TOKEN=your_token_here');
|
|
180
|
+
console.error('\nOr create a .env file with:');
|
|
181
|
+
console.error(' GITHUB_TOKEN=your_token_here');
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Get repository info
|
|
186
|
+
console.log('š Detecting repository...');
|
|
187
|
+
const repo = await this.getRepoInfo();
|
|
188
|
+
console.log(` Repository: ${repo.owner}/${repo.repo}`);
|
|
189
|
+
|
|
190
|
+
// Initialize Octokit
|
|
191
|
+
const octokit = new Octokit({ auth: githubToken });
|
|
192
|
+
|
|
193
|
+
// Verify API access
|
|
194
|
+
try {
|
|
195
|
+
await octokit.rest.users.getAuthenticated();
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error('ā GitHub authentication failed. Check your GITHUB_TOKEN.');
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Show configuration
|
|
202
|
+
console.log('\nāļø Configuration:');
|
|
203
|
+
console.log(` Type: ${options.type}`);
|
|
204
|
+
console.log(` Dry run: ${options.dryRun ? 'Yes' : 'No'}`);
|
|
205
|
+
console.log(` Max concurrent: ${options.maxConcurrent}`);
|
|
206
|
+
|
|
207
|
+
// Start sync
|
|
208
|
+
const startTime = Date.now();
|
|
209
|
+
console.log(`\nš Starting batch sync...${options.dryRun ? ' (DRY RUN)' : ''}\n`);
|
|
210
|
+
|
|
211
|
+
let results;
|
|
212
|
+
|
|
213
|
+
// Sync based on type
|
|
214
|
+
if (options.type === 'all') {
|
|
215
|
+
results = await batchSyncAll({
|
|
216
|
+
basePath: this.basePath,
|
|
217
|
+
owner: repo.owner,
|
|
218
|
+
repo: repo.repo,
|
|
219
|
+
octokit,
|
|
220
|
+
dryRun: options.dryRun,
|
|
221
|
+
maxConcurrent: options.maxConcurrent,
|
|
222
|
+
onProgress: (type, current, total) => {
|
|
223
|
+
this.printProgress(type, current, total);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
} else if (options.type === 'prd') {
|
|
227
|
+
results = await batchSyncPRDs({
|
|
228
|
+
basePath: this.basePath,
|
|
229
|
+
owner: repo.owner,
|
|
230
|
+
repo: repo.repo,
|
|
231
|
+
octokit,
|
|
232
|
+
dryRun: options.dryRun,
|
|
233
|
+
maxConcurrent: options.maxConcurrent,
|
|
234
|
+
onProgress: (current, total) => {
|
|
235
|
+
this.printProgress('prd', current, total);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
} else if (options.type === 'epic') {
|
|
239
|
+
results = await batchSyncEpics({
|
|
240
|
+
basePath: this.basePath,
|
|
241
|
+
owner: repo.owner,
|
|
242
|
+
repo: repo.repo,
|
|
243
|
+
octokit,
|
|
244
|
+
dryRun: options.dryRun,
|
|
245
|
+
maxConcurrent: options.maxConcurrent,
|
|
246
|
+
onProgress: (current, total) => {
|
|
247
|
+
this.printProgress('epic', current, total);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
} else if (options.type === 'task') {
|
|
251
|
+
results = await batchSyncTasks({
|
|
252
|
+
basePath: this.basePath,
|
|
253
|
+
owner: repo.owner,
|
|
254
|
+
repo: repo.repo,
|
|
255
|
+
octokit,
|
|
256
|
+
dryRun: options.dryRun,
|
|
257
|
+
maxConcurrent: options.maxConcurrent,
|
|
258
|
+
onProgress: (current, total) => {
|
|
259
|
+
this.printProgress('task', current, total);
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const duration = Date.now() - startTime;
|
|
265
|
+
|
|
266
|
+
// Print summary
|
|
267
|
+
console.log('\n' + 'ā'.repeat(50));
|
|
268
|
+
console.log('š Batch Sync Summary');
|
|
269
|
+
console.log('ā'.repeat(50));
|
|
270
|
+
|
|
271
|
+
if (options.type === 'all') {
|
|
272
|
+
console.log(`\nš PRDs: ${results.prds.succeeded}/${results.prds.total} synced`);
|
|
273
|
+
console.log(`š Epics: ${results.epics.succeeded}/${results.epics.total} synced`);
|
|
274
|
+
console.log(`ā
Tasks: ${results.tasks.succeeded}/${results.tasks.total} synced`);
|
|
275
|
+
console.log(`\nš¦ Total: ${results.succeeded}/${results.total} items`);
|
|
276
|
+
|
|
277
|
+
if (results.failed > 0) {
|
|
278
|
+
console.log(`ā Failed: ${results.failed}`);
|
|
279
|
+
}
|
|
280
|
+
} else {
|
|
281
|
+
console.log(`\nā
Succeeded: ${results.succeeded}`);
|
|
282
|
+
console.log(`ā Failed: ${results.failed}`);
|
|
283
|
+
console.log(`š¦ Total: ${results.total}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log(`\nā±ļø Duration: ${this.formatDuration(duration)}`);
|
|
287
|
+
|
|
288
|
+
if (results.rateLimit) {
|
|
289
|
+
console.log(`š Rate limit: ${results.rateLimit.remaining} requests remaining`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Show errors if any
|
|
293
|
+
if (results.errors && results.errors.length > 0) {
|
|
294
|
+
console.log(`\nā ļø Errors (${results.errors.length}):`);
|
|
295
|
+
results.errors.slice(0, 5).forEach(err => {
|
|
296
|
+
console.log(` ⢠${err.item}: ${err.error}`);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
if (results.errors.length > 5) {
|
|
300
|
+
console.log(` ... and ${results.errors.length - 5} more`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
console.log('\n' + 'ā'.repeat(50));
|
|
305
|
+
|
|
306
|
+
// Exit code based on results
|
|
307
|
+
if (results.failed === 0) {
|
|
308
|
+
console.log('ā
All items synced successfully!');
|
|
309
|
+
process.exit(0);
|
|
310
|
+
} else if (results.succeeded > 0) {
|
|
311
|
+
console.log('ā ļø Partial sync completed (some errors)');
|
|
312
|
+
process.exit(0);
|
|
313
|
+
} else {
|
|
314
|
+
console.log('ā Sync failed');
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error('\nā Error:', error.message);
|
|
320
|
+
|
|
321
|
+
if (error.stack && process.env.DEBUG) {
|
|
322
|
+
console.error('\nStack trace:');
|
|
323
|
+
console.error(error.stack);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Main execution
|
|
332
|
+
if (require.main === module) {
|
|
333
|
+
const command = new SyncBatchCommand();
|
|
334
|
+
command.run(process.argv.slice(2));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
module.exports = SyncBatchCommand;
|