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.
@@ -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
- let prdName = args[0];
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
- const success = await this.createPrd(prdName);
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;