@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.
- package/README.md +19 -0
- package/dist/__tests__/backlog-prune.test.js +478 -0
- package/dist/__tests__/deps-operations.test.js +206 -0
- package/dist/__tests__/file-operations.test.js +906 -0
- package/dist/__tests__/git-operations.test.js +668 -0
- package/dist/__tests__/guards-validation.test.js +416 -0
- package/dist/__tests__/init-plan.test.js +340 -0
- package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
- package/dist/__tests__/metrics-cli.test.js +619 -0
- package/dist/__tests__/rotate-progress.test.js +127 -0
- package/dist/__tests__/session-coordinator.test.js +109 -0
- package/dist/__tests__/state-bootstrap.test.js +432 -0
- package/dist/__tests__/trace-gen.test.js +115 -0
- package/dist/backlog-prune.js +299 -0
- package/dist/deps-add.js +215 -0
- package/dist/deps-remove.js +94 -0
- package/dist/docs-sync.js +72 -326
- package/dist/file-delete.js +236 -0
- package/dist/file-edit.js +247 -0
- package/dist/file-read.js +197 -0
- package/dist/file-write.js +220 -0
- package/dist/git-branch.js +187 -0
- package/dist/git-diff.js +177 -0
- package/dist/git-log.js +230 -0
- package/dist/git-status.js +208 -0
- package/dist/guard-locked.js +169 -0
- package/dist/guard-main-branch.js +202 -0
- package/dist/guard-worktree-commit.js +160 -0
- package/dist/init-plan.js +337 -0
- package/dist/lumenflow-upgrade.js +178 -0
- package/dist/metrics-cli.js +433 -0
- package/dist/rotate-progress.js +247 -0
- package/dist/session-coordinator.js +300 -0
- package/dist/state-bootstrap.js +307 -0
- package/dist/sync-templates.js +212 -0
- package/dist/trace-gen.js +331 -0
- package/dist/validate-agent-skills.js +218 -0
- package/dist/validate-agent-sync.js +148 -0
- package/dist/validate-backlog-sync.js +152 -0
- package/dist/validate-skills-spec.js +206 -0
- package/dist/validate.js +230 -0
- 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
|
+
}
|
package/dist/deps-add.js
ADDED
|
@@ -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
|
+
}
|