claude-autopm 1.29.0 → 1.30.1

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.
@@ -0,0 +1,322 @@
1
+ /**
2
+ * QueryParser - Parse command-line filter arguments into structured queries
3
+ *
4
+ * Converts CLI-style filter arguments into structured query objects for
5
+ * filtering PRDs, Epics, and Tasks.
6
+ *
7
+ * @example Basic Usage
8
+ * ```javascript
9
+ * const QueryParser = require('./lib/query-parser');
10
+ * const parser = new QueryParser();
11
+ *
12
+ * // Parse CLI arguments
13
+ * const query = parser.parse(['--status', 'active', '--priority', 'high']);
14
+ * // Returns: { status: 'active', priority: 'high' }
15
+ *
16
+ * // Validate query
17
+ * const validation = parser.validate(query);
18
+ * // Returns: { valid: true, errors: [] }
19
+ * ```
20
+ *
21
+ * @example Supported Filters
22
+ * ```javascript
23
+ * // Status filter
24
+ * parser.parse(['--status', 'active']);
25
+ *
26
+ * // Priority filter
27
+ * parser.parse(['--priority', 'P0']);
28
+ *
29
+ * // Date range filter
30
+ * parser.parse(['--created-after', '2025-01-01', '--created-before', '2025-12-31']);
31
+ *
32
+ * // Full-text search
33
+ * parser.parse(['--search', 'authentication API']);
34
+ *
35
+ * // Combined filters
36
+ * parser.parse([
37
+ * '--status', 'active',
38
+ * '--priority', 'high',
39
+ * '--created-after', '2025-01-01',
40
+ * '--search', 'OAuth2'
41
+ * ]);
42
+ * ```
43
+ *
44
+ * @module QueryParser
45
+ * @version 1.0.0
46
+ * @since v1.28.0
47
+ */
48
+
49
+ class QueryParser {
50
+ constructor() {
51
+ /**
52
+ * Supported filter names
53
+ * @type {string[]}
54
+ */
55
+ this.supportedFilters = [
56
+ 'status',
57
+ 'priority',
58
+ 'epic',
59
+ 'author',
60
+ 'assignee',
61
+ 'created-after',
62
+ 'created-before',
63
+ 'updated-after',
64
+ 'updated-before',
65
+ 'search'
66
+ ];
67
+
68
+ /**
69
+ * Valid status values
70
+ * @type {string[]}
71
+ */
72
+ this.validStatuses = [
73
+ 'draft',
74
+ 'active',
75
+ 'in_progress',
76
+ 'completed',
77
+ 'blocked',
78
+ 'archived'
79
+ ];
80
+
81
+ /**
82
+ * Valid priority values
83
+ * @type {string[]}
84
+ */
85
+ this.validPriorities = [
86
+ 'P0', 'P1', 'P2', 'P3',
87
+ 'p0', 'p1', 'p2', 'p3',
88
+ 'high', 'medium', 'low',
89
+ 'High', 'Medium', 'Low',
90
+ 'HIGH', 'MEDIUM', 'LOW'
91
+ ];
92
+
93
+ /**
94
+ * Date filter fields
95
+ * @type {string[]}
96
+ */
97
+ this.dateFilters = [
98
+ 'created-after',
99
+ 'created-before',
100
+ 'updated-after',
101
+ 'updated-before'
102
+ ];
103
+ }
104
+
105
+ /**
106
+ * Parse command-line filter arguments into structured query
107
+ *
108
+ * @param {string[]} args - Command-line arguments (e.g., ['--status', 'active'])
109
+ * @returns {Object} - Parsed query object
110
+ *
111
+ * @example
112
+ * const query = parser.parse(['--status', 'active', '--priority', 'high']);
113
+ * // Returns: { status: 'active', priority: 'high' }
114
+ */
115
+ parse(args) {
116
+ const query = {};
117
+
118
+ if (!Array.isArray(args) || args.length === 0) {
119
+ return query;
120
+ }
121
+
122
+ for (let i = 0; i < args.length; i++) {
123
+ const arg = args[i];
124
+
125
+ // Check if this is a filter flag (starts with --)
126
+ if (!arg.startsWith('--')) {
127
+ continue;
128
+ }
129
+
130
+ // Extract filter name (remove -- prefix)
131
+ const filterName = arg.substring(2);
132
+
133
+ // Check if this is a supported filter
134
+ if (!this.supportedFilters.includes(filterName)) {
135
+ continue;
136
+ }
137
+
138
+ // Get the next argument as the value
139
+ const value = args[i + 1];
140
+
141
+ // Skip if no value or value is another flag
142
+ if (!value || value.startsWith('--')) {
143
+ continue;
144
+ }
145
+
146
+ // Trim whitespace
147
+ const trimmedValue = value.trim();
148
+
149
+ // Skip empty values
150
+ if (trimmedValue === '') {
151
+ continue;
152
+ }
153
+
154
+ // Add to query
155
+ query[filterName] = trimmedValue;
156
+
157
+ // Skip the next argument (we just consumed it as a value)
158
+ i++;
159
+ }
160
+
161
+ return query;
162
+ }
163
+
164
+ /**
165
+ * Validate query object
166
+ *
167
+ * Checks for valid date formats and other constraints.
168
+ *
169
+ * @param {Object} query - Query object to validate
170
+ * @returns {Object} - { valid: boolean, errors: string[] }
171
+ *
172
+ * @example
173
+ * const result = parser.validate({ status: 'active', 'created-after': '2025-01-01' });
174
+ * // Returns: { valid: true, errors: [] }
175
+ *
176
+ * const result2 = parser.validate({ 'created-after': 'invalid-date' });
177
+ * // Returns: { valid: false, errors: ['Invalid date format...'] }
178
+ */
179
+ validate(query) {
180
+ const errors = [];
181
+
182
+ if (!query || typeof query !== 'object') {
183
+ return { valid: true, errors: [] };
184
+ }
185
+
186
+ // Validate date filters
187
+ for (const dateFilter of this.dateFilters) {
188
+ if (query[dateFilter]) {
189
+ const value = query[dateFilter];
190
+
191
+ // Check YYYY-MM-DD format
192
+ if (!this.isValidDateFormat(value)) {
193
+ errors.push(`Invalid date format for ${dateFilter}: ${value} (expected YYYY-MM-DD)`);
194
+ }
195
+ }
196
+ }
197
+
198
+ return {
199
+ valid: errors.length === 0,
200
+ errors
201
+ };
202
+ }
203
+
204
+ /**
205
+ * Check if a string is a valid date in YYYY-MM-DD format
206
+ *
207
+ * @param {string} dateString - Date string to validate
208
+ * @returns {boolean} - True if valid
209
+ * @private
210
+ */
211
+ isValidDateFormat(dateString) {
212
+ // Check format: YYYY-MM-DD
213
+ const regex = /^\d{4}-\d{2}-\d{2}$/;
214
+ if (!regex.test(dateString)) {
215
+ return false;
216
+ }
217
+
218
+ // Parse and validate actual date
219
+ const parts = dateString.split('-');
220
+ const year = parseInt(parts[0], 10);
221
+ const month = parseInt(parts[1], 10);
222
+ const day = parseInt(parts[2], 10);
223
+
224
+ // Check ranges
225
+ if (month < 1 || month > 12) {
226
+ return false;
227
+ }
228
+
229
+ if (day < 1 || day > 31) {
230
+ return false;
231
+ }
232
+
233
+ // Create date and verify it matches input
234
+ // (this catches invalid dates like 2025-02-31)
235
+ const date = new Date(year, month - 1, day);
236
+ return (
237
+ date.getFullYear() === year &&
238
+ date.getMonth() === month - 1 &&
239
+ date.getDate() === day
240
+ );
241
+ }
242
+
243
+ /**
244
+ * Get list of supported filters
245
+ *
246
+ * @returns {string[]} - Array of supported filter names
247
+ *
248
+ * @example
249
+ * const filters = parser.getSupportedFilters();
250
+ * // Returns: ['status', 'priority', 'epic', ...]
251
+ */
252
+ getSupportedFilters() {
253
+ return [...this.supportedFilters];
254
+ }
255
+
256
+ /**
257
+ * Get help text for filters
258
+ *
259
+ * Returns formatted help text describing all available filters
260
+ * with examples.
261
+ *
262
+ * @returns {string} - Formatted help text
263
+ *
264
+ * @example
265
+ * console.log(parser.getFilterHelp());
266
+ * // Outputs:
267
+ * // Available Filters:
268
+ * // --status <value> Filter by status (draft, active, ...)
269
+ * // --priority <value> Filter by priority (P0, P1, high, ...)
270
+ * // ...
271
+ */
272
+ getFilterHelp() {
273
+ return `
274
+ Available Filters:
275
+
276
+ --status <value> Filter by status
277
+ Values: draft, active, in_progress, completed, blocked, archived
278
+ Example: --status active
279
+
280
+ --priority <value> Filter by priority
281
+ Values: P0, P1, P2, P3, high, medium, low
282
+ Example: --priority high
283
+
284
+ --epic <id> Filter by epic ID
285
+ Example: --epic epic-001
286
+
287
+ --author <name> Filter by author name
288
+ Example: --author john
289
+
290
+ --assignee <name> Filter by assignee name
291
+ Example: --assignee jane
292
+
293
+ --created-after <date> Created after date (YYYY-MM-DD)
294
+ Example: --created-after 2025-01-01
295
+
296
+ --created-before <date> Created before date (YYYY-MM-DD)
297
+ Example: --created-before 2025-12-31
298
+
299
+ --updated-after <date> Updated after date (YYYY-MM-DD)
300
+ Example: --updated-after 2025-06-01
301
+
302
+ --updated-before <date> Updated before date (YYYY-MM-DD)
303
+ Example: --updated-before 2025-06-30
304
+
305
+ --search <text> Full-text search in content and frontmatter
306
+ Example: --search "authentication API"
307
+
308
+ Examples:
309
+
310
+ # Filter by status and priority
311
+ --status active --priority high
312
+
313
+ # Filter by date range
314
+ --created-after 2025-01-01 --created-before 2025-12-31
315
+
316
+ # Combine filters
317
+ --status active --priority P0 --epic epic-001 --search "OAuth2"
318
+ `.trim();
319
+ }
320
+ }
321
+
322
+ module.exports = QueryParser;
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Template Engine - Pure Node.js Implementation
3
+ *
4
+ * NO external dependencies - uses only built-in Node.js modules
5
+ *
6
+ * Features:
7
+ * - Variable substitution: {{variable}}
8
+ * - Conditionals: {{#if variable}}...{{/if}}
9
+ * - Loops: {{#each items}}...{{/each}}
10
+ * - Auto-generated variables: id, timestamp, author, date
11
+ * - Template discovery: user custom overrides built-in
12
+ *
13
+ * @example Basic Usage
14
+ * ```javascript
15
+ * const TemplateEngine = require('./lib/template-engine');
16
+ * const engine = new TemplateEngine();
17
+ *
18
+ * // Find template
19
+ * const templatePath = engine.findTemplate('prds', 'api-feature');
20
+ *
21
+ * // Render with variables
22
+ * const rendered = engine.renderFile(templatePath, {
23
+ * title: 'User Authentication API',
24
+ * priority: 'P0',
25
+ * problem: 'Users cannot login securely'
26
+ * });
27
+ * ```
28
+ *
29
+ * @example Advanced Features
30
+ * ```javascript
31
+ * // Template with conditionals and loops
32
+ * const template = `
33
+ * # {{title}}
34
+ *
35
+ * {{#if description}}
36
+ * ## Description
37
+ * {{description}}
38
+ * {{/if}}
39
+ *
40
+ * ## Features
41
+ * {{#each features}}
42
+ * - {{this}}
43
+ * {{/each}}
44
+ * `;
45
+ *
46
+ * const result = engine.render(template, {
47
+ * title: 'My Feature',
48
+ * description: 'Feature description',
49
+ * features: ['Auth', 'API', 'UI']
50
+ * });
51
+ * ```
52
+ *
53
+ * @module TemplateEngine
54
+ * @version 1.0.0
55
+ * @since v1.28.0
56
+ */
57
+
58
+ const fs = require('fs');
59
+ const path = require('path');
60
+
61
+ class TemplateEngine {
62
+ constructor(builtInDir, userDir) {
63
+ // For testing, allow custom built-in directory and user directory
64
+ // In production, use autopm/.claude/templates and .claude/templates
65
+ this.builtInDir = builtInDir || path.join(__dirname, '..', 'autopm', '.claude', 'templates');
66
+ this.userDir = userDir || path.join('.claude', 'templates');
67
+ }
68
+
69
+ /**
70
+ * Find template by name
71
+ * Priority: user templates > built-in templates
72
+ *
73
+ * @param {string} type - Template type (prds/epics/tasks)
74
+ * @param {string} name - Template name (without .md extension)
75
+ * @returns {string|null} - Path to template or null if not found
76
+ */
77
+ findTemplate(type, name) {
78
+ const userPath = path.resolve(this.userDir, type, `${name}.md`);
79
+ const builtInPath = path.resolve(this.builtInDir, type, `${name}.md`);
80
+
81
+ if (fs.existsSync(userPath)) return userPath;
82
+ if (fs.existsSync(builtInPath)) return builtInPath;
83
+ return null;
84
+ }
85
+
86
+ /**
87
+ * List all available templates of a given type
88
+ *
89
+ * @param {string} type - Template type (prds/epics/tasks)
90
+ * @returns {Array<{name: string, custom: boolean}>}
91
+ */
92
+ listTemplates(type) {
93
+ const templates = [];
94
+
95
+ // Built-in templates
96
+ const builtInPath = path.join(this.builtInDir, type);
97
+ if (fs.existsSync(builtInPath)) {
98
+ const files = fs.readdirSync(builtInPath)
99
+ .filter(f => f.endsWith('.md'))
100
+ .map(f => ({ name: f.replace('.md', ''), custom: false }));
101
+ templates.push(...files);
102
+ }
103
+
104
+ // User custom templates
105
+ const userPath = path.join(this.userDir, type);
106
+ if (fs.existsSync(userPath)) {
107
+ const files = fs.readdirSync(userPath)
108
+ .filter(f => f.endsWith('.md'))
109
+ .map(f => ({ name: f.replace('.md', ''), custom: true }));
110
+ templates.push(...files);
111
+ }
112
+
113
+ return templates;
114
+ }
115
+
116
+ /**
117
+ * Generate auto variables
118
+ *
119
+ * @returns {Object} - Auto-generated variables
120
+ */
121
+ generateAutoVariables() {
122
+ const now = new Date();
123
+ return {
124
+ id: '', // Will be set by generateId()
125
+ timestamp: now.toISOString(),
126
+ date: now.toISOString().split('T')[0],
127
+ author: process.env.USER || process.env.USERNAME || 'unknown'
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Generate sequential ID
133
+ *
134
+ * @param {string} prefix - ID prefix (prd/epic/task)
135
+ * @param {string} directory - Directory to scan for existing IDs
136
+ * @returns {string} - Next sequential ID
137
+ */
138
+ generateId(prefix, directory) {
139
+ if (!fs.existsSync(directory)) {
140
+ return `${prefix}-001`;
141
+ }
142
+
143
+ const files = fs.readdirSync(directory)
144
+ .filter(f => f.startsWith(`${prefix}-`) && f.endsWith('.md'));
145
+
146
+ if (files.length === 0) {
147
+ return `${prefix}-001`;
148
+ }
149
+
150
+ // Extract numbers and find max
151
+ const numbers = files
152
+ .map(f => {
153
+ const match = f.match(new RegExp(`${prefix}-(\\d+)\\.md`));
154
+ return match ? parseInt(match[1], 10) : 0;
155
+ })
156
+ .filter(n => !isNaN(n));
157
+
158
+ const maxNum = Math.max(...numbers);
159
+ const nextNum = maxNum + 1;
160
+
161
+ return `${prefix}-${String(nextNum).padStart(3, '0')}`;
162
+ }
163
+
164
+ /**
165
+ * Render template with variables
166
+ *
167
+ * @param {string} template - Template string
168
+ * @param {Object} variables - Variables to substitute
169
+ * @returns {string} - Rendered template
170
+ */
171
+ render(template, variables) {
172
+ // Auto-generate variables
173
+ const autoVars = this.generateAutoVariables();
174
+
175
+ // Generate ID if not provided
176
+ if (!variables.id && !autoVars.id) {
177
+ // Try to infer type and directory from variables
178
+ const type = variables.type || 'item';
179
+ const directory = `.claude/${type}s`;
180
+ autoVars.id = this.generateId(type, directory);
181
+ }
182
+
183
+ const allVars = { ...autoVars, ...variables };
184
+
185
+ let content = template;
186
+
187
+ // Process loops first ({{#each}}...{{/each}})
188
+ content = this.processLoops(content, allVars);
189
+
190
+ // Process conditionals ({{#if}}...{{/if}})
191
+ content = this.processConditionals(content, allVars);
192
+
193
+ // Simple variable substitution ({{variable}})
194
+ for (const [key, value] of Object.entries(allVars)) {
195
+ const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
196
+ content = content.replace(regex, value || '');
197
+ }
198
+
199
+ // Replace any remaining {{variables}} that weren't in allVars with empty string
200
+ content = content.replace(/\{\{(\w+)\}\}/g, '');
201
+
202
+ return content;
203
+ }
204
+
205
+ /**
206
+ * Render template from file
207
+ *
208
+ * @param {string} templatePath - Path to template file
209
+ * @param {Object} variables - Variables to substitute
210
+ * @returns {string} - Rendered template
211
+ */
212
+ renderFile(templatePath, variables) {
213
+ const template = fs.readFileSync(templatePath, 'utf8');
214
+ return this.render(template, variables);
215
+ }
216
+
217
+ /**
218
+ * Process conditionals {{#if var}}...{{/if}}
219
+ *
220
+ * @param {string} content - Template content
221
+ * @param {Object} vars - Variables
222
+ * @returns {string} - Processed content
223
+ */
224
+ processConditionals(content, vars) {
225
+ // Match {{#if variable}}...{{/if}} - process from inside out for nested conditionals
226
+ const ifRegex = /\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/;
227
+
228
+ let result = content;
229
+ let changed = true;
230
+ let iterations = 0;
231
+ const maxIterations = 100;
232
+
233
+ // Keep processing until no more conditionals found (handles nesting)
234
+ while (changed && iterations < maxIterations) {
235
+ iterations++;
236
+ changed = false;
237
+
238
+ const match = result.match(ifRegex);
239
+ if (match) {
240
+ const [fullMatch, varName, innerContent] = match;
241
+ const varValue = vars[varName];
242
+
243
+ // If variable is truthy, keep inner content; otherwise remove
244
+ const replacement = varValue ? innerContent : '';
245
+ result = result.replace(fullMatch, replacement);
246
+ changed = true;
247
+ }
248
+ }
249
+
250
+ return result;
251
+ }
252
+
253
+ /**
254
+ * Process loops {{#each items}}...{{/each}}
255
+ *
256
+ * @param {string} content - Template content
257
+ * @param {Object} vars - Variables
258
+ * @returns {string} - Processed content
259
+ */
260
+ processLoops(content, vars) {
261
+ // Match {{#each variable}}...{{/each}}
262
+ // Limit inner content to 10,000 characters to prevent ReDoS
263
+ const eachRegex = /\{\{#each\s+(\w+)\}\}([\s\S]{0,10000}?)\{\{\/each\}\}/g;
264
+
265
+ let result = content;
266
+ let match;
267
+
268
+ // Safety counter
269
+ let iterations = 0;
270
+ const maxIterations = 100;
271
+
272
+ while ((match = eachRegex.exec(content)) !== null && iterations < maxIterations) {
273
+ iterations++;
274
+ const [fullMatch, varName, template] = match;
275
+ const items = vars[varName];
276
+
277
+ if (!Array.isArray(items)) {
278
+ // Not an array, remove the loop
279
+ result = result.replace(fullMatch, '');
280
+ continue;
281
+ }
282
+
283
+ // Render each item
284
+ let rendered = '';
285
+ for (const item of items) {
286
+ if (typeof item === 'object' && item !== null) {
287
+ // Object: replace {{property}} with object properties
288
+ let itemRendered = template;
289
+ for (const [key, value] of Object.entries(item)) {
290
+ const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
291
+ itemRendered = itemRendered.replace(regex, value || '');
292
+ }
293
+ rendered += itemRendered;
294
+ } else {
295
+ // Primitive: replace {{this}} with the value
296
+ rendered += template.replace(/\{\{this\}\}/g, item);
297
+ }
298
+ }
299
+
300
+ result = result.replace(fullMatch, rendered);
301
+ }
302
+
303
+ return result;
304
+ }
305
+
306
+ /**
307
+ * Validate template
308
+ *
309
+ * @param {string} template - Template string
310
+ * @returns {Object} - {valid: boolean, errors: string[]}
311
+ */
312
+ validate(template) {
313
+ const errors = [];
314
+
315
+ // Check frontmatter
316
+ if (!template.startsWith('---')) {
317
+ errors.push('Missing frontmatter');
318
+ }
319
+
320
+ // Check required variables
321
+ const requiredVars = ['id', 'title', 'type'];
322
+ for (const varName of requiredVars) {
323
+ if (!template.includes(`{{${varName}}}`)) {
324
+ errors.push(`Missing required variable: {{${varName}}}`);
325
+ }
326
+ }
327
+
328
+ return {
329
+ valid: errors.length === 0,
330
+ errors
331
+ };
332
+ }
333
+
334
+ /**
335
+ * Ensure template directory exists
336
+ *
337
+ * @param {string} type - Template type (prds/epics/tasks)
338
+ */
339
+ ensureTemplateDir(type) {
340
+ const dir = path.join(this.userDir, type);
341
+ if (!fs.existsSync(dir)) {
342
+ fs.mkdirSync(dir, { recursive: true });
343
+ }
344
+ }
345
+ }
346
+
347
+ module.exports = TemplateEngine;