@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,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
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* File Read CLI Tool
|
|
4
|
+
*
|
|
5
|
+
* Provides audited file read operations with:
|
|
6
|
+
* - Scope checking against WU code_paths
|
|
7
|
+
* - File size limits
|
|
8
|
+
* - Line range support
|
|
9
|
+
* - Audit logging
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node file-read.js <path> [--encoding utf-8] [--start-line N] [--end-line M]
|
|
13
|
+
*
|
|
14
|
+
* WU-1108: INIT-003 Phase 4a - Migrate file operations
|
|
15
|
+
*/
|
|
16
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
17
|
+
import { resolve } from 'node:path';
|
|
18
|
+
/**
|
|
19
|
+
* Default configuration for file read operations
|
|
20
|
+
*/
|
|
21
|
+
export const FILE_READ_DEFAULTS = {
|
|
22
|
+
/** Maximum file size in bytes (10MB) */
|
|
23
|
+
maxFileSizeBytes: 10 * 1024 * 1024,
|
|
24
|
+
/** Default encoding */
|
|
25
|
+
encoding: 'utf-8',
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Parse command line arguments for file-read
|
|
29
|
+
*/
|
|
30
|
+
export function parseFileReadArgs(argv) {
|
|
31
|
+
const args = {
|
|
32
|
+
encoding: FILE_READ_DEFAULTS.encoding,
|
|
33
|
+
maxFileSizeBytes: FILE_READ_DEFAULTS.maxFileSizeBytes,
|
|
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 === '--encoding') {
|
|
46
|
+
args.encoding = cliArgs[++i];
|
|
47
|
+
}
|
|
48
|
+
else if (arg === '--start-line') {
|
|
49
|
+
const val = cliArgs[++i];
|
|
50
|
+
if (val)
|
|
51
|
+
args.startLine = parseInt(val, 10);
|
|
52
|
+
}
|
|
53
|
+
else if (arg === '--end-line') {
|
|
54
|
+
const val = cliArgs[++i];
|
|
55
|
+
if (val)
|
|
56
|
+
args.endLine = parseInt(val, 10);
|
|
57
|
+
}
|
|
58
|
+
else if (arg === '--max-size') {
|
|
59
|
+
const val = cliArgs[++i];
|
|
60
|
+
if (val)
|
|
61
|
+
args.maxFileSizeBytes = parseInt(val, 10);
|
|
62
|
+
}
|
|
63
|
+
else if (!arg.startsWith('-') && !args.path) {
|
|
64
|
+
// Positional argument for path
|
|
65
|
+
args.path = arg;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return args;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Extract a range of lines from content
|
|
72
|
+
*/
|
|
73
|
+
function extractLineRange(content, startLine, endLine) {
|
|
74
|
+
if (!startLine && !endLine) {
|
|
75
|
+
return content;
|
|
76
|
+
}
|
|
77
|
+
const lines = content.split('\n');
|
|
78
|
+
const start = (startLine ?? 1) - 1; // Convert to 0-based
|
|
79
|
+
const end = endLine ?? lines.length; // Keep as 1-based for slice
|
|
80
|
+
return lines.slice(start, end).join('\n');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Read a file with audit logging and safety checks
|
|
84
|
+
*/
|
|
85
|
+
export async function readFileWithAudit(args) {
|
|
86
|
+
const startTime = Date.now();
|
|
87
|
+
const filePath = args.path ? resolve(args.path) : '';
|
|
88
|
+
const encoding = args.encoding ?? FILE_READ_DEFAULTS.encoding;
|
|
89
|
+
const maxSize = args.maxFileSizeBytes ?? FILE_READ_DEFAULTS.maxFileSizeBytes;
|
|
90
|
+
const auditLog = {
|
|
91
|
+
operation: 'read',
|
|
92
|
+
path: filePath,
|
|
93
|
+
timestamp: new Date().toISOString(),
|
|
94
|
+
success: false,
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
// Validate path
|
|
98
|
+
if (!filePath) {
|
|
99
|
+
throw new Error('Path is required');
|
|
100
|
+
}
|
|
101
|
+
// Check file size before reading
|
|
102
|
+
const fileStats = await stat(filePath);
|
|
103
|
+
if (fileStats.size > maxSize) {
|
|
104
|
+
throw new Error(`File size (${fileStats.size} bytes) exceeds maximum allowed (${maxSize} bytes)`);
|
|
105
|
+
}
|
|
106
|
+
// Read file content
|
|
107
|
+
const content = await readFile(filePath, { encoding });
|
|
108
|
+
// Extract line range if specified
|
|
109
|
+
const lines = content.split('\n');
|
|
110
|
+
const resultContent = extractLineRange(content, args.startLine, args.endLine);
|
|
111
|
+
const resultLines = resultContent.split('\n');
|
|
112
|
+
// Build metadata
|
|
113
|
+
const metadata = {
|
|
114
|
+
sizeBytes: fileStats.size,
|
|
115
|
+
lineCount: lines.length,
|
|
116
|
+
};
|
|
117
|
+
if (args.startLine || args.endLine) {
|
|
118
|
+
metadata.linesReturned = resultLines.length;
|
|
119
|
+
}
|
|
120
|
+
// Success
|
|
121
|
+
auditLog.success = true;
|
|
122
|
+
auditLog.durationMs = Date.now() - startTime;
|
|
123
|
+
return {
|
|
124
|
+
success: true,
|
|
125
|
+
content: resultContent,
|
|
126
|
+
metadata,
|
|
127
|
+
auditLog,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
132
|
+
auditLog.error = errorMessage;
|
|
133
|
+
auditLog.durationMs = Date.now() - startTime;
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
error: errorMessage,
|
|
137
|
+
auditLog,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Print help message
|
|
143
|
+
*/
|
|
144
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
145
|
+
function printHelp() {
|
|
146
|
+
console.log(`
|
|
147
|
+
Usage: file-read <path> [options]
|
|
148
|
+
|
|
149
|
+
Read file content with audit logging.
|
|
150
|
+
|
|
151
|
+
Arguments:
|
|
152
|
+
path Path to file to read
|
|
153
|
+
|
|
154
|
+
Options:
|
|
155
|
+
--path <path> Path to file (alternative to positional)
|
|
156
|
+
--encoding <enc> File encoding (default: utf-8)
|
|
157
|
+
--start-line <n> Start line (1-based, inclusive)
|
|
158
|
+
--end-line <n> End line (1-based, inclusive)
|
|
159
|
+
--max-size <bytes> Maximum file size in bytes
|
|
160
|
+
-h, --help Show this help message
|
|
161
|
+
|
|
162
|
+
Examples:
|
|
163
|
+
file-read src/index.ts
|
|
164
|
+
file-read --path src/index.ts --start-line 10 --end-line 50
|
|
165
|
+
file-read config.json --encoding utf-8
|
|
166
|
+
`);
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Main entry point
|
|
170
|
+
*/
|
|
171
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
172
|
+
async function main() {
|
|
173
|
+
const args = parseFileReadArgs(process.argv);
|
|
174
|
+
if (args.help) {
|
|
175
|
+
printHelp();
|
|
176
|
+
process.exit(0);
|
|
177
|
+
}
|
|
178
|
+
if (!args.path) {
|
|
179
|
+
console.error('Error: path is required');
|
|
180
|
+
printHelp();
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
const result = await readFileWithAudit(args);
|
|
184
|
+
if (result.success) {
|
|
185
|
+
// Output content to stdout
|
|
186
|
+
console.log(result.content);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
console.error(`Error: ${result.error}`);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Run main if executed directly
|
|
194
|
+
import { runCLI } from './cli-entry-point.js';
|
|
195
|
+
if (import.meta.main) {
|
|
196
|
+
runCLI(main);
|
|
197
|
+
}
|