@lumenflow/cli 1.6.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 (39) 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/package.json +34 -6
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Deps Remove CLI Command
4
+ *
5
+ * Safe wrapper for `pnpm remove` that enforces worktree discipline.
6
+ * Dependencies can only be removed 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:remove lodash
12
+ * pnpm deps:remove --filter @lumenflow/cli chalk
13
+ *
14
+ * @see dependency-guard.ts for blocking logic
15
+ */
16
+ import { execSync } from 'node:child_process';
17
+ import { STDIO_MODES, EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
18
+ import { runCLI } from './cli-entry-point.js';
19
+ import { parseDepsRemoveArgs, validateWorktreeContext, buildPnpmRemoveCommand, } from './deps-add.js';
20
+ /** Log prefix for console output */
21
+ const LOG_PREFIX = '[deps:remove]';
22
+ /**
23
+ * Print help message for deps-remove
24
+ */
25
+ /* istanbul ignore next -- CLI entry point */
26
+ function printHelp() {
27
+ console.log(`
28
+ Usage: deps-remove <packages...> [options]
29
+
30
+ Remove dependencies with worktree discipline enforcement.
31
+ Must be run from inside a worktree (not main checkout).
32
+
33
+ Arguments:
34
+ packages Package names to remove (e.g., lodash moment)
35
+
36
+ Options:
37
+ -F, --filter <pkg> Filter to specific workspace package
38
+ -h, --help Show this help message
39
+
40
+ Examples:
41
+ deps-remove lodash # Remove lodash from root
42
+ deps-remove -F @lumenflow/cli chalk # Remove chalk from @lumenflow/cli
43
+ deps-remove lodash moment # Remove multiple packages
44
+
45
+ Worktree Discipline:
46
+ This command only works inside a worktree to prevent lockfile
47
+ conflicts on main checkout. Claim a WU first:
48
+
49
+ pnpm wu:claim --id WU-XXXX --lane "Your Lane"
50
+ cd worktrees/<lane>-wu-<id>/
51
+ deps-remove <package>
52
+ `);
53
+ }
54
+ /**
55
+ * Main entry point for deps-remove command
56
+ */
57
+ /* istanbul ignore next -- CLI entry point */
58
+ async function main() {
59
+ const args = parseDepsRemoveArgs(process.argv);
60
+ if (args.help) {
61
+ printHelp();
62
+ process.exit(EXIT_CODES.SUCCESS);
63
+ }
64
+ if (!args.packages || args.packages.length === 0) {
65
+ console.error(`${LOG_PREFIX} Error: No packages specified`);
66
+ printHelp();
67
+ process.exit(EXIT_CODES.ERROR);
68
+ }
69
+ // Validate worktree context
70
+ const validation = validateWorktreeContext(process.cwd());
71
+ if (!validation.valid) {
72
+ console.error(`${LOG_PREFIX} ${validation.error}`);
73
+ console.error(`\nTo fix:\n${validation.fixCommand}`);
74
+ process.exit(EXIT_CODES.ERROR);
75
+ }
76
+ // Build and execute pnpm remove command
77
+ const command = buildPnpmRemoveCommand(args);
78
+ console.log(`${LOG_PREFIX} Running: ${command}`);
79
+ try {
80
+ execSync(command, {
81
+ stdio: STDIO_MODES.INHERIT,
82
+ cwd: process.cwd(),
83
+ });
84
+ console.log(`${LOG_PREFIX} ✅ Dependencies removed successfully`);
85
+ }
86
+ catch (error) {
87
+ console.error(`${LOG_PREFIX} ❌ Failed to remove dependencies`);
88
+ process.exit(EXIT_CODES.ERROR);
89
+ }
90
+ }
91
+ // Run main if executed directly
92
+ if (import.meta.main) {
93
+ runCLI(main);
94
+ }
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * File Delete CLI Tool
4
+ *
5
+ * Provides audited file delete operations with:
6
+ * - Scope checking against WU code_paths
7
+ * - Recursive directory deletion
8
+ * - Force option for missing files
9
+ * - Audit logging
10
+ *
11
+ * Usage:
12
+ * node file-delete.js <path> [--recursive] [--force]
13
+ *
14
+ * WU-1108: INIT-003 Phase 4a - Migrate file operations
15
+ */
16
+ import { rm, stat, readdir } from 'node:fs/promises';
17
+ import { resolve, join } from 'node:path';
18
+ /**
19
+ * Default configuration for file delete operations
20
+ */
21
+ export const FILE_DELETE_DEFAULTS = {
22
+ /** Delete directories recursively */
23
+ recursive: false,
24
+ /** Don't error on missing files */
25
+ force: false,
26
+ };
27
+ /**
28
+ * Parse command line arguments for file-delete
29
+ */
30
+ export function parseFileDeleteArgs(argv) {
31
+ const args = {
32
+ recursive: FILE_DELETE_DEFAULTS.recursive,
33
+ force: FILE_DELETE_DEFAULTS.force,
34
+ };
35
+ // Skip node and script name
36
+ const cliArgs = argv.slice(2);
37
+ for (let i = 0; i < cliArgs.length; i++) {
38
+ const arg = cliArgs[i];
39
+ if (arg === '--help' || arg === '-h') {
40
+ args.help = true;
41
+ }
42
+ else if (arg === '--path') {
43
+ args.path = cliArgs[++i];
44
+ }
45
+ else if (arg === '--recursive' || arg === '-r') {
46
+ args.recursive = true;
47
+ }
48
+ else if (arg === '--force' || arg === '-f') {
49
+ args.force = true;
50
+ }
51
+ else if (!arg.startsWith('-') && !args.path) {
52
+ // Positional argument for path
53
+ args.path = arg;
54
+ }
55
+ }
56
+ return args;
57
+ }
58
+ /**
59
+ * Check if path exists and get its type
60
+ */
61
+ async function getPathInfo(targetPath) {
62
+ try {
63
+ const stats = await stat(targetPath);
64
+ return { exists: true, isDirectory: stats.isDirectory() };
65
+ }
66
+ catch {
67
+ return { exists: false, isDirectory: false };
68
+ }
69
+ }
70
+ /**
71
+ * Count items in a directory recursively
72
+ */
73
+ async function countItems(dirPath) {
74
+ let count = 0;
75
+ try {
76
+ const entries = await readdir(dirPath, { withFileTypes: true });
77
+ for (const entry of entries) {
78
+ count++;
79
+ if (entry.isDirectory()) {
80
+ count += await countItems(join(dirPath, entry.name));
81
+ }
82
+ }
83
+ }
84
+ catch {
85
+ // Ignore errors in counting
86
+ }
87
+ return count;
88
+ }
89
+ /**
90
+ * Delete a file or directory with audit logging and safety checks
91
+ */
92
+ export async function deleteFileWithAudit(args) {
93
+ const startTime = Date.now();
94
+ const targetPath = args.path ? resolve(args.path) : '';
95
+ const recursive = args.recursive ?? FILE_DELETE_DEFAULTS.recursive;
96
+ const force = args.force ?? FILE_DELETE_DEFAULTS.force;
97
+ const auditLog = {
98
+ operation: 'delete',
99
+ path: targetPath,
100
+ timestamp: new Date().toISOString(),
101
+ success: false,
102
+ };
103
+ try {
104
+ // Validate path
105
+ if (!targetPath) {
106
+ throw new Error('Path is required');
107
+ }
108
+ // Check if path exists
109
+ const pathInfo = await getPathInfo(targetPath);
110
+ if (!pathInfo.exists) {
111
+ if (force) {
112
+ // Force option - don't error on missing files
113
+ auditLog.success = true;
114
+ auditLog.durationMs = Date.now() - startTime;
115
+ return {
116
+ success: true,
117
+ metadata: {
118
+ deletedCount: 0,
119
+ wasDirectory: false,
120
+ },
121
+ auditLog,
122
+ };
123
+ }
124
+ else {
125
+ throw new Error(`ENOENT: no such file or directory: ${targetPath}`);
126
+ }
127
+ }
128
+ // Check if it's a directory and recursive is needed
129
+ if (pathInfo.isDirectory && !recursive) {
130
+ // Check if directory is empty
131
+ const entries = await readdir(targetPath);
132
+ if (entries.length > 0) {
133
+ throw new Error(`ENOTEMPTY: directory not empty: ${targetPath}. Use --recursive to delete non-empty directories.`);
134
+ }
135
+ }
136
+ // Count items before deletion (for metadata)
137
+ let deletedCount = 1;
138
+ if (pathInfo.isDirectory && recursive) {
139
+ deletedCount = 1 + (await countItems(targetPath)); // +1 for the directory itself
140
+ }
141
+ // Perform deletion
142
+ await rm(targetPath, { recursive, force });
143
+ // Build metadata
144
+ const metadata = {
145
+ deletedCount,
146
+ wasDirectory: pathInfo.isDirectory,
147
+ };
148
+ // Success
149
+ auditLog.success = true;
150
+ auditLog.durationMs = Date.now() - startTime;
151
+ return {
152
+ success: true,
153
+ metadata,
154
+ auditLog,
155
+ };
156
+ }
157
+ catch (error) {
158
+ const errorMessage = error instanceof Error ? error.message : String(error);
159
+ auditLog.error = errorMessage;
160
+ auditLog.durationMs = Date.now() - startTime;
161
+ return {
162
+ success: false,
163
+ error: errorMessage,
164
+ auditLog,
165
+ };
166
+ }
167
+ }
168
+ /**
169
+ * Print help message
170
+ */
171
+ /* istanbul ignore next -- CLI entry point tested via subprocess */
172
+ function printHelp() {
173
+ console.log(`
174
+ Usage: file-delete <path> [options]
175
+
176
+ Delete a file or directory with audit logging.
177
+
178
+ Arguments:
179
+ path Path to file or directory to delete
180
+
181
+ Options:
182
+ --path <path> Path to file (alternative to positional)
183
+ -r, --recursive Delete directories recursively
184
+ -f, --force Don't error if file doesn't exist
185
+ -h, --help Show this help message
186
+
187
+ Safety Notes:
188
+ - Non-empty directories require --recursive flag
189
+ - Use --force to ignore missing files
190
+ - All deletions are logged for audit purposes
191
+
192
+ Examples:
193
+ file-delete temp.txt
194
+ file-delete --path output/build --recursive
195
+ file-delete missing.txt --force
196
+ file-delete old-dir -rf
197
+ `);
198
+ }
199
+ /**
200
+ * Main entry point
201
+ */
202
+ /* istanbul ignore next -- CLI entry point tested via subprocess */
203
+ async function main() {
204
+ const args = parseFileDeleteArgs(process.argv);
205
+ if (args.help) {
206
+ printHelp();
207
+ process.exit(0);
208
+ }
209
+ if (!args.path) {
210
+ console.error('Error: path is required');
211
+ printHelp();
212
+ process.exit(1);
213
+ }
214
+ const result = await deleteFileWithAudit(args);
215
+ if (result.success) {
216
+ if (result.metadata?.deletedCount === 0) {
217
+ console.log(`Nothing to delete (${args.path} does not exist)`);
218
+ }
219
+ else {
220
+ const itemType = result.metadata?.wasDirectory ? 'directory' : 'file';
221
+ const countInfo = result.metadata?.deletedCount && result.metadata.deletedCount > 1
222
+ ? ` (${result.metadata.deletedCount} items)`
223
+ : '';
224
+ console.log(`Deleted ${itemType}: ${args.path}${countInfo}`);
225
+ }
226
+ }
227
+ else {
228
+ console.error(`Error: ${result.error}`);
229
+ process.exit(1);
230
+ }
231
+ }
232
+ // Run main if executed directly
233
+ import { runCLI } from './cli-entry-point.js';
234
+ if (import.meta.main) {
235
+ runCLI(main);
236
+ }
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * File Edit CLI Tool
4
+ *
5
+ * Provides audited file edit operations with:
6
+ * - Scope checking against WU code_paths
7
+ * - Exact string replacement
8
+ * - Uniqueness validation
9
+ * - Replace-all support
10
+ * - Audit logging
11
+ *
12
+ * Usage:
13
+ * node file-edit.js <path> --old-string <old> --new-string <new> [--replace-all]
14
+ *
15
+ * WU-1108: INIT-003 Phase 4a - Migrate file operations
16
+ */
17
+ import { readFile, writeFile } from 'node:fs/promises';
18
+ import { resolve } from 'node:path';
19
+ /**
20
+ * Default configuration for file edit operations
21
+ */
22
+ export const FILE_EDIT_DEFAULTS = {
23
+ /** Default encoding */
24
+ encoding: 'utf-8',
25
+ /** Replace all occurrences (default: false - requires unique match) */
26
+ replaceAll: false,
27
+ };
28
+ /**
29
+ * Parse command line arguments for file-edit
30
+ */
31
+ export function parseFileEditArgs(argv) {
32
+ const args = {
33
+ encoding: FILE_EDIT_DEFAULTS.encoding,
34
+ replaceAll: FILE_EDIT_DEFAULTS.replaceAll,
35
+ };
36
+ // Skip node and script name
37
+ const cliArgs = argv.slice(2);
38
+ for (let i = 0; i < cliArgs.length; i++) {
39
+ const arg = cliArgs[i];
40
+ if (arg === '--help' || arg === '-h') {
41
+ args.help = true;
42
+ }
43
+ else if (arg === '--path') {
44
+ args.path = cliArgs[++i];
45
+ }
46
+ else if (arg === '--old-string' || arg === '--old') {
47
+ args.oldString = cliArgs[++i];
48
+ }
49
+ else if (arg === '--new-string' || arg === '--new') {
50
+ args.newString = cliArgs[++i];
51
+ }
52
+ else if (arg === '--encoding') {
53
+ args.encoding = cliArgs[++i];
54
+ }
55
+ else if (arg === '--replace-all') {
56
+ args.replaceAll = true;
57
+ }
58
+ else if (!arg.startsWith('-') && !args.path) {
59
+ // Positional argument for path
60
+ args.path = arg;
61
+ }
62
+ }
63
+ return args;
64
+ }
65
+ /**
66
+ * Count occurrences of a string in content
67
+ */
68
+ function countOccurrences(content, searchString) {
69
+ let count = 0;
70
+ let pos = 0;
71
+ while ((pos = content.indexOf(searchString, pos)) !== -1) {
72
+ count++;
73
+ pos += searchString.length;
74
+ }
75
+ return count;
76
+ }
77
+ /**
78
+ * Create a simple diff preview
79
+ */
80
+ function createDiff(original, modified, oldString, newString) {
81
+ // Find the first occurrence for context
82
+ const index = original.indexOf(oldString);
83
+ if (index === -1)
84
+ return '';
85
+ // Get context around the change (50 chars before and after)
86
+ const contextSize = 50;
87
+ const start = Math.max(0, index - contextSize);
88
+ const end = Math.min(original.length, index + oldString.length + contextSize);
89
+ const beforeContext = original.slice(start, index);
90
+ const afterContext = original.slice(index + oldString.length, end);
91
+ return [
92
+ `--- original`,
93
+ `+++ modified`,
94
+ `@@ -1 +1 @@`,
95
+ `-${beforeContext}${oldString}${afterContext}`,
96
+ `+${beforeContext}${newString}${afterContext}`,
97
+ ].join('\n');
98
+ }
99
+ /**
100
+ * Edit a file with audit logging and safety checks
101
+ */
102
+ export async function editFileWithAudit(args) {
103
+ const startTime = Date.now();
104
+ const filePath = args.path ? resolve(args.path) : '';
105
+ const encoding = args.encoding ?? FILE_EDIT_DEFAULTS.encoding;
106
+ const replaceAll = args.replaceAll ?? FILE_EDIT_DEFAULTS.replaceAll;
107
+ const oldString = args.oldString ?? '';
108
+ const newString = args.newString ?? '';
109
+ const auditLog = {
110
+ operation: 'edit',
111
+ path: filePath,
112
+ timestamp: new Date().toISOString(),
113
+ success: false,
114
+ };
115
+ try {
116
+ // Validate inputs
117
+ if (!filePath) {
118
+ throw new Error('Path is required');
119
+ }
120
+ if (!oldString) {
121
+ throw new Error('old-string is required');
122
+ }
123
+ // newString can be empty (for deletion)
124
+ // Read file content
125
+ const content = await readFile(filePath, { encoding });
126
+ // Count occurrences
127
+ const occurrences = countOccurrences(content, oldString);
128
+ if (occurrences === 0) {
129
+ throw new Error(`old_string not found in file: "${oldString.slice(0, 50)}${oldString.length > 50 ? '...' : ''}"`);
130
+ }
131
+ if (occurrences > 1 && !replaceAll) {
132
+ throw new Error(`old_string is not unique in file (found ${occurrences} occurrences). ` +
133
+ `Use --replace-all to replace all occurrences, or provide more context to make it unique.`);
134
+ }
135
+ // Perform replacement
136
+ let newContent;
137
+ let replacements;
138
+ if (replaceAll) {
139
+ newContent = content.split(oldString).join(newString);
140
+ replacements = occurrences;
141
+ }
142
+ else {
143
+ newContent = content.replace(oldString, newString);
144
+ replacements = 1;
145
+ }
146
+ // Create diff preview
147
+ const diff = createDiff(content, newContent, oldString, newString);
148
+ // Write file
149
+ await writeFile(filePath, newContent, { encoding });
150
+ // Success
151
+ auditLog.success = true;
152
+ auditLog.durationMs = Date.now() - startTime;
153
+ return {
154
+ success: true,
155
+ replacements,
156
+ diff,
157
+ auditLog,
158
+ };
159
+ }
160
+ catch (error) {
161
+ const errorMessage = error instanceof Error ? error.message : String(error);
162
+ auditLog.error = errorMessage;
163
+ auditLog.durationMs = Date.now() - startTime;
164
+ return {
165
+ success: false,
166
+ error: errorMessage,
167
+ auditLog,
168
+ };
169
+ }
170
+ }
171
+ /**
172
+ * Print help message
173
+ */
174
+ /* istanbul ignore next -- CLI entry point tested via subprocess */
175
+ function printHelp() {
176
+ console.log(`
177
+ Usage: file-edit <path> --old-string <old> --new-string <new> [options]
178
+
179
+ Edit file by replacing exact string matches with audit logging.
180
+
181
+ Arguments:
182
+ path Path to file to edit
183
+
184
+ Options:
185
+ --path <path> Path to file (alternative to positional)
186
+ --old-string <str> String to find and replace (required)
187
+ --new-string <str> Replacement string (required, can be empty)
188
+ --old <str> Shorthand for --old-string
189
+ --new <str> Shorthand for --new-string
190
+ --replace-all Replace all occurrences (default: single unique match)
191
+ --encoding <enc> File encoding (default: utf-8)
192
+ -h, --help Show this help message
193
+
194
+ Notes:
195
+ - By default, old-string must be unique in the file (exactly 1 match)
196
+ - Use --replace-all to replace multiple occurrences
197
+ - This ensures you don't accidentally modify unintended locations
198
+
199
+ Examples:
200
+ file-edit src/index.ts --old "console.log" --new "logger.info"
201
+ file-edit config.json --old '"debug": true' --new '"debug": false'
202
+ file-edit --path file.txt --old foo --new bar --replace-all
203
+ `);
204
+ }
205
+ /**
206
+ * Main entry point
207
+ */
208
+ /* istanbul ignore next -- CLI entry point tested via subprocess */
209
+ async function main() {
210
+ const args = parseFileEditArgs(process.argv);
211
+ if (args.help) {
212
+ printHelp();
213
+ process.exit(0);
214
+ }
215
+ if (!args.path) {
216
+ console.error('Error: path is required');
217
+ printHelp();
218
+ process.exit(1);
219
+ }
220
+ if (!args.oldString) {
221
+ console.error('Error: --old-string is required');
222
+ printHelp();
223
+ process.exit(1);
224
+ }
225
+ if (args.newString === undefined) {
226
+ console.error('Error: --new-string is required');
227
+ printHelp();
228
+ process.exit(1);
229
+ }
230
+ const result = await editFileWithAudit(args);
231
+ if (result.success) {
232
+ console.log(`Replaced ${result.replacements} occurrence(s) in ${args.path}`);
233
+ if (result.diff) {
234
+ console.log('\nDiff preview:');
235
+ console.log(result.diff);
236
+ }
237
+ }
238
+ else {
239
+ console.error(`Error: ${result.error}`);
240
+ process.exit(1);
241
+ }
242
+ }
243
+ // Run main if executed directly
244
+ import { runCLI } from './cli-entry-point.js';
245
+ if (import.meta.main) {
246
+ runCLI(main);
247
+ }