@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.
Files changed (42) hide show
  1. package/README.md +19 -0
  2. package/dist/__tests__/backlog-prune.test.js +478 -0
  3. package/dist/__tests__/deps-operations.test.js +206 -0
  4. package/dist/__tests__/file-operations.test.js +906 -0
  5. package/dist/__tests__/git-operations.test.js +668 -0
  6. package/dist/__tests__/guards-validation.test.js +416 -0
  7. package/dist/__tests__/init-plan.test.js +340 -0
  8. package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
  9. package/dist/__tests__/metrics-cli.test.js +619 -0
  10. package/dist/__tests__/rotate-progress.test.js +127 -0
  11. package/dist/__tests__/session-coordinator.test.js +109 -0
  12. package/dist/__tests__/state-bootstrap.test.js +432 -0
  13. package/dist/__tests__/trace-gen.test.js +115 -0
  14. package/dist/backlog-prune.js +299 -0
  15. package/dist/deps-add.js +215 -0
  16. package/dist/deps-remove.js +94 -0
  17. package/dist/docs-sync.js +72 -326
  18. package/dist/file-delete.js +236 -0
  19. package/dist/file-edit.js +247 -0
  20. package/dist/file-read.js +197 -0
  21. package/dist/file-write.js +220 -0
  22. package/dist/git-branch.js +187 -0
  23. package/dist/git-diff.js +177 -0
  24. package/dist/git-log.js +230 -0
  25. package/dist/git-status.js +208 -0
  26. package/dist/guard-locked.js +169 -0
  27. package/dist/guard-main-branch.js +202 -0
  28. package/dist/guard-worktree-commit.js +160 -0
  29. package/dist/init-plan.js +337 -0
  30. package/dist/lumenflow-upgrade.js +178 -0
  31. package/dist/metrics-cli.js +433 -0
  32. package/dist/rotate-progress.js +247 -0
  33. package/dist/session-coordinator.js +300 -0
  34. package/dist/state-bootstrap.js +307 -0
  35. package/dist/sync-templates.js +212 -0
  36. package/dist/trace-gen.js +331 -0
  37. package/dist/validate-agent-skills.js +218 -0
  38. package/dist/validate-agent-sync.js +148 -0
  39. package/dist/validate-backlog-sync.js +152 -0
  40. package/dist/validate-skills-spec.js +206 -0
  41. package/dist/validate.js +230 -0
  42. package/package.json +37 -7
@@ -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
+ }