@lumenflow/cli 1.6.0 → 2.1.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 (42) hide show
  1. package/README.md +19 -0
  2. package/dist/__tests__/backlog-prune.test.js +478 -0
  3. package/dist/__tests__/deps-operations.test.js +206 -0
  4. package/dist/__tests__/file-operations.test.js +906 -0
  5. package/dist/__tests__/git-operations.test.js +668 -0
  6. package/dist/__tests__/guards-validation.test.js +416 -0
  7. package/dist/__tests__/init-plan.test.js +340 -0
  8. package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
  9. package/dist/__tests__/metrics-cli.test.js +619 -0
  10. package/dist/__tests__/rotate-progress.test.js +127 -0
  11. package/dist/__tests__/session-coordinator.test.js +109 -0
  12. package/dist/__tests__/state-bootstrap.test.js +432 -0
  13. package/dist/__tests__/trace-gen.test.js +115 -0
  14. package/dist/backlog-prune.js +299 -0
  15. package/dist/deps-add.js +215 -0
  16. package/dist/deps-remove.js +94 -0
  17. package/dist/docs-sync.js +72 -326
  18. package/dist/file-delete.js +236 -0
  19. package/dist/file-edit.js +247 -0
  20. package/dist/file-read.js +197 -0
  21. package/dist/file-write.js +220 -0
  22. package/dist/git-branch.js +187 -0
  23. package/dist/git-diff.js +177 -0
  24. package/dist/git-log.js +230 -0
  25. package/dist/git-status.js +208 -0
  26. package/dist/guard-locked.js +169 -0
  27. package/dist/guard-main-branch.js +202 -0
  28. package/dist/guard-worktree-commit.js +160 -0
  29. package/dist/init-plan.js +337 -0
  30. package/dist/lumenflow-upgrade.js +178 -0
  31. package/dist/metrics-cli.js +433 -0
  32. package/dist/rotate-progress.js +247 -0
  33. package/dist/session-coordinator.js +300 -0
  34. package/dist/state-bootstrap.js +307 -0
  35. package/dist/sync-templates.js +212 -0
  36. package/dist/trace-gen.js +331 -0
  37. package/dist/validate-agent-skills.js +218 -0
  38. package/dist/validate-agent-sync.js +148 -0
  39. package/dist/validate-backlog-sync.js +152 -0
  40. package/dist/validate-skills-spec.js +206 -0
  41. package/dist/validate.js +230 -0
  42. package/package.json +37 -7
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for trace-gen CLI command
4
+ *
5
+ * WU-1112: INIT-003 Phase 6 - Migrate remaining Tier 1 tools
6
+ *
7
+ * Trace generator creates traceability reports linking WUs to code changes,
8
+ * useful for audit trails and compliance documentation.
9
+ */
10
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
11
+ // Import functions under test
12
+ import { parseTraceArgs, TraceFormat, buildTraceEntry, } from '../trace-gen.js';
13
+ describe('trace-gen', () => {
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+ describe('parseTraceArgs', () => {
18
+ it('should parse --wu flag', () => {
19
+ const args = parseTraceArgs(['node', 'trace-gen.js', '--wu', 'WU-1112']);
20
+ expect(args.wuId).toBe('WU-1112');
21
+ });
22
+ it('should parse --format json', () => {
23
+ const args = parseTraceArgs(['node', 'trace-gen.js', '--format', 'json']);
24
+ expect(args.format).toBe('json');
25
+ });
26
+ it('should parse --format markdown', () => {
27
+ const args = parseTraceArgs(['node', 'trace-gen.js', '--format', 'markdown']);
28
+ expect(args.format).toBe('markdown');
29
+ });
30
+ it('should parse --output flag', () => {
31
+ const args = parseTraceArgs(['node', 'trace-gen.js', '--output', 'trace.json']);
32
+ expect(args.output).toBe('trace.json');
33
+ });
34
+ it('should parse --since flag', () => {
35
+ const args = parseTraceArgs(['node', 'trace-gen.js', '--since', '2024-01-01']);
36
+ expect(args.since).toBe('2024-01-01');
37
+ });
38
+ it('should parse --help flag', () => {
39
+ const args = parseTraceArgs(['node', 'trace-gen.js', '--help']);
40
+ expect(args.help).toBe(true);
41
+ });
42
+ it('should default format to json', () => {
43
+ const args = parseTraceArgs(['node', 'trace-gen.js']);
44
+ expect(args.format).toBeUndefined();
45
+ });
46
+ });
47
+ describe('TraceFormat enum', () => {
48
+ it('should have JSON format', () => {
49
+ expect(TraceFormat.JSON).toBe('json');
50
+ });
51
+ it('should have markdown format', () => {
52
+ expect(TraceFormat.MARKDOWN).toBe('markdown');
53
+ });
54
+ it('should have CSV format', () => {
55
+ expect(TraceFormat.CSV).toBe('csv');
56
+ });
57
+ });
58
+ describe('buildTraceEntry', () => {
59
+ it('should build trace entry from WU and commit data', () => {
60
+ const entry = buildTraceEntry({
61
+ wuId: 'WU-1112',
62
+ title: 'Migrate tools',
63
+ status: 'done',
64
+ commits: [{ sha: 'abc1234', message: 'feat: add deps-add', date: '2024-01-15T10:00:00Z' }],
65
+ files: ['src/deps-add.ts'],
66
+ });
67
+ expect(entry.wuId).toBe('WU-1112');
68
+ expect(entry.title).toBe('Migrate tools');
69
+ expect(entry.status).toBe('done');
70
+ expect(entry.commitCount).toBe(1);
71
+ expect(entry.fileCount).toBe(1);
72
+ });
73
+ it('should calculate commit count correctly', () => {
74
+ const entry = buildTraceEntry({
75
+ wuId: 'WU-1112',
76
+ title: 'Test',
77
+ status: 'done',
78
+ commits: [
79
+ { sha: 'abc1234', message: 'feat: first', date: '2024-01-15T10:00:00Z' },
80
+ { sha: 'def5678', message: 'feat: second', date: '2024-01-16T10:00:00Z' },
81
+ { sha: 'ghi9012', message: 'fix: third', date: '2024-01-17T10:00:00Z' },
82
+ ],
83
+ files: ['a.ts', 'b.ts'],
84
+ });
85
+ expect(entry.commitCount).toBe(3);
86
+ expect(entry.fileCount).toBe(2);
87
+ });
88
+ it('should include first and last commit dates', () => {
89
+ const entry = buildTraceEntry({
90
+ wuId: 'WU-1112',
91
+ title: 'Test',
92
+ status: 'done',
93
+ commits: [
94
+ { sha: 'abc1234', message: 'feat: first', date: '2024-01-15T10:00:00Z' },
95
+ { sha: 'def5678', message: 'feat: last', date: '2024-01-20T10:00:00Z' },
96
+ ],
97
+ files: [],
98
+ });
99
+ expect(entry.firstCommit).toBe('2024-01-15T10:00:00Z');
100
+ expect(entry.lastCommit).toBe('2024-01-20T10:00:00Z');
101
+ });
102
+ it('should handle empty commits array', () => {
103
+ const entry = buildTraceEntry({
104
+ wuId: 'WU-1112',
105
+ title: 'Test',
106
+ status: 'done',
107
+ commits: [],
108
+ files: [],
109
+ });
110
+ expect(entry.commitCount).toBe(0);
111
+ expect(entry.firstCommit).toBeUndefined();
112
+ expect(entry.lastCommit).toBeUndefined();
113
+ });
114
+ });
115
+ });
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Backlog Prune Command
4
+ *
5
+ * 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
+ * WU-1106: INIT-003 Phase 3b - Migrate from PatientPath tools/backlog-prune.mjs
10
+ *
11
+ * Usage:
12
+ * pnpm backlog:prune # Dry-run mode (shows what would be done)
13
+ * pnpm backlog:prune --execute # Apply changes
14
+ */
15
+ import { readdirSync, existsSync } from 'node:fs';
16
+ import path from 'node:path';
17
+ import { readWURaw, writeWU, appendNote } from '@lumenflow/core/dist/wu-yaml.js';
18
+ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
19
+ import { CLI_FLAGS, EXIT_CODES, EMOJI, WU_STATUS, STRING_LITERALS, } from '@lumenflow/core/dist/wu-constants.js';
20
+ /* eslint-disable security/detect-non-literal-fs-filename */
21
+ /** Log prefix for consistent output */
22
+ const LOG_PREFIX = '[backlog-prune]';
23
+ /**
24
+ * Default configuration for backlog pruning
25
+ */
26
+ export const BACKLOG_PRUNE_DEFAULTS = {
27
+ /** Days without activity before in_progress WU is considered stale */
28
+ staleDaysInProgress: 7,
29
+ /** Days without activity before ready WU is considered stale */
30
+ staleDaysReady: 30,
31
+ /** Days after completion before done WU can be archived */
32
+ archiveDaysDone: 90,
33
+ };
34
+ /**
35
+ * Parse command line arguments for backlog-prune
36
+ */
37
+ export function parseBacklogPruneArgs(argv) {
38
+ const args = {
39
+ dryRun: true,
40
+ staleDaysInProgress: BACKLOG_PRUNE_DEFAULTS.staleDaysInProgress,
41
+ staleDaysReady: BACKLOG_PRUNE_DEFAULTS.staleDaysReady,
42
+ archiveDaysDone: BACKLOG_PRUNE_DEFAULTS.archiveDaysDone,
43
+ help: false,
44
+ };
45
+ for (let i = 2; i < argv.length; i++) {
46
+ const arg = argv[i];
47
+ if (arg === CLI_FLAGS.EXECUTE) {
48
+ args.dryRun = false;
49
+ }
50
+ else if (arg === CLI_FLAGS.DRY_RUN) {
51
+ args.dryRun = true;
52
+ }
53
+ else if (arg === CLI_FLAGS.HELP || arg === CLI_FLAGS.HELP_SHORT) {
54
+ args.help = true;
55
+ }
56
+ else if (arg === '--stale-days-in-progress' && argv[i + 1]) {
57
+ args.staleDaysInProgress = parseInt(argv[++i], 10);
58
+ }
59
+ else if (arg === '--stale-days-ready' && argv[i + 1]) {
60
+ args.staleDaysReady = parseInt(argv[++i], 10);
61
+ }
62
+ else if (arg === '--archive-days' && argv[i + 1]) {
63
+ args.archiveDaysDone = parseInt(argv[++i], 10);
64
+ }
65
+ }
66
+ return args;
67
+ }
68
+ /**
69
+ * Calculate days since a date string
70
+ * @returns Number of days since date, or null if invalid
71
+ */
72
+ export function calculateStaleDays(dateStr) {
73
+ if (!dateStr)
74
+ return null;
75
+ const date = new Date(dateStr);
76
+ if (isNaN(date.getTime()))
77
+ return null;
78
+ const now = new Date();
79
+ const diffMs = now.getTime() - date.getTime();
80
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
81
+ return diffDays;
82
+ }
83
+ /**
84
+ * Check if a WU is stale based on its status and last activity date
85
+ */
86
+ export function isWuStale(wu, options) {
87
+ const { staleDaysInProgress = BACKLOG_PRUNE_DEFAULTS.staleDaysInProgress } = options;
88
+ const { staleDaysReady = BACKLOG_PRUNE_DEFAULTS.staleDaysReady } = options;
89
+ // Done/blocked WUs are not considered stale
90
+ if (wu.status === WU_STATUS.DONE ||
91
+ wu.status === WU_STATUS.COMPLETED ||
92
+ wu.status === WU_STATUS.BLOCKED) {
93
+ return false;
94
+ }
95
+ // Get the relevant date for staleness check
96
+ // Use updated date if available, otherwise fall back to created date
97
+ const lastActivityDate = wu.updated || wu.created;
98
+ const daysSinceActivity = calculateStaleDays(lastActivityDate);
99
+ if (daysSinceActivity === null)
100
+ return false;
101
+ // Check against threshold based on status
102
+ if (wu.status === WU_STATUS.IN_PROGRESS) {
103
+ return daysSinceActivity > staleDaysInProgress;
104
+ }
105
+ if (wu.status === WU_STATUS.READY ||
106
+ wu.status === WU_STATUS.BACKLOG ||
107
+ wu.status === WU_STATUS.TODO) {
108
+ return daysSinceActivity > staleDaysReady;
109
+ }
110
+ return false;
111
+ }
112
+ /**
113
+ * Check if a WU is archivable (done for more than N days)
114
+ */
115
+ export function isWuArchivable(wu, options) {
116
+ const { archiveDaysDone = BACKLOG_PRUNE_DEFAULTS.archiveDaysDone } = options;
117
+ // Only done WUs can be archived
118
+ if (wu.status !== WU_STATUS.DONE && wu.status !== WU_STATUS.COMPLETED) {
119
+ return false;
120
+ }
121
+ // Must have a completed date
122
+ if (!wu.completed) {
123
+ return false;
124
+ }
125
+ const daysSinceCompletion = calculateStaleDays(wu.completed);
126
+ if (daysSinceCompletion === null)
127
+ return false;
128
+ return daysSinceCompletion > archiveDaysDone;
129
+ }
130
+ /**
131
+ * Categorize WUs into stale, archivable, and healthy
132
+ */
133
+ export function categorizeWus(wus, options) {
134
+ const stale = [];
135
+ const archivable = [];
136
+ const healthy = [];
137
+ for (const wu of wus) {
138
+ if (isWuStale(wu, options)) {
139
+ stale.push(wu);
140
+ }
141
+ else if (isWuArchivable(wu, options)) {
142
+ archivable.push(wu);
143
+ }
144
+ else {
145
+ healthy.push(wu);
146
+ }
147
+ }
148
+ return { stale, archivable, healthy };
149
+ }
150
+ /**
151
+ * Load all WU YAML files from the WU directory
152
+ * @internal Exported for testing
153
+ */
154
+ export function loadAllWus() {
155
+ const wuDir = WU_PATHS.WU_DIR();
156
+ if (!existsSync(wuDir)) {
157
+ return [];
158
+ }
159
+ const files = readdirSync(wuDir).filter((f) => f.endsWith('.yaml'));
160
+ const wus = [];
161
+ for (const file of files) {
162
+ const filePath = path.join(wuDir, file);
163
+ try {
164
+ const doc = readWURaw(filePath);
165
+ if (doc && doc.id) {
166
+ wus.push({
167
+ id: doc.id,
168
+ status: doc.status || 'unknown',
169
+ title: doc.title,
170
+ created: doc.created,
171
+ updated: doc.updated,
172
+ completed: doc.completed,
173
+ });
174
+ }
175
+ }
176
+ catch {
177
+ // Skip invalid YAML files
178
+ }
179
+ }
180
+ return wus;
181
+ }
182
+ /**
183
+ * Tag a stale WU by appending a note
184
+ * @internal Exported for testing
185
+ */
186
+ export function tagStaleWu(wu, dryRun) {
187
+ const wuPath = WU_PATHS.WU(wu.id);
188
+ const today = new Date().toISOString().split('T')[0];
189
+ const note = `[${today}] Auto-tagged as stale by backlog:prune`;
190
+ if (dryRun) {
191
+ console.log(`${LOG_PREFIX} ${EMOJI.INFO} Would tag ${wu.id} as stale`);
192
+ return;
193
+ }
194
+ try {
195
+ const doc = readWURaw(wuPath);
196
+ appendNote(doc, note);
197
+ writeWU(wuPath, doc);
198
+ console.log(`${LOG_PREFIX} ${EMOJI.SUCCESS} Tagged ${wu.id} as stale`);
199
+ }
200
+ catch (err) {
201
+ console.error(`${LOG_PREFIX} ${EMOJI.FAILURE} Failed to tag ${wu.id}: ${err instanceof Error ? err.message : String(err)}`);
202
+ }
203
+ }
204
+ /**
205
+ * Print help text
206
+ * @internal Exported for testing
207
+ */
208
+ export function printHelp() {
209
+ console.log(`
210
+ ${LOG_PREFIX} Backlog Prune - Maintain backlog hygiene
211
+
212
+ Usage:
213
+ pnpm backlog:prune # Dry-run mode (default, shows what would be done)
214
+ pnpm backlog:prune --execute # Apply changes
215
+
216
+ Options:
217
+ --execute Execute changes (default is dry-run)
218
+ --dry-run Show what would be done without making changes
219
+ --stale-days-in-progress N Days before in_progress WU is stale (default: ${BACKLOG_PRUNE_DEFAULTS.staleDaysInProgress})
220
+ --stale-days-ready N Days before ready WU is stale (default: ${BACKLOG_PRUNE_DEFAULTS.staleDaysReady})
221
+ --archive-days N Days after completion before archiving (default: ${BACKLOG_PRUNE_DEFAULTS.archiveDaysDone})
222
+ --help, -h Show this help message
223
+
224
+ This tool:
225
+ ${EMOJI.SUCCESS} Identifies stale WUs (in_progress/ready too long without activity)
226
+ ${EMOJI.SUCCESS} Identifies archivable WUs (completed > N days ago)
227
+ ${EMOJI.SUCCESS} Auto-tags stale WUs with timestamped notes
228
+ ${EMOJI.SUCCESS} Safe to run regularly (dry-run by default)
229
+ `);
230
+ }
231
+ /**
232
+ * Main function
233
+ */
234
+ async function main() {
235
+ const args = parseBacklogPruneArgs(process.argv);
236
+ if (args.help) {
237
+ printHelp();
238
+ process.exit(EXIT_CODES.SUCCESS);
239
+ }
240
+ console.log(`${LOG_PREFIX} Backlog Hygiene Check`);
241
+ console.log(`${LOG_PREFIX} =====================${STRING_LITERALS.NEWLINE}`);
242
+ if (args.dryRun) {
243
+ console.log(`${LOG_PREFIX} ${EMOJI.INFO} DRY-RUN MODE (use --execute to apply changes)${STRING_LITERALS.NEWLINE}`);
244
+ }
245
+ // Load all WUs
246
+ const wus = loadAllWus();
247
+ console.log(`${LOG_PREFIX} Found ${wus.length} WU(s)${STRING_LITERALS.NEWLINE}`);
248
+ if (wus.length === 0) {
249
+ console.log(`${LOG_PREFIX} ${EMOJI.SUCCESS} No WUs to analyze`);
250
+ process.exit(EXIT_CODES.SUCCESS);
251
+ }
252
+ // Categorize WUs
253
+ const categorization = categorizeWus(wus, {
254
+ staleDaysInProgress: args.staleDaysInProgress,
255
+ staleDaysReady: args.staleDaysReady,
256
+ archiveDaysDone: args.archiveDaysDone,
257
+ });
258
+ // Report stale WUs
259
+ if (categorization.stale.length > 0) {
260
+ console.log(`${LOG_PREFIX} ${EMOJI.WARNING} Stale WUs (${categorization.stale.length}):`);
261
+ for (const wu of categorization.stale) {
262
+ const lastActivity = wu.updated || wu.created;
263
+ const days = calculateStaleDays(lastActivity);
264
+ console.log(` - ${wu.id}: ${wu.title || 'Untitled'} (${wu.status}, ${days} days since activity)`);
265
+ tagStaleWu(wu, args.dryRun);
266
+ }
267
+ console.log('');
268
+ }
269
+ // Report archivable WUs
270
+ if (categorization.archivable.length > 0) {
271
+ console.log(`${LOG_PREFIX} ${EMOJI.INFO} Archivable WUs (${categorization.archivable.length}):`);
272
+ for (const wu of categorization.archivable) {
273
+ const days = calculateStaleDays(wu.completed);
274
+ console.log(` - ${wu.id}: ${wu.title || 'Untitled'} (completed ${days} days ago)`);
275
+ }
276
+ console.log(`${LOG_PREFIX} ${EMOJI.INFO} Archive functionality not yet implemented. Consider manual cleanup.`);
277
+ console.log('');
278
+ }
279
+ // Summary
280
+ console.log(`${LOG_PREFIX} Summary`);
281
+ console.log(`${LOG_PREFIX} ========`);
282
+ console.log(`${LOG_PREFIX} Total WUs: ${wus.length}`);
283
+ console.log(`${LOG_PREFIX} Stale: ${categorization.stale.length}`);
284
+ console.log(`${LOG_PREFIX} Archivable: ${categorization.archivable.length}`);
285
+ console.log(`${LOG_PREFIX} Healthy: ${categorization.healthy.length}`);
286
+ if (categorization.stale.length === 0 && categorization.archivable.length === 0) {
287
+ console.log(`${STRING_LITERALS.NEWLINE}${LOG_PREFIX} ${EMOJI.SUCCESS} Backlog is healthy!`);
288
+ }
289
+ else if (args.dryRun) {
290
+ console.log(`${STRING_LITERALS.NEWLINE}${LOG_PREFIX} ${EMOJI.INFO} This was a dry-run. Use --execute to apply changes.`);
291
+ }
292
+ process.exit(EXIT_CODES.SUCCESS);
293
+ }
294
+ // Guard main() for testability
295
+ import { fileURLToPath } from 'node:url';
296
+ import { runCLI } from './cli-entry-point.js';
297
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
298
+ runCLI(main);
299
+ }
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Deps Add CLI Command
4
+ *
5
+ * Safe wrapper for `pnpm add` that enforces worktree discipline.
6
+ * Dependencies can only be added from within a worktree, not from main checkout.
7
+ *
8
+ * WU-1112: INIT-003 Phase 6 - Migrate remaining Tier 1 tools
9
+ *
10
+ * Usage:
11
+ * pnpm deps:add react
12
+ * pnpm deps:add --dev vitest
13
+ * pnpm deps:add --filter @lumenflow/cli chalk
14
+ *
15
+ * @see dependency-guard.ts for blocking logic
16
+ */
17
+ import { execSync } from 'node:child_process';
18
+ import { STDIO_MODES, EXIT_CODES, PKG_MANAGER, DEFAULTS, } from '@lumenflow/core/dist/wu-constants.js';
19
+ import { runCLI } from './cli-entry-point.js';
20
+ /** Log prefix for console output */
21
+ const LOG_PREFIX = '[deps:add]';
22
+ /**
23
+ * Parse command line arguments for deps-add
24
+ *
25
+ * @param argv - Process argv array
26
+ * @returns Parsed arguments
27
+ */
28
+ export function parseDepsAddArgs(argv) {
29
+ const args = {
30
+ packages: [],
31
+ };
32
+ // Skip node and script name
33
+ const cliArgs = argv.slice(2);
34
+ for (let i = 0; i < cliArgs.length; i++) {
35
+ const arg = cliArgs[i];
36
+ if (arg === '--help' || arg === '-h') {
37
+ args.help = true;
38
+ }
39
+ else if (arg === '--dev' || arg === '-D') {
40
+ args.dev = true;
41
+ }
42
+ else if (arg === '--exact' || arg === '-E') {
43
+ args.exact = true;
44
+ }
45
+ else if (arg === '--filter' || arg === '-F') {
46
+ args.filter = cliArgs[++i];
47
+ }
48
+ else if (!arg.startsWith('-')) {
49
+ // Positional argument - package name
50
+ args.packages.push(arg);
51
+ }
52
+ }
53
+ return args;
54
+ }
55
+ /**
56
+ * Parse command line arguments for deps-remove
57
+ *
58
+ * @param argv - Process argv array
59
+ * @returns Parsed arguments
60
+ */
61
+ export function parseDepsRemoveArgs(argv) {
62
+ const args = {
63
+ packages: [],
64
+ };
65
+ // Skip node and script name
66
+ const cliArgs = argv.slice(2);
67
+ for (let i = 0; i < cliArgs.length; i++) {
68
+ const arg = cliArgs[i];
69
+ if (arg === '--help' || arg === '-h') {
70
+ args.help = true;
71
+ }
72
+ else if (arg === '--filter' || arg === '-F') {
73
+ args.filter = cliArgs[++i];
74
+ }
75
+ else if (!arg.startsWith('-')) {
76
+ // Positional argument - package name
77
+ args.packages.push(arg);
78
+ }
79
+ }
80
+ return args;
81
+ }
82
+ /**
83
+ * Validate that the current directory is within a worktree
84
+ *
85
+ * Dependencies should only be modified in worktrees to maintain
86
+ * isolation and prevent lockfile conflicts on main checkout.
87
+ *
88
+ * @param cwd - Current working directory to validate
89
+ * @returns Validation result with error and fix command if invalid
90
+ */
91
+ export function validateWorktreeContext(cwd) {
92
+ const worktreesDir = `/${DEFAULTS.WORKTREES_DIR}/`;
93
+ if (cwd.includes(worktreesDir)) {
94
+ return { valid: true };
95
+ }
96
+ return {
97
+ valid: false,
98
+ error: `Cannot modify dependencies on main checkout.\n\nReason: Running pnpm add/remove on main bypasses worktree isolation.\nThis can cause lockfile conflicts and block other agents.`,
99
+ fixCommand: `1. Claim a WU: pnpm wu:claim --id WU-XXXX --lane "Your Lane"\n2. cd worktrees/<lane>-wu-<id>/\n3. Run deps:add from the worktree`,
100
+ };
101
+ }
102
+ /**
103
+ * Build pnpm add command string from arguments
104
+ *
105
+ * @param args - Parsed deps-add arguments
106
+ * @returns Command string ready for execution
107
+ */
108
+ export function buildPnpmAddCommand(args) {
109
+ const parts = [PKG_MANAGER, 'add'];
110
+ if (args.filter) {
111
+ parts.push('--filter', args.filter);
112
+ }
113
+ if (args.dev) {
114
+ parts.push('--save-dev');
115
+ }
116
+ if (args.exact) {
117
+ parts.push('--save-exact');
118
+ }
119
+ if (args.packages && args.packages.length > 0) {
120
+ parts.push(...args.packages);
121
+ }
122
+ return parts.join(' ');
123
+ }
124
+ /**
125
+ * Build pnpm remove command string from arguments
126
+ *
127
+ * @param args - Parsed deps-remove arguments
128
+ * @returns Command string ready for execution
129
+ */
130
+ export function buildPnpmRemoveCommand(args) {
131
+ const parts = [PKG_MANAGER, 'remove'];
132
+ if (args.filter) {
133
+ parts.push('--filter', args.filter);
134
+ }
135
+ if (args.packages && args.packages.length > 0) {
136
+ parts.push(...args.packages);
137
+ }
138
+ return parts.join(' ');
139
+ }
140
+ /**
141
+ * Print help message for deps-add
142
+ */
143
+ /* istanbul ignore next -- CLI entry point */
144
+ function printHelp() {
145
+ console.log(`
146
+ Usage: deps-add <packages...> [options]
147
+
148
+ Add dependencies with worktree discipline enforcement.
149
+ Must be run from inside a worktree (not main checkout).
150
+
151
+ Arguments:
152
+ packages Package names to add (e.g., react react-dom)
153
+
154
+ Options:
155
+ -D, --dev Add as dev dependency
156
+ -F, --filter <pkg> Filter to specific workspace package
157
+ -E, --exact Use exact version (--save-exact)
158
+ -h, --help Show this help message
159
+
160
+ Examples:
161
+ deps-add react # Add react to root
162
+ deps-add --dev vitest # Add vitest as dev dependency
163
+ deps-add -F @lumenflow/cli chalk # Add chalk to @lumenflow/cli
164
+ deps-add --exact react@18.2.0 # Add exact version
165
+
166
+ Worktree Discipline:
167
+ This command only works inside a worktree to prevent lockfile
168
+ conflicts on main checkout. Claim a WU first:
169
+
170
+ pnpm wu:claim --id WU-XXXX --lane "Your Lane"
171
+ cd worktrees/<lane>-wu-<id>/
172
+ deps-add <package>
173
+ `);
174
+ }
175
+ /**
176
+ * Main entry point for deps-add command
177
+ */
178
+ /* istanbul ignore next -- CLI entry point */
179
+ async function main() {
180
+ const args = parseDepsAddArgs(process.argv);
181
+ if (args.help) {
182
+ printHelp();
183
+ process.exit(EXIT_CODES.SUCCESS);
184
+ }
185
+ if (!args.packages || args.packages.length === 0) {
186
+ console.error(`${LOG_PREFIX} Error: No packages specified`);
187
+ printHelp();
188
+ process.exit(EXIT_CODES.ERROR);
189
+ }
190
+ // Validate worktree context
191
+ const validation = validateWorktreeContext(process.cwd());
192
+ if (!validation.valid) {
193
+ console.error(`${LOG_PREFIX} ${validation.error}`);
194
+ console.error(`\nTo fix:\n${validation.fixCommand}`);
195
+ process.exit(EXIT_CODES.ERROR);
196
+ }
197
+ // Build and execute pnpm add command
198
+ const command = buildPnpmAddCommand(args);
199
+ console.log(`${LOG_PREFIX} Running: ${command}`);
200
+ try {
201
+ execSync(command, {
202
+ stdio: STDIO_MODES.INHERIT,
203
+ cwd: process.cwd(),
204
+ });
205
+ console.log(`${LOG_PREFIX} ✅ Dependencies added successfully`);
206
+ }
207
+ catch (error) {
208
+ console.error(`${LOG_PREFIX} ❌ Failed to add dependencies`);
209
+ process.exit(EXIT_CODES.ERROR);
210
+ }
211
+ }
212
+ // Run main if executed directly
213
+ if (import.meta.main) {
214
+ runCLI(main);
215
+ }