@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
package/dist/git-diff.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Git Diff CLI Tool
|
|
4
|
+
*
|
|
5
|
+
* Provides WU-aware git diff with:
|
|
6
|
+
* - Staged/cached diff support
|
|
7
|
+
* - Name-only and stat output modes
|
|
8
|
+
* - File filtering
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node git-diff.js [ref] [--staged] [--name-only] [--stat]
|
|
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-diff
|
|
18
|
+
*/
|
|
19
|
+
export function parseGitDiffArgs(argv) {
|
|
20
|
+
const args = {};
|
|
21
|
+
// Skip node and script name
|
|
22
|
+
const cliArgs = argv.slice(2);
|
|
23
|
+
let afterDoubleDash = false;
|
|
24
|
+
for (let i = 0; i < cliArgs.length; i++) {
|
|
25
|
+
const arg = cliArgs[i];
|
|
26
|
+
if (arg === '--') {
|
|
27
|
+
afterDoubleDash = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (afterDoubleDash) {
|
|
31
|
+
// Everything after -- is a path
|
|
32
|
+
args.path = arg;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (arg === '--help' || arg === '-h') {
|
|
36
|
+
args.help = true;
|
|
37
|
+
}
|
|
38
|
+
else if (arg === '--staged' || arg === '--cached') {
|
|
39
|
+
args.staged = true;
|
|
40
|
+
}
|
|
41
|
+
else if (arg === '--name-only') {
|
|
42
|
+
args.nameOnly = true;
|
|
43
|
+
}
|
|
44
|
+
else if (arg === '--stat') {
|
|
45
|
+
args.stat = true;
|
|
46
|
+
}
|
|
47
|
+
else if (arg === '--base-dir') {
|
|
48
|
+
args.baseDir = cliArgs[++i];
|
|
49
|
+
}
|
|
50
|
+
else if (!arg.startsWith('-') && !args.ref) {
|
|
51
|
+
// Positional argument for ref
|
|
52
|
+
args.ref = arg;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return args;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Get git diff with audit logging
|
|
59
|
+
*/
|
|
60
|
+
export async function getGitDiff(args) {
|
|
61
|
+
try {
|
|
62
|
+
const git = args.baseDir ? createGitForPath(args.baseDir) : getGitForCwd();
|
|
63
|
+
// Build diff arguments
|
|
64
|
+
const rawArgs = ['diff'];
|
|
65
|
+
if (args.staged) {
|
|
66
|
+
rawArgs.push('--cached');
|
|
67
|
+
}
|
|
68
|
+
if (args.nameOnly) {
|
|
69
|
+
rawArgs.push('--name-only');
|
|
70
|
+
}
|
|
71
|
+
if (args.stat) {
|
|
72
|
+
rawArgs.push('--stat');
|
|
73
|
+
}
|
|
74
|
+
if (args.ref) {
|
|
75
|
+
rawArgs.push(args.ref);
|
|
76
|
+
}
|
|
77
|
+
if (args.path) {
|
|
78
|
+
rawArgs.push('--', args.path);
|
|
79
|
+
}
|
|
80
|
+
const output = await git.raw(rawArgs);
|
|
81
|
+
const trimmedOutput = output.trim();
|
|
82
|
+
const hasDiff = trimmedOutput !== '';
|
|
83
|
+
// Parse output based on mode
|
|
84
|
+
if (args.nameOnly) {
|
|
85
|
+
const files = trimmedOutput ? trimmedOutput.split('\n').filter((f) => f.trim()) : [];
|
|
86
|
+
return {
|
|
87
|
+
success: true,
|
|
88
|
+
hasDiff,
|
|
89
|
+
files,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (args.stat) {
|
|
93
|
+
return {
|
|
94
|
+
success: true,
|
|
95
|
+
hasDiff,
|
|
96
|
+
stat: trimmedOutput,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
hasDiff,
|
|
102
|
+
diff: trimmedOutput,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: errorMessage,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Print help message
|
|
115
|
+
*/
|
|
116
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
117
|
+
function printHelp() {
|
|
118
|
+
console.log(`
|
|
119
|
+
Usage: git-diff [ref] [options] [-- path]
|
|
120
|
+
|
|
121
|
+
Show changes between commits, commit and working tree, etc.
|
|
122
|
+
|
|
123
|
+
Arguments:
|
|
124
|
+
ref Reference to diff against (e.g., HEAD~1, main)
|
|
125
|
+
path Path to filter diff
|
|
126
|
+
|
|
127
|
+
Options:
|
|
128
|
+
--base-dir <dir> Base directory for git operations
|
|
129
|
+
--staged, --cached Show staged changes
|
|
130
|
+
--name-only Show only names of changed files
|
|
131
|
+
--stat Show diffstat
|
|
132
|
+
-h, --help Show this help message
|
|
133
|
+
|
|
134
|
+
Examples:
|
|
135
|
+
git-diff
|
|
136
|
+
git-diff --staged
|
|
137
|
+
git-diff HEAD~1
|
|
138
|
+
git-diff --name-only
|
|
139
|
+
git-diff -- src/index.ts
|
|
140
|
+
`);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Main entry point
|
|
144
|
+
*/
|
|
145
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
146
|
+
async function main() {
|
|
147
|
+
const args = parseGitDiffArgs(process.argv);
|
|
148
|
+
if (args.help) {
|
|
149
|
+
printHelp();
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
const result = await getGitDiff(args);
|
|
153
|
+
if (result.success) {
|
|
154
|
+
if (result.files) {
|
|
155
|
+
// Name-only mode
|
|
156
|
+
result.files.forEach((f) => console.log(f));
|
|
157
|
+
}
|
|
158
|
+
else if (result.stat) {
|
|
159
|
+
// Stat mode
|
|
160
|
+
console.log(result.stat);
|
|
161
|
+
}
|
|
162
|
+
else if (result.diff) {
|
|
163
|
+
// Full diff
|
|
164
|
+
console.log(result.diff);
|
|
165
|
+
}
|
|
166
|
+
// Empty diff produces no output
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.error(`Error: ${result.error}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Run main if executed directly
|
|
174
|
+
import { runCLI } from './cli-entry-point.js';
|
|
175
|
+
if (import.meta.main) {
|
|
176
|
+
runCLI(main);
|
|
177
|
+
}
|
package/dist/git-log.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Git Log CLI Tool
|
|
4
|
+
*
|
|
5
|
+
* Provides WU-aware git log with:
|
|
6
|
+
* - Oneline and custom format output
|
|
7
|
+
* - Max count limiting
|
|
8
|
+
* - Date and author filtering
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node git-log.js [ref] [--oneline] [-n <count>] [--format <format>]
|
|
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-log
|
|
18
|
+
*/
|
|
19
|
+
export function parseGitLogArgs(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 === '--oneline') {
|
|
29
|
+
args.oneline = true;
|
|
30
|
+
}
|
|
31
|
+
else if (arg === '-n') {
|
|
32
|
+
const val = cliArgs[++i];
|
|
33
|
+
if (val)
|
|
34
|
+
args.maxCount = parseInt(val, 10);
|
|
35
|
+
}
|
|
36
|
+
else if (arg === '--max-count') {
|
|
37
|
+
const val = cliArgs[++i];
|
|
38
|
+
if (val)
|
|
39
|
+
args.maxCount = parseInt(val, 10);
|
|
40
|
+
}
|
|
41
|
+
else if (arg.startsWith('-n') && arg.length > 2) {
|
|
42
|
+
// Handle -n5 format
|
|
43
|
+
args.maxCount = parseInt(arg.slice(2), 10);
|
|
44
|
+
}
|
|
45
|
+
else if (arg === '--format') {
|
|
46
|
+
args.format = cliArgs[++i];
|
|
47
|
+
}
|
|
48
|
+
else if (arg === '--since') {
|
|
49
|
+
args.since = cliArgs[++i];
|
|
50
|
+
}
|
|
51
|
+
else if (arg === '--author') {
|
|
52
|
+
args.author = cliArgs[++i];
|
|
53
|
+
}
|
|
54
|
+
else if (arg === '--base-dir') {
|
|
55
|
+
args.baseDir = cliArgs[++i];
|
|
56
|
+
}
|
|
57
|
+
else if (!arg.startsWith('-') && !args.ref) {
|
|
58
|
+
// Positional argument for ref
|
|
59
|
+
args.ref = arg;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return args;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse structured log output
|
|
66
|
+
*/
|
|
67
|
+
function parseLogOutput(output) {
|
|
68
|
+
if (!output.trim()) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const commits = [];
|
|
72
|
+
// Parse format: hash|message|author|date (separated by |||)
|
|
73
|
+
const lines = output.trim().split('\n');
|
|
74
|
+
for (const line of lines) {
|
|
75
|
+
if (!line.trim())
|
|
76
|
+
continue;
|
|
77
|
+
const parts = line.split('|||');
|
|
78
|
+
if (parts.length >= 2) {
|
|
79
|
+
commits.push({
|
|
80
|
+
hash: parts[0].trim(),
|
|
81
|
+
message: parts[1].trim(),
|
|
82
|
+
author: parts[2]?.trim(),
|
|
83
|
+
date: parts[3]?.trim(),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
// Fallback for oneline format (hash + message)
|
|
88
|
+
const match = line.match(/^([a-f0-9]+)\s+(.*)$/);
|
|
89
|
+
if (match) {
|
|
90
|
+
commits.push({
|
|
91
|
+
hash: match[1],
|
|
92
|
+
message: match[2],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return commits;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get git log with audit logging
|
|
101
|
+
*/
|
|
102
|
+
export async function getGitLog(args) {
|
|
103
|
+
try {
|
|
104
|
+
const git = args.baseDir ? createGitForPath(args.baseDir) : getGitForCwd();
|
|
105
|
+
// Build log arguments
|
|
106
|
+
const rawArgs = ['log'];
|
|
107
|
+
if (args.maxCount) {
|
|
108
|
+
rawArgs.push(`-n`, String(args.maxCount));
|
|
109
|
+
}
|
|
110
|
+
if (args.oneline) {
|
|
111
|
+
rawArgs.push('--oneline');
|
|
112
|
+
}
|
|
113
|
+
else if (args.format) {
|
|
114
|
+
rawArgs.push(`--format=${args.format}`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Use structured format for parsing
|
|
118
|
+
rawArgs.push('--format=%H|||%s|||%an|||%ai');
|
|
119
|
+
}
|
|
120
|
+
if (args.since) {
|
|
121
|
+
rawArgs.push(`--since=${args.since}`);
|
|
122
|
+
}
|
|
123
|
+
if (args.author) {
|
|
124
|
+
rawArgs.push(`--author=${args.author}`);
|
|
125
|
+
}
|
|
126
|
+
if (args.ref) {
|
|
127
|
+
rawArgs.push(args.ref);
|
|
128
|
+
}
|
|
129
|
+
const output = await git.raw(rawArgs);
|
|
130
|
+
const trimmedOutput = output.trim();
|
|
131
|
+
// Parse commits
|
|
132
|
+
const commits = args.oneline || args.format ? [] : parseLogOutput(trimmedOutput);
|
|
133
|
+
return {
|
|
134
|
+
success: true,
|
|
135
|
+
commits,
|
|
136
|
+
output: args.oneline || args.format ? trimmedOutput : undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
141
|
+
// Handle case of repo with no commits
|
|
142
|
+
if (errorMessage.includes('does not have any commits') ||
|
|
143
|
+
errorMessage.includes('fatal: bad revision') ||
|
|
144
|
+
errorMessage.includes("fatal: your current branch 'main' does not have any commits")) {
|
|
145
|
+
return {
|
|
146
|
+
success: true,
|
|
147
|
+
commits: [],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
error: errorMessage,
|
|
153
|
+
commits: [],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Print help message
|
|
159
|
+
*/
|
|
160
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
161
|
+
function printHelp() {
|
|
162
|
+
console.log(`
|
|
163
|
+
Usage: git-log [ref] [options]
|
|
164
|
+
|
|
165
|
+
Show commit logs.
|
|
166
|
+
|
|
167
|
+
Arguments:
|
|
168
|
+
ref Revision range (e.g., main..feature)
|
|
169
|
+
|
|
170
|
+
Options:
|
|
171
|
+
--base-dir <dir> Base directory for git operations
|
|
172
|
+
--oneline Show each commit on a single line
|
|
173
|
+
-n <number> Limit the number of commits
|
|
174
|
+
--max-count <number> Limit the number of commits
|
|
175
|
+
--format <format> Pretty-print format string
|
|
176
|
+
--since <date> Show commits more recent than a date
|
|
177
|
+
--author <pattern> Limit to commits by author
|
|
178
|
+
-h, --help Show this help message
|
|
179
|
+
|
|
180
|
+
Examples:
|
|
181
|
+
git-log
|
|
182
|
+
git-log --oneline
|
|
183
|
+
git-log -n 10
|
|
184
|
+
git-log main..feature
|
|
185
|
+
git-log --since="2024-01-01"
|
|
186
|
+
git-log --author="test@example.com"
|
|
187
|
+
`);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Main entry point
|
|
191
|
+
*/
|
|
192
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
193
|
+
async function main() {
|
|
194
|
+
const args = parseGitLogArgs(process.argv);
|
|
195
|
+
if (args.help) {
|
|
196
|
+
printHelp();
|
|
197
|
+
process.exit(0);
|
|
198
|
+
}
|
|
199
|
+
const result = await getGitLog(args);
|
|
200
|
+
if (result.success) {
|
|
201
|
+
if (result.output !== undefined) {
|
|
202
|
+
// Custom format or oneline mode
|
|
203
|
+
if (result.output) {
|
|
204
|
+
console.log(result.output);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
// Structured output
|
|
209
|
+
for (const commit of result.commits) {
|
|
210
|
+
console.log(`commit ${commit.hash}`);
|
|
211
|
+
if (commit.author)
|
|
212
|
+
console.log(`Author: ${commit.author}`);
|
|
213
|
+
if (commit.date)
|
|
214
|
+
console.log(`Date: ${commit.date}`);
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log(` ${commit.message}`);
|
|
217
|
+
console.log('');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
console.error(`Error: ${result.error}`);
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Run main if executed directly
|
|
227
|
+
import { runCLI } from './cli-entry-point.js';
|
|
228
|
+
if (import.meta.main) {
|
|
229
|
+
runCLI(main);
|
|
230
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Git Status CLI Tool
|
|
4
|
+
*
|
|
5
|
+
* Provides WU-aware git status with:
|
|
6
|
+
* - Porcelain and short output formats
|
|
7
|
+
* - Parsed file status (staged, modified, untracked)
|
|
8
|
+
* - Clean/dirty state detection
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node git-status.js [path] [--porcelain] [--short]
|
|
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-status
|
|
18
|
+
*/
|
|
19
|
+
export function parseGitStatusArgs(argv) {
|
|
20
|
+
const args = {
|
|
21
|
+
porcelain: false,
|
|
22
|
+
short: false,
|
|
23
|
+
};
|
|
24
|
+
// Skip node and script name
|
|
25
|
+
const cliArgs = argv.slice(2);
|
|
26
|
+
for (let i = 0; i < cliArgs.length; i++) {
|
|
27
|
+
const arg = cliArgs[i];
|
|
28
|
+
if (arg === '--help' || arg === '-h') {
|
|
29
|
+
args.help = true;
|
|
30
|
+
}
|
|
31
|
+
else if (arg === '--porcelain') {
|
|
32
|
+
args.porcelain = true;
|
|
33
|
+
}
|
|
34
|
+
else if (arg === '--short' || arg === '-s') {
|
|
35
|
+
args.short = true;
|
|
36
|
+
}
|
|
37
|
+
else if (arg === '--base-dir') {
|
|
38
|
+
args.baseDir = cliArgs[++i];
|
|
39
|
+
}
|
|
40
|
+
else if (!arg.startsWith('-')) {
|
|
41
|
+
// Positional argument for path
|
|
42
|
+
args.path = arg;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return args;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse porcelain status output into categorized files
|
|
49
|
+
*/
|
|
50
|
+
function parseStatusOutput(output) {
|
|
51
|
+
const staged = [];
|
|
52
|
+
const modified = [];
|
|
53
|
+
const untracked = [];
|
|
54
|
+
const deleted = [];
|
|
55
|
+
// Don't filter based on trim - leading spaces are significant in git status
|
|
56
|
+
const lines = output.split('\n').filter((line) => line.length > 0);
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (line.length < 3)
|
|
59
|
+
continue;
|
|
60
|
+
const indexStatus = line[0];
|
|
61
|
+
const workTreeStatus = line[1];
|
|
62
|
+
const filePath = line.slice(3).trim();
|
|
63
|
+
// Handle renames (e.g., "R old -> new")
|
|
64
|
+
const fileName = filePath.includes(' -> ') ? filePath.split(' -> ')[1] : filePath;
|
|
65
|
+
// Untracked files
|
|
66
|
+
if (indexStatus === '?' && workTreeStatus === '?') {
|
|
67
|
+
untracked.push(fileName);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
// Staged changes (index has status)
|
|
71
|
+
if (indexStatus !== ' ' && indexStatus !== '?') {
|
|
72
|
+
staged.push(fileName);
|
|
73
|
+
if (indexStatus === 'D') {
|
|
74
|
+
deleted.push(fileName);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Working tree changes (unstaged modifications)
|
|
78
|
+
if (workTreeStatus === 'M') {
|
|
79
|
+
modified.push(fileName);
|
|
80
|
+
}
|
|
81
|
+
else if (workTreeStatus === 'D' && indexStatus === ' ') {
|
|
82
|
+
// Only count as deleted in working tree if not already staged for deletion
|
|
83
|
+
deleted.push(fileName);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { staged, modified, untracked, deleted };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get git status with audit logging
|
|
90
|
+
*/
|
|
91
|
+
export async function getGitStatus(args) {
|
|
92
|
+
try {
|
|
93
|
+
const git = args.baseDir ? createGitForPath(args.baseDir) : getGitForCwd();
|
|
94
|
+
// Get porcelain status
|
|
95
|
+
const rawArgs = ['status', '--porcelain'];
|
|
96
|
+
if (args.path) {
|
|
97
|
+
rawArgs.push('--', args.path);
|
|
98
|
+
}
|
|
99
|
+
const output = await git.raw(rawArgs);
|
|
100
|
+
// Don't trim the entire output - leading spaces in lines are significant for git status
|
|
101
|
+
// Only trim trailing newlines
|
|
102
|
+
const trimmedOutput = output.replace(/\n+$/, '');
|
|
103
|
+
const isClean = trimmedOutput === '';
|
|
104
|
+
// If porcelain mode requested, return raw output
|
|
105
|
+
if (args.porcelain) {
|
|
106
|
+
return {
|
|
107
|
+
success: true,
|
|
108
|
+
isClean,
|
|
109
|
+
output: trimmedOutput,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Parse the status output
|
|
113
|
+
const { staged, modified, untracked, deleted } = parseStatusOutput(trimmedOutput);
|
|
114
|
+
return {
|
|
115
|
+
success: true,
|
|
116
|
+
isClean,
|
|
117
|
+
staged,
|
|
118
|
+
modified,
|
|
119
|
+
untracked,
|
|
120
|
+
deleted,
|
|
121
|
+
output: args.short ? trimmedOutput : undefined,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
error: errorMessage,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Print help message
|
|
134
|
+
*/
|
|
135
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
136
|
+
function printHelp() {
|
|
137
|
+
console.log(`
|
|
138
|
+
Usage: git-status [path] [options]
|
|
139
|
+
|
|
140
|
+
Show the working tree status.
|
|
141
|
+
|
|
142
|
+
Arguments:
|
|
143
|
+
path Path to filter status
|
|
144
|
+
|
|
145
|
+
Options:
|
|
146
|
+
--base-dir <dir> Base directory for git operations
|
|
147
|
+
--porcelain Give the output in an easy-to-parse format
|
|
148
|
+
--short, -s Give the output in short format
|
|
149
|
+
-h, --help Show this help message
|
|
150
|
+
|
|
151
|
+
Examples:
|
|
152
|
+
git-status
|
|
153
|
+
git-status src/
|
|
154
|
+
git-status --porcelain
|
|
155
|
+
git-status --short
|
|
156
|
+
`);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Main entry point
|
|
160
|
+
*/
|
|
161
|
+
/* istanbul ignore next -- CLI entry point tested via subprocess */
|
|
162
|
+
async function main() {
|
|
163
|
+
const args = parseGitStatusArgs(process.argv);
|
|
164
|
+
if (args.help) {
|
|
165
|
+
printHelp();
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
const result = await getGitStatus(args);
|
|
169
|
+
if (result.success) {
|
|
170
|
+
if (result.output !== undefined) {
|
|
171
|
+
// Porcelain or short mode
|
|
172
|
+
if (result.output) {
|
|
173
|
+
console.log(result.output);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
// Human-readable output
|
|
178
|
+
if (result.isClean) {
|
|
179
|
+
console.log('nothing to commit, working tree clean');
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
if (result.staged && result.staged.length > 0) {
|
|
183
|
+
console.log('Changes to be committed:');
|
|
184
|
+
result.staged.forEach((f) => console.log(` ${f}`));
|
|
185
|
+
console.log('');
|
|
186
|
+
}
|
|
187
|
+
if (result.modified && result.modified.length > 0) {
|
|
188
|
+
console.log('Changes not staged for commit:');
|
|
189
|
+
result.modified.forEach((f) => console.log(` modified: ${f}`));
|
|
190
|
+
console.log('');
|
|
191
|
+
}
|
|
192
|
+
if (result.untracked && result.untracked.length > 0) {
|
|
193
|
+
console.log('Untracked files:');
|
|
194
|
+
result.untracked.forEach((f) => console.log(` ${f}`));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
console.error(`Error: ${result.error}`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Run main if executed directly
|
|
205
|
+
import { runCLI } from './cli-entry-point.js';
|
|
206
|
+
if (import.meta.main) {
|
|
207
|
+
runCLI(main);
|
|
208
|
+
}
|