@lumenflow/cli 1.5.0 → 2.0.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.
Files changed (41) hide show
  1. package/dist/__tests__/backlog-prune.test.js +478 -0
  2. package/dist/__tests__/deps-operations.test.js +206 -0
  3. package/dist/__tests__/file-operations.test.js +906 -0
  4. package/dist/__tests__/git-operations.test.js +668 -0
  5. package/dist/__tests__/guards-validation.test.js +416 -0
  6. package/dist/__tests__/init-plan.test.js +340 -0
  7. package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
  8. package/dist/__tests__/metrics-cli.test.js +619 -0
  9. package/dist/__tests__/rotate-progress.test.js +127 -0
  10. package/dist/__tests__/session-coordinator.test.js +109 -0
  11. package/dist/__tests__/state-bootstrap.test.js +432 -0
  12. package/dist/__tests__/trace-gen.test.js +115 -0
  13. package/dist/backlog-prune.js +299 -0
  14. package/dist/deps-add.js +215 -0
  15. package/dist/deps-remove.js +94 -0
  16. package/dist/file-delete.js +236 -0
  17. package/dist/file-edit.js +247 -0
  18. package/dist/file-read.js +197 -0
  19. package/dist/file-write.js +220 -0
  20. package/dist/git-branch.js +187 -0
  21. package/dist/git-diff.js +177 -0
  22. package/dist/git-log.js +230 -0
  23. package/dist/git-status.js +208 -0
  24. package/dist/guard-locked.js +169 -0
  25. package/dist/guard-main-branch.js +202 -0
  26. package/dist/guard-worktree-commit.js +160 -0
  27. package/dist/init-plan.js +337 -0
  28. package/dist/lumenflow-upgrade.js +178 -0
  29. package/dist/metrics-cli.js +433 -0
  30. package/dist/rotate-progress.js +247 -0
  31. package/dist/session-coordinator.js +300 -0
  32. package/dist/state-bootstrap.js +307 -0
  33. package/dist/trace-gen.js +331 -0
  34. package/dist/validate-agent-skills.js +218 -0
  35. package/dist/validate-agent-sync.js +148 -0
  36. package/dist/validate-backlog-sync.js +152 -0
  37. package/dist/validate-skills-spec.js +206 -0
  38. package/dist/validate.js +230 -0
  39. package/dist/wu-recover.js +329 -0
  40. package/dist/wu-status.js +188 -0
  41. package/package.json +37 -7
@@ -0,0 +1,478 @@
1
+ /**
2
+ * @file backlog-prune.test.ts
3
+ * @description Tests for backlog-prune CLI command (WU-1106)
4
+ *
5
+ * backlog-prune maintains backlog hygiene by:
6
+ * - Auto-tagging stale WUs (in_progress/ready too long without activity)
7
+ * - Archiving old completed WUs (done for > N days)
8
+ *
9
+ * TDD: RED phase - these tests define expected behavior before implementation
10
+ */
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
+ import { existsSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { calculateStaleDays, isWuStale, isWuArchivable, categorizeWus, parseBacklogPruneArgs, BACKLOG_PRUNE_DEFAULTS, loadAllWus, tagStaleWu, printHelp, } from '../backlog-prune.js';
16
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
17
+ describe('backlog-prune CLI', () => {
18
+ describe('source file existence', () => {
19
+ it('should have the CLI source file', () => {
20
+ const srcPath = join(__dirname, '../backlog-prune.ts');
21
+ expect(existsSync(srcPath)).toBe(true);
22
+ });
23
+ it('should be buildable (dist file exists after build)', () => {
24
+ // This test verifies that tsc compiled the file successfully
25
+ const distPath = join(__dirname, '../../dist/backlog-prune.js');
26
+ expect(existsSync(distPath)).toBe(true);
27
+ });
28
+ });
29
+ describe('BACKLOG_PRUNE_DEFAULTS', () => {
30
+ it('should have default stale days for in_progress WUs', () => {
31
+ expect(BACKLOG_PRUNE_DEFAULTS.staleDaysInProgress).toBeTypeOf('number');
32
+ expect(BACKLOG_PRUNE_DEFAULTS.staleDaysInProgress).toBeGreaterThan(0);
33
+ });
34
+ it('should have default stale days for ready WUs', () => {
35
+ expect(BACKLOG_PRUNE_DEFAULTS.staleDaysReady).toBeTypeOf('number');
36
+ expect(BACKLOG_PRUNE_DEFAULTS.staleDaysReady).toBeGreaterThan(0);
37
+ });
38
+ it('should have default archive days for done WUs', () => {
39
+ expect(BACKLOG_PRUNE_DEFAULTS.archiveDaysDone).toBeTypeOf('number');
40
+ expect(BACKLOG_PRUNE_DEFAULTS.archiveDaysDone).toBeGreaterThan(0);
41
+ });
42
+ });
43
+ describe('parseBacklogPruneArgs', () => {
44
+ it('should parse --dry-run flag (default)', () => {
45
+ const args = parseBacklogPruneArgs(['node', 'backlog-prune']);
46
+ expect(args.dryRun).toBe(true);
47
+ });
48
+ it('should parse --execute flag', () => {
49
+ const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--execute']);
50
+ expect(args.dryRun).toBe(false);
51
+ });
52
+ it('should parse --stale-days-in-progress option', () => {
53
+ const args = parseBacklogPruneArgs([
54
+ 'node',
55
+ 'backlog-prune',
56
+ '--stale-days-in-progress',
57
+ '5',
58
+ ]);
59
+ expect(args.staleDaysInProgress).toBe(5);
60
+ });
61
+ it('should parse --stale-days-ready option', () => {
62
+ const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--stale-days-ready', '14']);
63
+ expect(args.staleDaysReady).toBe(14);
64
+ });
65
+ it('should parse --archive-days option', () => {
66
+ const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--archive-days', '60']);
67
+ expect(args.archiveDaysDone).toBe(60);
68
+ });
69
+ it('should use defaults when options not provided', () => {
70
+ const args = parseBacklogPruneArgs(['node', 'backlog-prune']);
71
+ expect(args.staleDaysInProgress).toBe(BACKLOG_PRUNE_DEFAULTS.staleDaysInProgress);
72
+ expect(args.staleDaysReady).toBe(BACKLOG_PRUNE_DEFAULTS.staleDaysReady);
73
+ expect(args.archiveDaysDone).toBe(BACKLOG_PRUNE_DEFAULTS.archiveDaysDone);
74
+ });
75
+ it('should parse --help flag', () => {
76
+ const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--help']);
77
+ expect(args.help).toBe(true);
78
+ });
79
+ });
80
+ describe('calculateStaleDays', () => {
81
+ beforeEach(() => {
82
+ vi.useFakeTimers();
83
+ // Set current time to 2026-01-25
84
+ vi.setSystemTime(new Date('2026-01-25T12:00:00Z'));
85
+ });
86
+ afterEach(() => {
87
+ vi.useRealTimers();
88
+ });
89
+ it('should return days since date string', () => {
90
+ const result = calculateStaleDays('2026-01-20');
91
+ expect(result).toBe(5);
92
+ });
93
+ it('should return days since ISO date string', () => {
94
+ const result = calculateStaleDays('2026-01-23T10:00:00Z');
95
+ expect(result).toBe(2);
96
+ });
97
+ it('should return 0 for same day', () => {
98
+ const result = calculateStaleDays('2026-01-25');
99
+ expect(result).toBe(0);
100
+ });
101
+ it('should return null for invalid date', () => {
102
+ const result = calculateStaleDays('not-a-date');
103
+ expect(result).toBeNull();
104
+ });
105
+ it('should return null for undefined', () => {
106
+ const result = calculateStaleDays(undefined);
107
+ expect(result).toBeNull();
108
+ });
109
+ it('should return null for null', () => {
110
+ const result = calculateStaleDays(null);
111
+ expect(result).toBeNull();
112
+ });
113
+ });
114
+ describe('isWuStale', () => {
115
+ beforeEach(() => {
116
+ vi.useFakeTimers();
117
+ vi.setSystemTime(new Date('2026-01-25T12:00:00Z'));
118
+ });
119
+ afterEach(() => {
120
+ vi.useRealTimers();
121
+ });
122
+ it('should return true for in_progress WU older than threshold', () => {
123
+ const wu = {
124
+ id: 'WU-100',
125
+ status: 'in_progress',
126
+ created: '2026-01-10',
127
+ updated: '2026-01-15', // 10 days old
128
+ };
129
+ expect(isWuStale(wu, { staleDaysInProgress: 7 })).toBe(true);
130
+ });
131
+ it('should return false for in_progress WU within threshold', () => {
132
+ const wu = {
133
+ id: 'WU-100',
134
+ status: 'in_progress',
135
+ created: '2026-01-20',
136
+ updated: '2026-01-24', // 1 day old
137
+ };
138
+ expect(isWuStale(wu, { staleDaysInProgress: 7 })).toBe(false);
139
+ });
140
+ it('should return true for ready WU older than threshold', () => {
141
+ const wu = {
142
+ id: 'WU-100',
143
+ status: 'ready',
144
+ created: '2025-12-20', // 36 days old
145
+ };
146
+ expect(isWuStale(wu, { staleDaysReady: 30 })).toBe(true);
147
+ });
148
+ it('should return false for ready WU within threshold', () => {
149
+ const wu = {
150
+ id: 'WU-100',
151
+ status: 'ready',
152
+ created: '2026-01-10', // 15 days old
153
+ };
154
+ expect(isWuStale(wu, { staleDaysReady: 30 })).toBe(false);
155
+ });
156
+ it('should return false for done WU (done WUs are not stale, they may be archivable)', () => {
157
+ const wu = {
158
+ id: 'WU-100',
159
+ status: 'done',
160
+ created: '2025-01-01',
161
+ completed: '2025-01-10',
162
+ };
163
+ expect(isWuStale(wu, { staleDaysInProgress: 7, staleDaysReady: 30 })).toBe(false);
164
+ });
165
+ it('should use updated date if available for staleness check', () => {
166
+ const wu = {
167
+ id: 'WU-100',
168
+ status: 'in_progress',
169
+ created: '2025-01-01', // Very old
170
+ updated: '2026-01-24', // Recently updated (1 day ago)
171
+ };
172
+ expect(isWuStale(wu, { staleDaysInProgress: 7 })).toBe(false);
173
+ });
174
+ it('should fall back to created date if updated not available', () => {
175
+ const wu = {
176
+ id: 'WU-100',
177
+ status: 'in_progress',
178
+ created: '2026-01-10', // 15 days old
179
+ };
180
+ expect(isWuStale(wu, { staleDaysInProgress: 7 })).toBe(true);
181
+ });
182
+ });
183
+ describe('isWuArchivable', () => {
184
+ beforeEach(() => {
185
+ vi.useFakeTimers();
186
+ vi.setSystemTime(new Date('2026-01-25T12:00:00Z'));
187
+ });
188
+ afterEach(() => {
189
+ vi.useRealTimers();
190
+ });
191
+ it('should return true for done WU completed older than threshold', () => {
192
+ const wu = {
193
+ id: 'WU-100',
194
+ status: 'done',
195
+ created: '2025-01-01',
196
+ completed: '2025-11-25', // 61 days ago
197
+ };
198
+ expect(isWuArchivable(wu, { archiveDaysDone: 60 })).toBe(true);
199
+ });
200
+ it('should return false for done WU completed within threshold', () => {
201
+ const wu = {
202
+ id: 'WU-100',
203
+ status: 'done',
204
+ created: '2025-12-01',
205
+ completed: '2026-01-10', // 15 days ago
206
+ };
207
+ expect(isWuArchivable(wu, { archiveDaysDone: 60 })).toBe(false);
208
+ });
209
+ it('should return false for non-done WU', () => {
210
+ const wu = {
211
+ id: 'WU-100',
212
+ status: 'in_progress',
213
+ created: '2025-01-01',
214
+ };
215
+ expect(isWuArchivable(wu, { archiveDaysDone: 60 })).toBe(false);
216
+ });
217
+ it('should return false for done WU without completed date', () => {
218
+ const wu = {
219
+ id: 'WU-100',
220
+ status: 'done',
221
+ created: '2025-01-01',
222
+ };
223
+ expect(isWuArchivable(wu, { archiveDaysDone: 60 })).toBe(false);
224
+ });
225
+ });
226
+ describe('categorizeWus', () => {
227
+ beforeEach(() => {
228
+ vi.useFakeTimers();
229
+ vi.setSystemTime(new Date('2026-01-25T12:00:00Z'));
230
+ });
231
+ afterEach(() => {
232
+ vi.useRealTimers();
233
+ });
234
+ it('should categorize stale, archivable, and healthy WUs', () => {
235
+ const wus = [
236
+ // Stale in_progress (updated 10 days ago)
237
+ { id: 'WU-101', status: 'in_progress', created: '2026-01-01', updated: '2026-01-15' },
238
+ // Healthy in_progress (updated 2 days ago)
239
+ { id: 'WU-102', status: 'in_progress', created: '2026-01-01', updated: '2026-01-23' },
240
+ // Stale ready (created 40 days ago)
241
+ { id: 'WU-103', status: 'ready', created: '2025-12-16' },
242
+ // Healthy ready (created 10 days ago)
243
+ { id: 'WU-104', status: 'ready', created: '2026-01-15' },
244
+ // Archivable done (completed 70 days ago)
245
+ { id: 'WU-105', status: 'done', created: '2025-10-01', completed: '2025-11-16' },
246
+ // Healthy done (completed 30 days ago)
247
+ { id: 'WU-106', status: 'done', created: '2025-12-01', completed: '2025-12-26' },
248
+ // Blocked WU (not considered stale/archivable)
249
+ { id: 'WU-107', status: 'blocked', created: '2025-01-01' },
250
+ ];
251
+ const result = categorizeWus(wus, {
252
+ staleDaysInProgress: 7,
253
+ staleDaysReady: 30,
254
+ archiveDaysDone: 60,
255
+ });
256
+ expect(result.stale).toHaveLength(2);
257
+ expect(result.stale.map((w) => w.id)).toContain('WU-101');
258
+ expect(result.stale.map((w) => w.id)).toContain('WU-103');
259
+ expect(result.archivable).toHaveLength(1);
260
+ expect(result.archivable[0].id).toBe('WU-105');
261
+ expect(result.healthy).toHaveLength(4);
262
+ expect(result.healthy.map((w) => w.id)).toContain('WU-102');
263
+ expect(result.healthy.map((w) => w.id)).toContain('WU-104');
264
+ expect(result.healthy.map((w) => w.id)).toContain('WU-106');
265
+ expect(result.healthy.map((w) => w.id)).toContain('WU-107');
266
+ });
267
+ it('should return empty arrays when no WUs provided', () => {
268
+ const result = categorizeWus([], {
269
+ staleDaysInProgress: 7,
270
+ staleDaysReady: 30,
271
+ archiveDaysDone: 60,
272
+ });
273
+ expect(result.stale).toHaveLength(0);
274
+ expect(result.archivable).toHaveLength(0);
275
+ expect(result.healthy).toHaveLength(0);
276
+ });
277
+ });
278
+ describe('isWuStale - additional edge cases', () => {
279
+ beforeEach(() => {
280
+ vi.useFakeTimers();
281
+ vi.setSystemTime(new Date('2026-01-25T12:00:00Z'));
282
+ });
283
+ afterEach(() => {
284
+ vi.useRealTimers();
285
+ });
286
+ it('should return false for blocked WU regardless of age', () => {
287
+ const wu = {
288
+ id: 'WU-100',
289
+ status: 'blocked',
290
+ created: '2025-01-01', // Very old
291
+ };
292
+ expect(isWuStale(wu, { staleDaysInProgress: 7, staleDaysReady: 30 })).toBe(false);
293
+ });
294
+ it('should return false for completed WU (legacy status)', () => {
295
+ const wu = {
296
+ id: 'WU-100',
297
+ status: 'completed',
298
+ created: '2025-01-01',
299
+ completed: '2025-01-10',
300
+ };
301
+ expect(isWuStale(wu, { staleDaysInProgress: 7, staleDaysReady: 30 })).toBe(false);
302
+ });
303
+ it('should handle backlog status (legacy ready)', () => {
304
+ const wu = {
305
+ id: 'WU-100',
306
+ status: 'backlog',
307
+ created: '2025-12-20', // 36 days old
308
+ };
309
+ expect(isWuStale(wu, { staleDaysReady: 30 })).toBe(true);
310
+ });
311
+ it('should handle todo status (legacy ready)', () => {
312
+ const wu = {
313
+ id: 'WU-100',
314
+ status: 'todo',
315
+ created: '2025-12-20', // 36 days old
316
+ };
317
+ expect(isWuStale(wu, { staleDaysReady: 30 })).toBe(true);
318
+ });
319
+ it('should return false when no date is available', () => {
320
+ const wu = {
321
+ id: 'WU-100',
322
+ status: 'in_progress',
323
+ };
324
+ expect(isWuStale(wu, { staleDaysInProgress: 7 })).toBe(false);
325
+ });
326
+ it('should return false for unknown status', () => {
327
+ const wu = {
328
+ id: 'WU-100',
329
+ status: 'unknown',
330
+ created: '2025-01-01', // Very old
331
+ };
332
+ expect(isWuStale(wu, { staleDaysInProgress: 7, staleDaysReady: 30 })).toBe(false);
333
+ });
334
+ it('should use default threshold when not specified', () => {
335
+ const wu = {
336
+ id: 'WU-100',
337
+ status: 'in_progress',
338
+ created: '2026-01-10', // 15 days old
339
+ };
340
+ // Default is 7 days, so 15 days is stale
341
+ expect(isWuStale(wu, {})).toBe(true);
342
+ });
343
+ });
344
+ describe('isWuArchivable - additional edge cases', () => {
345
+ beforeEach(() => {
346
+ vi.useFakeTimers();
347
+ vi.setSystemTime(new Date('2026-01-25T12:00:00Z'));
348
+ });
349
+ afterEach(() => {
350
+ vi.useRealTimers();
351
+ });
352
+ it('should return true for completed WU (legacy done status)', () => {
353
+ const wu = {
354
+ id: 'WU-100',
355
+ status: 'completed',
356
+ created: '2025-01-01',
357
+ completed: '2025-11-20', // 66 days ago
358
+ };
359
+ expect(isWuArchivable(wu, { archiveDaysDone: 60 })).toBe(true);
360
+ });
361
+ it('should use default threshold when not specified', () => {
362
+ const wu = {
363
+ id: 'WU-100',
364
+ status: 'done',
365
+ created: '2025-01-01',
366
+ completed: '2025-10-20', // 97 days ago
367
+ };
368
+ // Default is 90 days, so 97 days is archivable
369
+ expect(isWuArchivable(wu, {})).toBe(true);
370
+ });
371
+ it('should return false when completed date is invalid', () => {
372
+ const wu = {
373
+ id: 'WU-100',
374
+ status: 'done',
375
+ created: '2025-01-01',
376
+ completed: 'invalid-date',
377
+ };
378
+ expect(isWuArchivable(wu, { archiveDaysDone: 60 })).toBe(false);
379
+ });
380
+ });
381
+ describe('parseBacklogPruneArgs - additional edge cases', () => {
382
+ it('should handle -h flag as help', () => {
383
+ const args = parseBacklogPruneArgs(['node', 'backlog-prune', '-h']);
384
+ expect(args.help).toBe(true);
385
+ });
386
+ it('should handle missing value for --stale-days-in-progress', () => {
387
+ const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--stale-days-in-progress']);
388
+ // Should use default when no value follows
389
+ expect(args.staleDaysInProgress).toBe(BACKLOG_PRUNE_DEFAULTS.staleDaysInProgress);
390
+ });
391
+ it('should handle missing value for --stale-days-ready', () => {
392
+ const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--stale-days-ready']);
393
+ expect(args.staleDaysReady).toBe(BACKLOG_PRUNE_DEFAULTS.staleDaysReady);
394
+ });
395
+ it('should handle missing value for --archive-days', () => {
396
+ const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--archive-days']);
397
+ expect(args.archiveDaysDone).toBe(BACKLOG_PRUNE_DEFAULTS.archiveDaysDone);
398
+ });
399
+ it('should handle multiple flags combined', () => {
400
+ const args = parseBacklogPruneArgs([
401
+ 'node',
402
+ 'backlog-prune',
403
+ '--execute',
404
+ '--stale-days-in-progress',
405
+ '10',
406
+ '--stale-days-ready',
407
+ '20',
408
+ '--archive-days',
409
+ '45',
410
+ ]);
411
+ expect(args.dryRun).toBe(false);
412
+ expect(args.staleDaysInProgress).toBe(10);
413
+ expect(args.staleDaysReady).toBe(20);
414
+ expect(args.archiveDaysDone).toBe(45);
415
+ });
416
+ it('should override with --dry-run after --execute', () => {
417
+ const args = parseBacklogPruneArgs(['node', 'backlog-prune', '--execute', '--dry-run']);
418
+ expect(args.dryRun).toBe(true);
419
+ });
420
+ });
421
+ describe('loadAllWus', () => {
422
+ it('should be a function', () => {
423
+ expect(typeof loadAllWus).toBe('function');
424
+ });
425
+ it('should return an array', () => {
426
+ // This will load from the actual WU directory in the test environment
427
+ const result = loadAllWus();
428
+ expect(Array.isArray(result)).toBe(true);
429
+ });
430
+ it('should return WuPruneInfo objects with required fields', () => {
431
+ const result = loadAllWus();
432
+ // All returned items should have at least id and status
433
+ for (const wu of result) {
434
+ expect(wu).toHaveProperty('id');
435
+ expect(wu).toHaveProperty('status');
436
+ }
437
+ });
438
+ });
439
+ describe('tagStaleWu', () => {
440
+ it('should be a function', () => {
441
+ expect(typeof tagStaleWu).toBe('function');
442
+ });
443
+ it('should log in dry-run mode without modifying files', () => {
444
+ const wu = {
445
+ id: 'WU-TEST-999',
446
+ status: 'in_progress',
447
+ created: '2026-01-01',
448
+ };
449
+ // In dry-run mode, it should just log
450
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
451
+ tagStaleWu(wu, true);
452
+ expect(consoleSpy).toHaveBeenCalled();
453
+ consoleSpy.mockRestore();
454
+ });
455
+ });
456
+ describe('printHelp', () => {
457
+ it('should be a function', () => {
458
+ expect(typeof printHelp).toBe('function');
459
+ });
460
+ it('should print help text to console', () => {
461
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
462
+ printHelp();
463
+ expect(consoleSpy).toHaveBeenCalled();
464
+ consoleSpy.mockRestore();
465
+ });
466
+ it('should include usage information', () => {
467
+ let output = '';
468
+ const consoleSpy = vi.spyOn(console, 'log').mockImplementation((msg) => {
469
+ output += msg;
470
+ });
471
+ printHelp();
472
+ expect(output).toContain('backlog:prune');
473
+ expect(output).toContain('--execute');
474
+ expect(output).toContain('--dry-run');
475
+ consoleSpy.mockRestore();
476
+ });
477
+ });
478
+ });
@@ -0,0 +1,206 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for deps-add and deps-remove CLI commands
4
+ *
5
+ * WU-1112: INIT-003 Phase 6 - Migrate remaining Tier 1 tools
6
+ *
7
+ * These commands provide safe wrappers for pnpm add/remove that enforce
8
+ * worktree discipline - dependencies can only be modified in worktrees,
9
+ * not on the main checkout.
10
+ */
11
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
12
+ // Import functions under test
13
+ import { parseDepsAddArgs, parseDepsRemoveArgs, validateWorktreeContext, buildPnpmAddCommand, buildPnpmRemoveCommand, } from '../deps-add.js';
14
+ describe('deps-add', () => {
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ });
18
+ describe('parseDepsAddArgs', () => {
19
+ it('should parse package name from positional argument', () => {
20
+ const args = parseDepsAddArgs(['node', 'deps-add.js', 'react']);
21
+ expect(args.packages).toEqual(['react']);
22
+ });
23
+ it('should parse multiple packages', () => {
24
+ const args = parseDepsAddArgs(['node', 'deps-add.js', 'react', 'react-dom', 'typescript']);
25
+ expect(args.packages).toEqual(['react', 'react-dom', 'typescript']);
26
+ });
27
+ it('should parse --dev flag', () => {
28
+ const args = parseDepsAddArgs(['node', 'deps-add.js', 'vitest', '--dev']);
29
+ expect(args.dev).toBe(true);
30
+ expect(args.packages).toEqual(['vitest']);
31
+ });
32
+ it('should parse -D flag as dev dependency', () => {
33
+ const args = parseDepsAddArgs(['node', 'deps-add.js', '-D', 'vitest']);
34
+ expect(args.dev).toBe(true);
35
+ expect(args.packages).toEqual(['vitest']);
36
+ });
37
+ it('should parse --filter flag', () => {
38
+ const args = parseDepsAddArgs(['node', 'deps-add.js', '--filter', '@lumenflow/cli', 'chalk']);
39
+ expect(args.filter).toBe('@lumenflow/cli');
40
+ expect(args.packages).toEqual(['chalk']);
41
+ });
42
+ it('should parse --exact flag', () => {
43
+ const args = parseDepsAddArgs(['node', 'deps-add.js', '--exact', 'react@18.2.0']);
44
+ expect(args.exact).toBe(true);
45
+ });
46
+ it('should set help flag', () => {
47
+ const args = parseDepsAddArgs(['node', 'deps-add.js', '--help']);
48
+ expect(args.help).toBe(true);
49
+ });
50
+ it('should return empty packages array when no packages specified', () => {
51
+ const args = parseDepsAddArgs(['node', 'deps-add.js']);
52
+ expect(args.packages).toEqual([]);
53
+ });
54
+ });
55
+ describe('validateWorktreeContext', () => {
56
+ it('should return valid when cwd contains worktrees/', () => {
57
+ const result = validateWorktreeContext('/home/user/project/worktrees/framework-cli-wu-1112');
58
+ expect(result.valid).toBe(true);
59
+ expect(result.error).toBeUndefined();
60
+ });
61
+ it('should return invalid when cwd is main checkout', () => {
62
+ const result = validateWorktreeContext('/home/user/project');
63
+ expect(result.valid).toBe(false);
64
+ expect(result.error).toContain('main checkout');
65
+ });
66
+ it('should provide fix command when invalid', () => {
67
+ const result = validateWorktreeContext('/home/user/project');
68
+ expect(result.fixCommand).toBeDefined();
69
+ expect(result.fixCommand).toContain('wu:claim');
70
+ });
71
+ });
72
+ describe('buildPnpmAddCommand', () => {
73
+ it('should build basic add command', () => {
74
+ const args = { packages: ['react'] };
75
+ const cmd = buildPnpmAddCommand(args);
76
+ expect(cmd).toBe('pnpm add react');
77
+ });
78
+ it('should add --save-dev for dev dependencies', () => {
79
+ const args = { packages: ['vitest'], dev: true };
80
+ const cmd = buildPnpmAddCommand(args);
81
+ expect(cmd).toBe('pnpm add --save-dev vitest');
82
+ });
83
+ it('should add --filter for workspace packages', () => {
84
+ const args = { packages: ['chalk'], filter: '@lumenflow/cli' };
85
+ const cmd = buildPnpmAddCommand(args);
86
+ expect(cmd).toBe('pnpm add --filter @lumenflow/cli chalk');
87
+ });
88
+ it('should add --save-exact for exact versions', () => {
89
+ const args = { packages: ['react@18.2.0'], exact: true };
90
+ const cmd = buildPnpmAddCommand(args);
91
+ expect(cmd).toBe('pnpm add --save-exact react@18.2.0');
92
+ });
93
+ it('should combine multiple flags', () => {
94
+ const args = {
95
+ packages: ['vitest'],
96
+ dev: true,
97
+ filter: '@lumenflow/cli',
98
+ exact: true,
99
+ };
100
+ const cmd = buildPnpmAddCommand(args);
101
+ expect(cmd).toContain('--save-dev');
102
+ expect(cmd).toContain('--filter @lumenflow/cli');
103
+ expect(cmd).toContain('--save-exact');
104
+ expect(cmd).toContain('vitest');
105
+ });
106
+ it('should handle multiple packages', () => {
107
+ const args = { packages: ['react', 'react-dom'] };
108
+ const cmd = buildPnpmAddCommand(args);
109
+ expect(cmd).toBe('pnpm add react react-dom');
110
+ });
111
+ });
112
+ });
113
+ describe('deps-remove', () => {
114
+ beforeEach(() => {
115
+ vi.clearAllMocks();
116
+ });
117
+ describe('parseDepsRemoveArgs', () => {
118
+ it('should parse package name from positional argument', () => {
119
+ const args = parseDepsRemoveArgs(['node', 'deps-remove.js', 'lodash']);
120
+ expect(args.packages).toEqual(['lodash']);
121
+ });
122
+ it('should parse multiple packages', () => {
123
+ const args = parseDepsRemoveArgs(['node', 'deps-remove.js', 'lodash', 'moment']);
124
+ expect(args.packages).toEqual(['lodash', 'moment']);
125
+ });
126
+ it('should parse --filter flag', () => {
127
+ const args = parseDepsRemoveArgs([
128
+ 'node',
129
+ 'deps-remove.js',
130
+ '--filter',
131
+ '@lumenflow/core',
132
+ 'lodash',
133
+ ]);
134
+ expect(args.filter).toBe('@lumenflow/core');
135
+ expect(args.packages).toEqual(['lodash']);
136
+ });
137
+ it('should set help flag', () => {
138
+ const args = parseDepsRemoveArgs(['node', 'deps-remove.js', '--help']);
139
+ expect(args.help).toBe(true);
140
+ });
141
+ });
142
+ describe('buildPnpmRemoveCommand', () => {
143
+ it('should build basic remove command', () => {
144
+ const args = { packages: ['lodash'] };
145
+ const cmd = buildPnpmRemoveCommand(args);
146
+ expect(cmd).toBe('pnpm remove lodash');
147
+ });
148
+ it('should add --filter for workspace packages', () => {
149
+ const args = { packages: ['lodash'], filter: '@lumenflow/core' };
150
+ const cmd = buildPnpmRemoveCommand(args);
151
+ expect(cmd).toBe('pnpm remove --filter @lumenflow/core lodash');
152
+ });
153
+ it('should handle multiple packages', () => {
154
+ const args = { packages: ['lodash', 'moment'] };
155
+ const cmd = buildPnpmRemoveCommand(args);
156
+ expect(cmd).toBe('pnpm remove lodash moment');
157
+ });
158
+ it('should handle empty packages array', () => {
159
+ const args = { packages: [] };
160
+ const cmd = buildPnpmRemoveCommand(args);
161
+ expect(cmd).toBe('pnpm remove');
162
+ });
163
+ });
164
+ describe('parseDepsAddArgs edge cases', () => {
165
+ it('should handle -h flag', () => {
166
+ const args = parseDepsAddArgs(['node', 'deps-add.js', '-h']);
167
+ expect(args.help).toBe(true);
168
+ });
169
+ it('should handle -E flag for exact', () => {
170
+ const args = parseDepsAddArgs(['node', 'deps-add.js', '-E', 'react']);
171
+ expect(args.exact).toBe(true);
172
+ });
173
+ it('should handle -F flag for filter', () => {
174
+ const args = parseDepsAddArgs(['node', 'deps-add.js', '-F', '@lumenflow/cli', 'chalk']);
175
+ expect(args.filter).toBe('@lumenflow/cli');
176
+ });
177
+ });
178
+ describe('parseDepsRemoveArgs edge cases', () => {
179
+ it('should handle -h flag', () => {
180
+ const args = parseDepsRemoveArgs(['node', 'deps-remove.js', '-h']);
181
+ expect(args.help).toBe(true);
182
+ });
183
+ it('should handle -F flag for filter', () => {
184
+ const args = parseDepsRemoveArgs([
185
+ 'node',
186
+ 'deps-remove.js',
187
+ '-F',
188
+ '@lumenflow/cli',
189
+ 'lodash',
190
+ ]);
191
+ expect(args.filter).toBe('@lumenflow/cli');
192
+ });
193
+ });
194
+ describe('buildPnpmAddCommand edge cases', () => {
195
+ it('should handle empty packages array', () => {
196
+ const args = { packages: [] };
197
+ const cmd = buildPnpmAddCommand(args);
198
+ expect(cmd).toBe('pnpm add');
199
+ });
200
+ it('should handle undefined packages', () => {
201
+ const args = {};
202
+ const cmd = buildPnpmAddCommand(args);
203
+ expect(cmd).toBe('pnpm add');
204
+ });
205
+ });
206
+ });