@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
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
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file guard-locked.ts
|
|
4
|
+
* @description Guard that prevents changes to locked WUs (WU-1111)
|
|
5
|
+
*
|
|
6
|
+
* Validates that a WU is not locked before allowing modifications.
|
|
7
|
+
* Used by git hooks and wu: commands to enforce workflow discipline.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* guard-locked WU-123 # Check if WU-123 is locked
|
|
11
|
+
* guard-locked --wu WU-123 # Same with explicit flag
|
|
12
|
+
*
|
|
13
|
+
* Exit codes:
|
|
14
|
+
* 0 - WU is not locked (safe to proceed)
|
|
15
|
+
* 1 - WU is locked (block operation)
|
|
16
|
+
*
|
|
17
|
+
* @see {@link docs/04-operations/_frameworks/lumenflow/lumenflow-complete.md} - WU lifecycle
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { parseYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
23
|
+
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
24
|
+
import { PATTERNS, FILE_SYSTEM } from '@lumenflow/core/dist/wu-constants.js';
|
|
25
|
+
const LOG_PREFIX = '[guard-locked]';
|
|
26
|
+
/**
|
|
27
|
+
* Check if a WU is locked
|
|
28
|
+
*
|
|
29
|
+
* @param wuPath - Path to WU YAML file
|
|
30
|
+
* @returns true if WU has locked: true, false otherwise
|
|
31
|
+
* @throws Error if WU file does not exist or cannot be parsed
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* if (isWULocked('/path/to/WU-123.yaml')) {
|
|
35
|
+
* console.log('WU is locked, cannot modify');
|
|
36
|
+
* }
|
|
37
|
+
*/
|
|
38
|
+
export function isWULocked(wuPath) {
|
|
39
|
+
if (!existsSync(wuPath)) {
|
|
40
|
+
throw new Error(`WU file not found: ${wuPath}`);
|
|
41
|
+
}
|
|
42
|
+
const content = readFileSync(wuPath, { encoding: FILE_SYSTEM.UTF8 });
|
|
43
|
+
const doc = parseYAML(content);
|
|
44
|
+
return doc.locked === true;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Assert that a WU is not locked
|
|
48
|
+
*
|
|
49
|
+
* @param wuPath - Path to WU YAML file
|
|
50
|
+
* @throws Error if WU is locked, with actionable fix instructions
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* try {
|
|
54
|
+
* assertWUNotLocked('/path/to/WU-123.yaml');
|
|
55
|
+
* // Safe to modify
|
|
56
|
+
* } catch (error) {
|
|
57
|
+
* console.error(error.message);
|
|
58
|
+
* process.exit(1);
|
|
59
|
+
* }
|
|
60
|
+
*/
|
|
61
|
+
export function assertWUNotLocked(wuPath) {
|
|
62
|
+
if (!existsSync(wuPath)) {
|
|
63
|
+
throw new Error(`WU file not found: ${wuPath}`);
|
|
64
|
+
}
|
|
65
|
+
const content = readFileSync(wuPath, { encoding: FILE_SYSTEM.UTF8 });
|
|
66
|
+
const doc = parseYAML(content);
|
|
67
|
+
if (doc.locked === true) {
|
|
68
|
+
const wuId = doc.id || path.basename(wuPath, '.yaml');
|
|
69
|
+
throw new Error(`${LOG_PREFIX} WU ${wuId} is locked.
|
|
70
|
+
|
|
71
|
+
Locked WUs cannot be modified. This prevents accidental changes to completed work.
|
|
72
|
+
|
|
73
|
+
If you need to modify this WU:
|
|
74
|
+
1. Check if modification is really necessary (locked WUs are done)
|
|
75
|
+
2. Use wu:unlock to unlock the WU first:
|
|
76
|
+
pnpm wu:unlock --id ${wuId} --reason "reason for unlocking"
|
|
77
|
+
|
|
78
|
+
For more information:
|
|
79
|
+
See docs/04-operations/_frameworks/lumenflow/lumenflow-complete.md
|
|
80
|
+
`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Check if a WU ID is locked by looking up the YAML file
|
|
85
|
+
*
|
|
86
|
+
* @param wuId - WU ID (e.g., "WU-123")
|
|
87
|
+
* @returns true if WU has locked: true, false otherwise
|
|
88
|
+
* @throws Error if WU file does not exist
|
|
89
|
+
*/
|
|
90
|
+
export function isWUIdLocked(wuId) {
|
|
91
|
+
const wuPath = WU_PATHS.WU(wuId);
|
|
92
|
+
return isWULocked(wuPath);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Assert that a WU ID is not locked
|
|
96
|
+
*
|
|
97
|
+
* @param wuId - WU ID (e.g., "WU-123")
|
|
98
|
+
* @throws Error if WU is locked
|
|
99
|
+
*/
|
|
100
|
+
export function assertWUIdNotLocked(wuId) {
|
|
101
|
+
const wuPath = WU_PATHS.WU(wuId);
|
|
102
|
+
assertWUNotLocked(wuPath);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Main CLI entry point
|
|
106
|
+
*/
|
|
107
|
+
async function main() {
|
|
108
|
+
const args = process.argv.slice(2);
|
|
109
|
+
// Parse arguments
|
|
110
|
+
let wuId;
|
|
111
|
+
for (let i = 0; i < args.length; i++) {
|
|
112
|
+
const arg = args[i];
|
|
113
|
+
if (arg === '--wu' || arg === '--id') {
|
|
114
|
+
wuId = args[++i];
|
|
115
|
+
}
|
|
116
|
+
else if (arg === '--help' || arg === '-h') {
|
|
117
|
+
console.log(`Usage: guard-locked [--wu] WU-XXX
|
|
118
|
+
|
|
119
|
+
Check if a WU is locked. Exits with code 1 if locked.
|
|
120
|
+
|
|
121
|
+
Options:
|
|
122
|
+
--wu, --id WU-XXX WU ID to check
|
|
123
|
+
-h, --help Show this help message
|
|
124
|
+
|
|
125
|
+
Examples:
|
|
126
|
+
guard-locked WU-123
|
|
127
|
+
guard-locked --wu WU-123
|
|
128
|
+
`);
|
|
129
|
+
process.exit(0);
|
|
130
|
+
}
|
|
131
|
+
else if (PATTERNS.WU_ID.test(arg.toUpperCase())) {
|
|
132
|
+
wuId = arg.toUpperCase();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (!wuId) {
|
|
136
|
+
console.error(`${LOG_PREFIX} Error: WU ID required`);
|
|
137
|
+
console.error('Usage: guard-locked [--wu] WU-XXX');
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
// Normalize WU ID
|
|
141
|
+
wuId = wuId.toUpperCase();
|
|
142
|
+
if (!PATTERNS.WU_ID.test(wuId)) {
|
|
143
|
+
console.error(`${LOG_PREFIX} Invalid WU ID: ${wuId}`);
|
|
144
|
+
console.error('Expected format: WU-123');
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
if (isWUIdLocked(wuId)) {
|
|
149
|
+
console.error(`${LOG_PREFIX} ${wuId} is locked`);
|
|
150
|
+
console.error('');
|
|
151
|
+
console.error('Locked WUs cannot be modified.');
|
|
152
|
+
console.error(`To unlock: pnpm wu:unlock --id ${wuId} --reason "your reason"`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
console.log(`${LOG_PREFIX} ${wuId} is not locked (OK)`);
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
console.error(`${LOG_PREFIX} Error: ${error.message}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Guard main() for testability
|
|
164
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
165
|
+
main().catch((error) => {
|
|
166
|
+
console.error(`${LOG_PREFIX} Unexpected error:`, error);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
});
|
|
169
|
+
}
|