claude-autopm 1.28.0 → 1.30.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 +75 -15
- package/autopm/.claude/scripts/pm/analytics.js +425 -0
- package/autopm/.claude/scripts/pm/sync-batch.js +337 -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/conflict-history.js +316 -0
- package/lib/conflict-resolver.js +330 -0
- package/lib/dependency-analyzer.js +466 -0
- package/lib/filter-engine.js +414 -0
- package/lib/query-parser.js +322 -0
- package/lib/visual-diff.js +297 -0
- package/package.json +5 -4
|
@@ -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,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual Diff Renderer
|
|
3
|
+
*
|
|
4
|
+
* Renders ASCII side-by-side comparisons of text differences.
|
|
5
|
+
* Highlights conflicting sections and provides context lines.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const VisualDiff = require('./lib/visual-diff');
|
|
9
|
+
*
|
|
10
|
+
* const diff = new VisualDiff({
|
|
11
|
+
* columnWidth: 80
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* // Render side-by-side comparison
|
|
15
|
+
* const rendered = diff.sideBySide(localContent, remoteContent);
|
|
16
|
+
* console.log(rendered);
|
|
17
|
+
*
|
|
18
|
+
* // Highlight conflicts
|
|
19
|
+
* const highlighted = diff.highlightConflicts(text, conflicts);
|
|
20
|
+
*
|
|
21
|
+
* // Render with context
|
|
22
|
+
* const context = diff.renderContext(text, [5, 10], 3);
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
class VisualDiff {
|
|
26
|
+
/**
|
|
27
|
+
* Create a new VisualDiff instance
|
|
28
|
+
*
|
|
29
|
+
* @param {Object} options - Configuration options
|
|
30
|
+
* @param {number} options.columnWidth - Width of each column in side-by-side view (default: 60)
|
|
31
|
+
* @param {boolean} options.showLineNumbers - Show line numbers (default: true)
|
|
32
|
+
* @param {string} options.separator - Column separator (default: ' | ')
|
|
33
|
+
*/
|
|
34
|
+
constructor(options = {}) {
|
|
35
|
+
// Validate configuration
|
|
36
|
+
if (options.columnWidth !== undefined) {
|
|
37
|
+
if (typeof options.columnWidth !== 'number' || options.columnWidth <= 0) {
|
|
38
|
+
throw new Error('columnWidth must be a positive number');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.options = {
|
|
43
|
+
columnWidth: options.columnWidth || 60,
|
|
44
|
+
showLineNumbers: options.showLineNumbers !== false,
|
|
45
|
+
separator: options.separator || ' | '
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Render side-by-side comparison of two text versions
|
|
51
|
+
*
|
|
52
|
+
* @param {string} left - Left side content (local)
|
|
53
|
+
* @param {string} right - Right side content (remote)
|
|
54
|
+
* @param {Object} options - Rendering options
|
|
55
|
+
* @returns {string} ASCII side-by-side comparison
|
|
56
|
+
*/
|
|
57
|
+
sideBySide(left, right, options = {}) {
|
|
58
|
+
const leftLines = left.split('\n');
|
|
59
|
+
const rightLines = right.split('\n');
|
|
60
|
+
|
|
61
|
+
const maxLines = Math.max(leftLines.length, rightLines.length);
|
|
62
|
+
|
|
63
|
+
const output = [];
|
|
64
|
+
|
|
65
|
+
// Header
|
|
66
|
+
const colWidth = this.options.columnWidth;
|
|
67
|
+
const header = this._padRight('LOCAL', colWidth) +
|
|
68
|
+
this.options.separator +
|
|
69
|
+
this._padRight('REMOTE', colWidth);
|
|
70
|
+
output.push(header);
|
|
71
|
+
output.push('='.repeat(header.length));
|
|
72
|
+
|
|
73
|
+
// Content lines
|
|
74
|
+
for (let i = 0; i < maxLines; i++) {
|
|
75
|
+
const leftLine = leftLines[i] || '';
|
|
76
|
+
const rightLine = rightLines[i] || '';
|
|
77
|
+
|
|
78
|
+
const lineNum = this.options.showLineNumbers ? `${(i + 1).toString().padStart(4)} ` : '';
|
|
79
|
+
|
|
80
|
+
const leftContent = this._truncate(leftLine, colWidth - lineNum.length);
|
|
81
|
+
const rightContent = this._truncate(rightLine, colWidth - lineNum.length);
|
|
82
|
+
|
|
83
|
+
// Mark differences
|
|
84
|
+
const marker = leftLine !== rightLine ? '*' : ' ';
|
|
85
|
+
|
|
86
|
+
const line = marker + lineNum + this._padRight(leftContent, colWidth - lineNum.length - 1) +
|
|
87
|
+
this.options.separator +
|
|
88
|
+
marker + lineNum + this._padRight(rightContent, colWidth - lineNum.length - 1);
|
|
89
|
+
|
|
90
|
+
output.push(line);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return output.join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Highlight conflicts in text with conflict markers
|
|
98
|
+
*
|
|
99
|
+
* @param {string} text - Original text
|
|
100
|
+
* @param {Array<Object>} conflicts - Array of conflict objects
|
|
101
|
+
* @returns {string} Text with conflict markers inserted
|
|
102
|
+
*/
|
|
103
|
+
highlightConflicts(text, conflicts) {
|
|
104
|
+
const lines = text.split('\n');
|
|
105
|
+
|
|
106
|
+
// Sort conflicts by line number (ascending)
|
|
107
|
+
const sortedConflicts = [...conflicts].sort((a, b) => a.line - b.line);
|
|
108
|
+
|
|
109
|
+
// Build result in a single pass
|
|
110
|
+
const result = [];
|
|
111
|
+
let conflictIdx = 0;
|
|
112
|
+
for (let i = 0; i < lines.length; i++) {
|
|
113
|
+
// Check if current line matches the next conflict
|
|
114
|
+
if (
|
|
115
|
+
conflictIdx < sortedConflicts.length &&
|
|
116
|
+
sortedConflicts[conflictIdx].line - 1 === i
|
|
117
|
+
) {
|
|
118
|
+
const conflict = sortedConflicts[conflictIdx];
|
|
119
|
+
result.push('<<<<<<< LOCAL');
|
|
120
|
+
result.push(conflict.localContent);
|
|
121
|
+
result.push('=======');
|
|
122
|
+
result.push(conflict.remoteContent);
|
|
123
|
+
result.push('>>>>>>> REMOTE');
|
|
124
|
+
conflictIdx++;
|
|
125
|
+
} else {
|
|
126
|
+
result.push(lines[i]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result.join('\n');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Render context lines around specified line numbers
|
|
135
|
+
*
|
|
136
|
+
* @param {string} text - Full text content
|
|
137
|
+
* @param {Array<number>} lineNumbers - Line numbers to show context for
|
|
138
|
+
* @param {number} contextLines - Number of context lines before and after (default: 3)
|
|
139
|
+
* @returns {string} Text with only context lines
|
|
140
|
+
*/
|
|
141
|
+
renderContext(text, lineNumbers, contextLines = 3) {
|
|
142
|
+
const lines = text.split('\n');
|
|
143
|
+
const lineSet = new Set();
|
|
144
|
+
|
|
145
|
+
// Add context lines for each specified line number
|
|
146
|
+
for (const lineNum of lineNumbers) {
|
|
147
|
+
const lineIndex = lineNum - 1; // Convert to 0-based
|
|
148
|
+
|
|
149
|
+
// Add lines in context range
|
|
150
|
+
const start = Math.max(0, lineIndex - contextLines);
|
|
151
|
+
const end = Math.min(lines.length - 1, lineIndex + contextLines);
|
|
152
|
+
|
|
153
|
+
for (let i = start; i <= end; i++) {
|
|
154
|
+
lineSet.add(i);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Convert to sorted array
|
|
159
|
+
const includedLines = Array.from(lineSet).sort((a, b) => a - b);
|
|
160
|
+
|
|
161
|
+
// Build output with ellipsis for gaps
|
|
162
|
+
const output = [];
|
|
163
|
+
let lastLine = -2;
|
|
164
|
+
|
|
165
|
+
for (const lineIndex of includedLines) {
|
|
166
|
+
// Add ellipsis if there's a gap
|
|
167
|
+
if (lineIndex > lastLine + 1) {
|
|
168
|
+
output.push('...');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Add the line with line number
|
|
172
|
+
const lineNum = (lineIndex + 1).toString().padStart(4);
|
|
173
|
+
output.push(`${lineNum} | ${lines[lineIndex]}`);
|
|
174
|
+
|
|
175
|
+
lastLine = lineIndex;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return output.join('\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Calculate diff statistics
|
|
183
|
+
*
|
|
184
|
+
* @param {string} left - Left side content
|
|
185
|
+
* @param {string} right - Right side content
|
|
186
|
+
* @returns {Object} Diff statistics
|
|
187
|
+
*/
|
|
188
|
+
getStats(left, right) {
|
|
189
|
+
const leftLines = left.split('\n');
|
|
190
|
+
const rightLines = right.split('\n');
|
|
191
|
+
|
|
192
|
+
let added = 0;
|
|
193
|
+
let removed = 0;
|
|
194
|
+
let modified = 0;
|
|
195
|
+
let unchanged = 0;
|
|
196
|
+
|
|
197
|
+
const maxLines = Math.max(leftLines.length, rightLines.length);
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < maxLines; i++) {
|
|
200
|
+
const leftLine = leftLines[i];
|
|
201
|
+
const rightLine = rightLines[i];
|
|
202
|
+
|
|
203
|
+
if (leftLine === undefined && rightLine !== undefined) {
|
|
204
|
+
added++;
|
|
205
|
+
} else if (leftLine !== undefined && rightLine === undefined) {
|
|
206
|
+
removed++;
|
|
207
|
+
} else if (leftLine !== rightLine) {
|
|
208
|
+
modified++;
|
|
209
|
+
} else {
|
|
210
|
+
unchanged++;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
added,
|
|
216
|
+
removed,
|
|
217
|
+
modified,
|
|
218
|
+
unchanged,
|
|
219
|
+
total: maxLines
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Truncate text to fit within column width
|
|
225
|
+
*
|
|
226
|
+
* @private
|
|
227
|
+
* @param {string} text - Text to truncate
|
|
228
|
+
* @param {number} maxWidth - Maximum width
|
|
229
|
+
* @returns {string} Truncated text
|
|
230
|
+
*/
|
|
231
|
+
_truncate(text, maxWidth) {
|
|
232
|
+
if (text.length <= maxWidth) {
|
|
233
|
+
return text;
|
|
234
|
+
}
|
|
235
|
+
// If maxWidth is less than 3, we can't fit the ellipsis, so just truncate to maxWidth
|
|
236
|
+
if (maxWidth < 3) {
|
|
237
|
+
return text.substring(0, maxWidth);
|
|
238
|
+
}
|
|
239
|
+
return text.substring(0, maxWidth - 3) + '...';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Pad text to the right with spaces
|
|
244
|
+
*
|
|
245
|
+
* @private
|
|
246
|
+
* @param {string} text - Text to pad
|
|
247
|
+
* @param {number} width - Target width
|
|
248
|
+
* @returns {string} Padded text
|
|
249
|
+
*/
|
|
250
|
+
_padRight(text, width) {
|
|
251
|
+
if (text.length >= width) {
|
|
252
|
+
return text.substring(0, width);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return text + ' '.repeat(width - text.length);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Render unified diff format (similar to git diff)
|
|
260
|
+
*
|
|
261
|
+
* @param {string} left - Left side content
|
|
262
|
+
* @param {string} right - Right side content
|
|
263
|
+
* @param {Object} options - Rendering options
|
|
264
|
+
* @returns {string} Unified diff output
|
|
265
|
+
*/
|
|
266
|
+
unified(left, right, options = {}) {
|
|
267
|
+
const leftLines = left.split('\n');
|
|
268
|
+
const rightLines = right.split('\n');
|
|
269
|
+
|
|
270
|
+
const output = [];
|
|
271
|
+
output.push('--- LOCAL');
|
|
272
|
+
output.push('+++ REMOTE');
|
|
273
|
+
output.push(`@@ -1,${leftLines.length} +1,${rightLines.length} @@`);
|
|
274
|
+
|
|
275
|
+
const maxLines = Math.max(leftLines.length, rightLines.length);
|
|
276
|
+
|
|
277
|
+
for (let i = 0; i < maxLines; i++) {
|
|
278
|
+
const leftLine = leftLines[i];
|
|
279
|
+
const rightLine = rightLines[i];
|
|
280
|
+
|
|
281
|
+
if (leftLine === rightLine) {
|
|
282
|
+
output.push(` ${leftLine || ''}`);
|
|
283
|
+
} else if (leftLine === undefined) {
|
|
284
|
+
output.push(`+${rightLine}`);
|
|
285
|
+
} else if (rightLine === undefined) {
|
|
286
|
+
output.push(`-${leftLine}`);
|
|
287
|
+
} else {
|
|
288
|
+
output.push(`-${leftLine}`);
|
|
289
|
+
output.push(`+${rightLine}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return output.join('\n');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = VisualDiff;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-autopm",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.30.0",
|
|
4
4
|
"description": "Autonomous Project Management Framework for Claude Code - Advanced AI-powered development automation",
|
|
5
5
|
"main": "bin/autopm.js",
|
|
6
6
|
"bin": {
|
|
@@ -114,6 +114,7 @@
|
|
|
114
114
|
"README.md"
|
|
115
115
|
],
|
|
116
116
|
"dependencies": {
|
|
117
|
+
"@octokit/rest": "^22.0.0",
|
|
117
118
|
"azure-devops-node-api": "^15.1.1",
|
|
118
119
|
"chalk": "^5.3.0",
|
|
119
120
|
"dotenv": "^16.6.1",
|
|
@@ -128,9 +129,9 @@
|
|
|
128
129
|
"ora": "^5.4.1",
|
|
129
130
|
"simple-git": "^3.20.0",
|
|
130
131
|
"table": "^6.8.1",
|
|
131
|
-
"which": "^
|
|
132
|
+
"which": "^5.0.0",
|
|
132
133
|
"yaml": "^2.8.1",
|
|
133
|
-
"yargs": "^
|
|
134
|
+
"yargs": "^18.0.0"
|
|
134
135
|
},
|
|
135
136
|
"devDependencies": {
|
|
136
137
|
"@jest/globals": "^30.1.2",
|
|
@@ -151,7 +152,7 @@
|
|
|
151
152
|
"sinon": "^21.0.0"
|
|
152
153
|
},
|
|
153
154
|
"optionalDependencies": {
|
|
154
|
-
"@playwright/mcp": "^0.0.
|
|
155
|
+
"@playwright/mcp": "^0.0.41",
|
|
155
156
|
"@upstash/context7-mcp": "^1.0.0"
|
|
156
157
|
},
|
|
157
158
|
"publishConfig": {
|