@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.
- 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/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/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 +34 -6
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* File Write CLI Tool
|
|
4
|
+
*
|
|
5
|
+
* Provides audited file write operations with:
|
|
6
|
+
* - Scope checking against WU code_paths
|
|
7
|
+
* - PHI scanning (optional)
|
|
8
|
+
* - Directory creation
|
|
9
|
+
* - Audit logging
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node file-write.js <path> --content <content> [--encoding utf-8]
|
|
13
|
+
*
|
|
14
|
+
* WU-1108: INIT-003 Phase 4a - Migrate file operations
|
|
15
|
+
*/
|
|
16
|
+
import { writeFile, mkdir, stat } from 'node:fs/promises';
|
|
17
|
+
import { resolve, dirname } from 'node:path';
|
|
18
|
+
/**
|
|
19
|
+
* Default configuration for file write operations
|
|
20
|
+
*/
|
|
21
|
+
export const FILE_WRITE_DEFAULTS = {
|
|
22
|
+
/** Default encoding */
|
|
23
|
+
encoding: 'utf-8',
|
|
24
|
+
/** Create parent directories if they don't exist */
|
|
25
|
+
createDirectories: true,
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Parse command line arguments for file-write
|
|
29
|
+
*/
|
|
30
|
+
export function parseFileWriteArgs(argv) {
|
|
31
|
+
const args = {
|
|
32
|
+
encoding: FILE_WRITE_DEFAULTS.encoding,
|
|
33
|
+
createDirectories: FILE_WRITE_DEFAULTS.createDirectories,
|
|
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 === '--content') {
|
|
46
|
+
args.content = cliArgs[++i];
|
|
47
|
+
}
|
|
48
|
+
else if (arg === '--encoding') {
|
|
49
|
+
args.encoding = cliArgs[++i];
|
|
50
|
+
}
|
|
51
|
+
else if (arg === '--no-create-dirs') {
|
|
52
|
+
args.createDirectories = false;
|
|
53
|
+
}
|
|
54
|
+
else if (arg === '--scan-phi') {
|
|
55
|
+
args.scanPHI = true;
|
|
56
|
+
}
|
|
57
|
+
else if (!arg.startsWith('-') && !args.path) {
|
|
58
|
+
// Positional argument for path
|
|
59
|
+
args.path = arg;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return args;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Check if directory exists
|
|
66
|
+
*/
|
|
67
|
+
async function directoryExists(dirPath) {
|
|
68
|
+
try {
|
|
69
|
+
const stats = await stat(dirPath);
|
|
70
|
+
return stats.isDirectory();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Write a file with audit logging and safety checks
|
|
78
|
+
*/
|
|
79
|
+
export async function writeFileWithAudit(args) {
|
|
80
|
+
const startTime = Date.now();
|
|
81
|
+
const filePath = args.path ? resolve(args.path) : '';
|
|
82
|
+
const encoding = args.encoding ?? FILE_WRITE_DEFAULTS.encoding;
|
|
83
|
+
const createDirs = args.createDirectories ?? FILE_WRITE_DEFAULTS.createDirectories;
|
|
84
|
+
const content = args.content ?? '';
|
|
85
|
+
const auditLog = {
|
|
86
|
+
operation: 'write',
|
|
87
|
+
path: filePath,
|
|
88
|
+
timestamp: new Date().toISOString(),
|
|
89
|
+
success: false,
|
|
90
|
+
};
|
|
91
|
+
const warnings = [];
|
|
92
|
+
try {
|
|
93
|
+
// Validate path
|
|
94
|
+
if (!filePath) {
|
|
95
|
+
throw new Error('Path is required');
|
|
96
|
+
}
|
|
97
|
+
const parentDir = dirname(filePath);
|
|
98
|
+
let directoriesCreated = false;
|
|
99
|
+
// Check parent directory
|
|
100
|
+
const parentExists = await directoryExists(parentDir);
|
|
101
|
+
if (!parentExists) {
|
|
102
|
+
if (createDirs) {
|
|
103
|
+
await mkdir(parentDir, { recursive: true });
|
|
104
|
+
directoriesCreated = true;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
throw new Error(`ENOENT: parent directory does not exist: ${parentDir}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// PHI scanning (if enabled and @lumenflow/core is available)
|
|
111
|
+
if (args.scanPHI) {
|
|
112
|
+
try {
|
|
113
|
+
// Dynamic import to avoid hard dependency
|
|
114
|
+
const { scanForPHI } = await import('@lumenflow/core/dist/validators/phi-scanner.js');
|
|
115
|
+
const scanResult = scanForPHI(content);
|
|
116
|
+
if (scanResult.hasPHI) {
|
|
117
|
+
warnings.push(`PHI detected: ${scanResult.matches.length} potential match(es). Review before committing.`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// PHI scanner not available - skip silently
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Write file
|
|
125
|
+
await writeFile(filePath, content, { encoding });
|
|
126
|
+
// Calculate bytes written
|
|
127
|
+
const bytesWritten = Buffer.byteLength(content, encoding);
|
|
128
|
+
// Build metadata
|
|
129
|
+
const metadata = {
|
|
130
|
+
bytesWritten,
|
|
131
|
+
directoriesCreated,
|
|
132
|
+
};
|
|
133
|
+
// Success
|
|
134
|
+
auditLog.success = true;
|
|
135
|
+
auditLog.durationMs = Date.now() - startTime;
|
|
136
|
+
return {
|
|
137
|
+
success: true,
|
|
138
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
139
|
+
metadata,
|
|
140
|
+
auditLog,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
145
|
+
auditLog.error = errorMessage;
|
|
146
|
+
auditLog.durationMs = Date.now() - startTime;
|
|
147
|
+
return {
|
|
148
|
+
success: false,
|
|
149
|
+
error: errorMessage,
|
|
150
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
151
|
+
auditLog,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Print help message
|
|
157
|
+
*/
|
|
158
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
159
|
+
function printHelp() {
|
|
160
|
+
console.log(`
|
|
161
|
+
Usage: file-write <path> --content <content> [options]
|
|
162
|
+
|
|
163
|
+
Write content to a file with audit logging.
|
|
164
|
+
|
|
165
|
+
Arguments:
|
|
166
|
+
path Path to file to write
|
|
167
|
+
|
|
168
|
+
Options:
|
|
169
|
+
--path <path> Path to file (alternative to positional)
|
|
170
|
+
--content <content> Content to write (required)
|
|
171
|
+
--encoding <enc> File encoding (default: utf-8)
|
|
172
|
+
--no-create-dirs Don't create parent directories
|
|
173
|
+
--scan-phi Scan content for PHI before writing
|
|
174
|
+
-h, --help Show this help message
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
file-write output.txt --content "Hello, World!"
|
|
178
|
+
file-write --path nested/dir/file.txt --content "Content"
|
|
179
|
+
file-write config.json --content '{"key": "value"}' --encoding utf-8
|
|
180
|
+
`);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Main entry point
|
|
184
|
+
*/
|
|
185
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
186
|
+
async function main() {
|
|
187
|
+
const args = parseFileWriteArgs(process.argv);
|
|
188
|
+
if (args.help) {
|
|
189
|
+
printHelp();
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
if (!args.path) {
|
|
193
|
+
console.error('Error: path is required');
|
|
194
|
+
printHelp();
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
if (args.content === undefined) {
|
|
198
|
+
console.error('Error: --content is required');
|
|
199
|
+
printHelp();
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
const result = await writeFileWithAudit(args);
|
|
203
|
+
if (result.success) {
|
|
204
|
+
console.log(`Written ${result.metadata?.bytesWritten} bytes to ${args.path}`);
|
|
205
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
206
|
+
for (const warning of result.warnings) {
|
|
207
|
+
console.warn(`Warning: ${warning}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
console.error(`Error: ${result.error}`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Run main if executed directly
|
|
217
|
+
import { runCLI } from './cli-entry-point.js';
|
|
218
|
+
if (import.meta.main) {
|
|
219
|
+
runCLI(main);
|
|
220
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Git Branch CLI Tool
|
|
4
|
+
*
|
|
5
|
+
* Provides WU-aware git branch with:
|
|
6
|
+
* - Branch listing (local, remote, all)
|
|
7
|
+
* - Current branch detection
|
|
8
|
+
* - Contains filtering
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node git-branch.js [--list] [-a] [-r] [--show-current]
|
|
12
|
+
*
|
|
13
|
+
* WU-1109: INIT-003 Phase 4b - Migrate git operations
|
|
14
|
+
*/
|
|
15
|
+
import { createGitForPath, getGitForCwd } from '@lumenflow/core';
|
|
16
|
+
/**
|
|
17
|
+
* Parse command line arguments for git-branch
|
|
18
|
+
*/
|
|
19
|
+
export function parseGitBranchArgs(argv) {
|
|
20
|
+
const args = {};
|
|
21
|
+
// Skip node and script name
|
|
22
|
+
const cliArgs = argv.slice(2);
|
|
23
|
+
for (let i = 0; i < cliArgs.length; i++) {
|
|
24
|
+
const arg = cliArgs[i];
|
|
25
|
+
if (arg === '--help' || arg === '-h') {
|
|
26
|
+
args.help = true;
|
|
27
|
+
}
|
|
28
|
+
else if (arg === '--list' || arg === '-l') {
|
|
29
|
+
args.list = true;
|
|
30
|
+
}
|
|
31
|
+
else if (arg === '--all' || arg === '-a') {
|
|
32
|
+
args.all = true;
|
|
33
|
+
}
|
|
34
|
+
else if (arg === '--remotes' || arg === '-r') {
|
|
35
|
+
args.remotes = true;
|
|
36
|
+
}
|
|
37
|
+
else if (arg === '--show-current') {
|
|
38
|
+
args.showCurrent = true;
|
|
39
|
+
}
|
|
40
|
+
else if (arg === '--contains') {
|
|
41
|
+
args.contains = cliArgs[++i];
|
|
42
|
+
}
|
|
43
|
+
else if (arg === '--base-dir') {
|
|
44
|
+
args.baseDir = cliArgs[++i];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return args;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse branch list output
|
|
51
|
+
*/
|
|
52
|
+
function parseBranchOutput(output) {
|
|
53
|
+
const branches = [];
|
|
54
|
+
const lines = output.split('\n').filter((line) => line.trim());
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (!trimmed)
|
|
58
|
+
continue;
|
|
59
|
+
// Check if current branch (starts with *)
|
|
60
|
+
const isCurrent = trimmed.startsWith('*');
|
|
61
|
+
// Check if remote branch
|
|
62
|
+
const isRemote = trimmed.includes('remotes/') || trimmed.startsWith('origin/');
|
|
63
|
+
// Extract branch name
|
|
64
|
+
let name = trimmed;
|
|
65
|
+
if (isCurrent) {
|
|
66
|
+
name = name.slice(1).trim();
|
|
67
|
+
}
|
|
68
|
+
// Remove remote prefix for display
|
|
69
|
+
if (name.startsWith('remotes/')) {
|
|
70
|
+
name = name.slice(8);
|
|
71
|
+
}
|
|
72
|
+
// Skip HEAD pointer entries
|
|
73
|
+
if (name.includes(' -> ')) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
branches.push({
|
|
77
|
+
name,
|
|
78
|
+
isCurrent,
|
|
79
|
+
isRemote,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return branches;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get git branch information
|
|
86
|
+
*/
|
|
87
|
+
export async function getGitBranch(args) {
|
|
88
|
+
try {
|
|
89
|
+
const git = args.baseDir ? createGitForPath(args.baseDir) : getGitForCwd();
|
|
90
|
+
// Show current branch only
|
|
91
|
+
if (args.showCurrent) {
|
|
92
|
+
const current = await git.getCurrentBranch();
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
current: current || undefined,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// Build branch arguments
|
|
99
|
+
const rawArgs = ['branch'];
|
|
100
|
+
if (args.all) {
|
|
101
|
+
rawArgs.push('-a');
|
|
102
|
+
}
|
|
103
|
+
else if (args.remotes) {
|
|
104
|
+
rawArgs.push('-r');
|
|
105
|
+
}
|
|
106
|
+
if (args.contains) {
|
|
107
|
+
rawArgs.push('--contains', args.contains);
|
|
108
|
+
}
|
|
109
|
+
const output = await git.raw(rawArgs);
|
|
110
|
+
const branches = parseBranchOutput(output);
|
|
111
|
+
// Find current branch
|
|
112
|
+
const currentBranch = branches.find((b) => b.isCurrent);
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
current: currentBranch?.name,
|
|
116
|
+
branches,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
error: errorMessage,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Print help message
|
|
129
|
+
*/
|
|
130
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
131
|
+
function printHelp() {
|
|
132
|
+
console.log(`
|
|
133
|
+
Usage: git-branch [options]
|
|
134
|
+
|
|
135
|
+
List, create, or delete branches.
|
|
136
|
+
|
|
137
|
+
Options:
|
|
138
|
+
--base-dir <dir> Base directory for git operations
|
|
139
|
+
--list, -l List branches
|
|
140
|
+
-a, --all List both local and remote branches
|
|
141
|
+
-r, --remotes List only remote branches
|
|
142
|
+
--show-current Print the name of the current branch
|
|
143
|
+
--contains <commit> Only list branches containing the specified commit
|
|
144
|
+
-h, --help Show this help message
|
|
145
|
+
|
|
146
|
+
Examples:
|
|
147
|
+
git-branch
|
|
148
|
+
git-branch --list
|
|
149
|
+
git-branch -a
|
|
150
|
+
git-branch --show-current
|
|
151
|
+
git-branch --contains abc123
|
|
152
|
+
`);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Main entry point
|
|
156
|
+
*/
|
|
157
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
158
|
+
async function main() {
|
|
159
|
+
const args = parseGitBranchArgs(process.argv);
|
|
160
|
+
if (args.help) {
|
|
161
|
+
printHelp();
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
const result = await getGitBranch(args);
|
|
165
|
+
if (result.success) {
|
|
166
|
+
if (args.showCurrent) {
|
|
167
|
+
if (result.current) {
|
|
168
|
+
console.log(result.current);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else if (result.branches) {
|
|
172
|
+
for (const branch of result.branches) {
|
|
173
|
+
const prefix = branch.isCurrent ? '* ' : ' ';
|
|
174
|
+
console.log(`${prefix}${branch.name}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
console.error(`Error: ${result.error}`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Run main if executed directly
|
|
184
|
+
import { runCLI } from './cli-entry-point.js';
|
|
185
|
+
if (import.meta.main) {
|
|
186
|
+
runCLI(main);
|
|
187
|
+
}
|