@lumenflow/cli 1.3.0 → 1.3.2
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__/cli-entry-point.test.js +50 -0
- package/dist/__tests__/cli-subprocess.test.js +64 -0
- package/dist/cli-entry-point.js +46 -0
- package/dist/initiative-add-wu.js +2 -1
- package/dist/initiative-create.js +2 -1
- package/dist/initiative-list.js +2 -1
- package/dist/initiative-status.js +2 -1
- package/dist/wu-claim.js +2 -1
- package/dist/wu-cleanup.js +82 -56
- package/dist/wu-create.js +2 -1
- package/dist/wu-deps.js +2 -1
- package/dist/wu-done.js +2 -1
- package/dist/wu-infer-lane.js +3 -2
- package/dist/wu-preflight.js +2 -1
- package/dist/wu-prune.js +12 -3
- package/dist/wu-repair.js +2 -1
- package/dist/wu-spawn.js +2 -1
- package/dist/wu-unlock-lane.js +6 -1
- package/dist/wu-validate.js +2 -1
- package/package.json +6 -6
- package/dist/gates.d.ts +0 -41
- package/dist/gates.d.ts.map +0 -1
- package/dist/gates.js.map +0 -1
- package/dist/initiative-add-wu.d.ts +0 -22
- package/dist/initiative-add-wu.d.ts.map +0 -1
- package/dist/initiative-add-wu.js.map +0 -1
- package/dist/initiative-create.d.ts +0 -28
- package/dist/initiative-create.d.ts.map +0 -1
- package/dist/initiative-create.js.map +0 -1
- package/dist/initiative-edit.d.ts +0 -34
- package/dist/initiative-edit.d.ts.map +0 -1
- package/dist/initiative-edit.js.map +0 -1
- package/dist/initiative-list.d.ts +0 -12
- package/dist/initiative-list.d.ts.map +0 -1
- package/dist/initiative-list.js.map +0 -1
- package/dist/initiative-status.d.ts +0 -11
- package/dist/initiative-status.d.ts.map +0 -1
- package/dist/initiative-status.js.map +0 -1
- package/dist/mem-checkpoint.d.ts +0 -16
- package/dist/mem-checkpoint.d.ts.map +0 -1
- package/dist/mem-checkpoint.js.map +0 -1
- package/dist/mem-cleanup.d.ts +0 -29
- package/dist/mem-cleanup.d.ts.map +0 -1
- package/dist/mem-cleanup.js.map +0 -1
- package/dist/mem-create.d.ts +0 -17
- package/dist/mem-create.d.ts.map +0 -1
- package/dist/mem-create.js.map +0 -1
- package/dist/mem-inbox.d.ts +0 -35
- package/dist/mem-inbox.d.ts.map +0 -1
- package/dist/mem-inbox.js.map +0 -1
- package/dist/mem-init.d.ts +0 -15
- package/dist/mem-init.d.ts.map +0 -1
- package/dist/mem-init.js.map +0 -1
- package/dist/mem-ready.d.ts +0 -16
- package/dist/mem-ready.d.ts.map +0 -1
- package/dist/mem-ready.js.map +0 -1
- package/dist/mem-signal.d.ts +0 -16
- package/dist/mem-signal.d.ts.map +0 -1
- package/dist/mem-signal.js.map +0 -1
- package/dist/mem-start.d.ts +0 -16
- package/dist/mem-start.d.ts.map +0 -1
- package/dist/mem-start.js.map +0 -1
- package/dist/mem-summarize.d.ts +0 -22
- package/dist/mem-summarize.d.ts.map +0 -1
- package/dist/mem-summarize.js.map +0 -1
- package/dist/mem-triage.d.ts +0 -22
- package/dist/mem-triage.d.ts.map +0 -1
- package/dist/mem-triage.js.map +0 -1
- package/dist/spawn-list.d.ts +0 -16
- package/dist/spawn-list.d.ts.map +0 -1
- package/dist/spawn-list.js.map +0 -1
- package/dist/wu-block.d.ts +0 -16
- package/dist/wu-block.d.ts.map +0 -1
- package/dist/wu-block.js.map +0 -1
- package/dist/wu-claim.d.ts +0 -32
- package/dist/wu-claim.d.ts.map +0 -1
- package/dist/wu-claim.js.map +0 -1
- package/dist/wu-cleanup.d.ts +0 -17
- package/dist/wu-cleanup.d.ts.map +0 -1
- package/dist/wu-cleanup.js.map +0 -1
- package/dist/wu-create.d.ts +0 -38
- package/dist/wu-create.d.ts.map +0 -1
- package/dist/wu-create.js.map +0 -1
- package/dist/wu-deps.d.ts +0 -13
- package/dist/wu-deps.d.ts.map +0 -1
- package/dist/wu-deps.js.map +0 -1
- package/dist/wu-done.d.ts +0 -153
- package/dist/wu-done.d.ts.map +0 -1
- package/dist/wu-done.js.map +0 -1
- package/dist/wu-edit.d.ts +0 -29
- package/dist/wu-edit.d.ts.map +0 -1
- package/dist/wu-edit.js.map +0 -1
- package/dist/wu-infer-lane.d.ts +0 -17
- package/dist/wu-infer-lane.d.ts.map +0 -1
- package/dist/wu-infer-lane.js.map +0 -1
- package/dist/wu-preflight.d.ts +0 -47
- package/dist/wu-preflight.d.ts.map +0 -1
- package/dist/wu-preflight.js.map +0 -1
- package/dist/wu-prune.d.ts +0 -16
- package/dist/wu-prune.d.ts.map +0 -1
- package/dist/wu-prune.js.map +0 -1
- package/dist/wu-repair.d.ts +0 -60
- package/dist/wu-repair.d.ts.map +0 -1
- package/dist/wu-repair.js.map +0 -1
- package/dist/wu-spawn-completion.d.ts +0 -10
- package/dist/wu-spawn.d.ts +0 -168
- package/dist/wu-spawn.d.ts.map +0 -1
- package/dist/wu-spawn.js.map +0 -1
- package/dist/wu-unblock.d.ts +0 -16
- package/dist/wu-unblock.d.ts.map +0 -1
- package/dist/wu-unblock.js.map +0 -1
- package/dist/wu-validate.d.ts +0 -16
- package/dist/wu-validate.d.ts.map +0 -1
- package/dist/wu-validate.js.map +0 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CLI entry point error handling
|
|
3
|
+
*
|
|
4
|
+
* Verifies that runCLI wrapper properly:
|
|
5
|
+
* - Catches async errors from main()
|
|
6
|
+
* - Logs error messages to stderr
|
|
7
|
+
* - Exits with EXIT_CODES.ERROR on failure
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import { runCLI } from '../cli-entry-point.js';
|
|
11
|
+
import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
|
|
12
|
+
describe('runCLI', () => {
|
|
13
|
+
let mockExit;
|
|
14
|
+
let mockConsoleError;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
mockExit = vi.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
17
|
+
mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
mockExit.mockRestore();
|
|
21
|
+
mockConsoleError.mockRestore();
|
|
22
|
+
});
|
|
23
|
+
it('should call main() and do nothing on success', async () => {
|
|
24
|
+
const main = vi.fn().mockResolvedValue(undefined);
|
|
25
|
+
await runCLI(main);
|
|
26
|
+
expect(main).toHaveBeenCalledOnce();
|
|
27
|
+
expect(mockExit).not.toHaveBeenCalled();
|
|
28
|
+
expect(mockConsoleError).not.toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
it('should catch errors and exit with ERROR code', async () => {
|
|
31
|
+
const error = new Error('Test error message');
|
|
32
|
+
const main = vi.fn().mockRejectedValue(error);
|
|
33
|
+
await runCLI(main);
|
|
34
|
+
expect(main).toHaveBeenCalledOnce();
|
|
35
|
+
expect(mockConsoleError).toHaveBeenCalledWith('Test error message');
|
|
36
|
+
expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
37
|
+
});
|
|
38
|
+
it('should handle errors without message property', async () => {
|
|
39
|
+
const main = vi.fn().mockRejectedValue('string error');
|
|
40
|
+
await runCLI(main);
|
|
41
|
+
expect(mockConsoleError).toHaveBeenCalledWith('string error');
|
|
42
|
+
expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
43
|
+
});
|
|
44
|
+
it('should handle null/undefined errors', async () => {
|
|
45
|
+
const main = vi.fn().mockRejectedValue(null);
|
|
46
|
+
await runCLI(main);
|
|
47
|
+
expect(mockConsoleError).toHaveBeenCalledWith('Unknown error');
|
|
48
|
+
expect(mockExit).toHaveBeenCalledWith(EXIT_CODES.ERROR);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for CLI subprocess execution
|
|
3
|
+
*
|
|
4
|
+
* Tests that CLI commands properly:
|
|
5
|
+
* - Exit with non-zero code on errors
|
|
6
|
+
* - Output error messages to stderr
|
|
7
|
+
* - Don't silently fail
|
|
8
|
+
*
|
|
9
|
+
* These tests run CLI commands as subprocesses to verify
|
|
10
|
+
* the entry point error handling works end-to-end.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { resolve } from 'node:path';
|
|
15
|
+
const CLI_DIR = resolve(__dirname, '../../dist');
|
|
16
|
+
/**
|
|
17
|
+
* Helper to run a CLI command as subprocess
|
|
18
|
+
*/
|
|
19
|
+
function runCLI(command, args = []) {
|
|
20
|
+
const result = spawnSync('node', [resolve(CLI_DIR, `${command}.js`), ...args], {
|
|
21
|
+
encoding: 'utf-8',
|
|
22
|
+
timeout: 10000,
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
code: result.status ?? -1,
|
|
26
|
+
stdout: result.stdout ?? '',
|
|
27
|
+
stderr: result.stderr ?? '',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
describe('CLI subprocess error handling', () => {
|
|
31
|
+
describe('wu-claim', () => {
|
|
32
|
+
it('should exit with non-zero code when required options are missing', () => {
|
|
33
|
+
const result = runCLI('wu-claim', ['--id', 'WU-TEST']);
|
|
34
|
+
// Should NOT exit 0 (silent failure)
|
|
35
|
+
expect(result.code).not.toBe(0);
|
|
36
|
+
// Should have some error output
|
|
37
|
+
expect(result.stderr.length + result.stdout.length).toBeGreaterThan(0);
|
|
38
|
+
});
|
|
39
|
+
it('should output help when --help is passed', () => {
|
|
40
|
+
const result = runCLI('wu-claim', ['--help']);
|
|
41
|
+
// Help should work
|
|
42
|
+
expect(result.code).toBe(0);
|
|
43
|
+
expect(result.stdout).toContain('Usage');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
describe('wu-done', () => {
|
|
47
|
+
it('should exit with non-zero code for non-existent WU', () => {
|
|
48
|
+
const result = runCLI('wu-done', ['--id', 'WU-NONEXISTENT-99999']);
|
|
49
|
+
// Should NOT exit 0 (silent failure)
|
|
50
|
+
expect(result.code).not.toBe(0);
|
|
51
|
+
// Should have some error output
|
|
52
|
+
expect(result.stderr.length + result.stdout.length).toBeGreaterThan(0);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('wu-create', () => {
|
|
56
|
+
it('should exit with non-zero code when validation fails', () => {
|
|
57
|
+
const result = runCLI('wu-create', ['--id', 'WU-TEST', '--lane', 'Invalid Lane']);
|
|
58
|
+
// Should NOT exit 0 (silent failure)
|
|
59
|
+
expect(result.code).not.toBe(0);
|
|
60
|
+
// Should have some error output
|
|
61
|
+
expect(result.stderr.length + result.stdout.length).toBeGreaterThan(0);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared CLI entry point wrapper
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent error handling for all CLI commands.
|
|
5
|
+
* Catches async errors, logs them, and exits with proper code.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // At the bottom of each CLI file:
|
|
10
|
+
* import { runCLI } from './cli-entry-point.js';
|
|
11
|
+
* import { fileURLToPath } from 'node:url';
|
|
12
|
+
*
|
|
13
|
+
* if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
14
|
+
* runCLI(main);
|
|
15
|
+
* }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
|
|
19
|
+
/**
|
|
20
|
+
* Wraps an async main function with proper error handling.
|
|
21
|
+
*
|
|
22
|
+
* @param main - The async main function to execute
|
|
23
|
+
* @returns Promise that resolves when main completes (or after error handling)
|
|
24
|
+
*/
|
|
25
|
+
export async function runCLI(main) {
|
|
26
|
+
try {
|
|
27
|
+
await main();
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
const message = getErrorMessage(err);
|
|
31
|
+
console.error(message);
|
|
32
|
+
process.exit(EXIT_CODES.ERROR);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Extracts error message from unknown error type.
|
|
37
|
+
*/
|
|
38
|
+
function getErrorMessage(err) {
|
|
39
|
+
if (err === null || err === undefined) {
|
|
40
|
+
return 'Unknown error';
|
|
41
|
+
}
|
|
42
|
+
if (err instanceof Error) {
|
|
43
|
+
return err.message;
|
|
44
|
+
}
|
|
45
|
+
return String(err);
|
|
46
|
+
}
|
|
@@ -229,6 +229,7 @@ async function main() {
|
|
|
229
229
|
}
|
|
230
230
|
// Guard main() for testability
|
|
231
231
|
import { fileURLToPath } from 'node:url';
|
|
232
|
+
import { runCLI } from './cli-entry-point.js';
|
|
232
233
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
233
|
-
main
|
|
234
|
+
runCLI(main);
|
|
234
235
|
}
|
|
@@ -163,6 +163,7 @@ async function main() {
|
|
|
163
163
|
}
|
|
164
164
|
// Guard main() for testability
|
|
165
165
|
import { fileURLToPath } from 'node:url';
|
|
166
|
+
import { runCLI } from './cli-entry-point.js';
|
|
166
167
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
167
|
-
main
|
|
168
|
+
runCLI(main);
|
|
168
169
|
}
|
package/dist/initiative-list.js
CHANGED
|
@@ -96,6 +96,7 @@ async function main() {
|
|
|
96
96
|
}
|
|
97
97
|
// Guard main() for testability (WU-1366)
|
|
98
98
|
import { fileURLToPath } from 'node:url';
|
|
99
|
+
import { runCLI } from './cli-entry-point.js';
|
|
99
100
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
100
|
-
main
|
|
101
|
+
runCLI(main);
|
|
101
102
|
}
|
|
@@ -216,6 +216,7 @@ async function main() {
|
|
|
216
216
|
}
|
|
217
217
|
// Guard main() for testability (WU-1366)
|
|
218
218
|
import { fileURLToPath } from 'node:url';
|
|
219
|
+
import { runCLI } from './cli-entry-point.js';
|
|
219
220
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
220
|
-
main
|
|
221
|
+
runCLI(main);
|
|
221
222
|
}
|
package/dist/wu-claim.js
CHANGED
|
@@ -1298,6 +1298,7 @@ async function main() {
|
|
|
1298
1298
|
}
|
|
1299
1299
|
// Guard main() for testability (WU-1366)
|
|
1300
1300
|
import { fileURLToPath } from 'node:url';
|
|
1301
|
+
import { runCLI } from './cli-entry-point.js';
|
|
1301
1302
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
1302
|
-
main
|
|
1303
|
+
runCLI(main);
|
|
1303
1304
|
}
|
package/dist/wu-cleanup.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Cleans up worktree and branch after PR merge (PR-based completion workflow).
|
|
6
6
|
*
|
|
7
7
|
* Sequence:
|
|
8
|
-
* 1) Verify PR is merged (via gh API
|
|
8
|
+
* 1) Verify PR is merged (via gh API; no merge-base fallback)
|
|
9
9
|
* 2) Remove worktree (if exists)
|
|
10
10
|
* 3) Delete lane branch (local + remote)
|
|
11
11
|
*
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* pnpm wu:cleanup --artifacts
|
|
17
17
|
*/
|
|
18
18
|
import { execSync } from 'node:child_process';
|
|
19
|
-
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
19
|
+
import { createGitForPath, getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
20
20
|
import { existsSync } from 'node:fs';
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
@@ -25,16 +25,11 @@ import { cleanupWorktreeBuildArtifacts } from '@lumenflow/core/dist/rebase-artif
|
|
|
25
25
|
import { detectCurrentWorktree } from '@lumenflow/core/dist/wu-done-validators.js';
|
|
26
26
|
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
27
27
|
import { readWU } from '@lumenflow/core/dist/wu-yaml.js';
|
|
28
|
-
import {
|
|
28
|
+
import { isGhCliAvailable } from '@lumenflow/core/dist/wu-done-pr.js';
|
|
29
|
+
import { BOX, CLEANUP_GUARD, EXIT_CODES, FILE_SYSTEM, LOG_PREFIX, REMOTES, STRING_LITERALS, WU_STATUS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
29
30
|
// WU-2278: Import ownership validation for cross-agent protection
|
|
30
31
|
import { validateWorktreeOwnership } from '@lumenflow/core/dist/worktree-ownership.js';
|
|
31
32
|
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
32
|
-
// Box drawing characters for consistent output
|
|
33
|
-
const BOX = {
|
|
34
|
-
TOP: '╔═══════════════════════════════════════════════════════════════════╗',
|
|
35
|
-
MID: '╠═══════════════════════════════════════════════════════════════════╣',
|
|
36
|
-
BOT: '╚═══════════════════════════════════════════════════════════════════╝',
|
|
37
|
-
};
|
|
38
33
|
const CLEANUP_OPTIONS = {
|
|
39
34
|
artifacts: {
|
|
40
35
|
name: 'artifacts',
|
|
@@ -42,15 +37,36 @@ const CLEANUP_OPTIONS = {
|
|
|
42
37
|
description: 'Remove build artifacts (dist, tsbuildinfo) in current worktree',
|
|
43
38
|
},
|
|
44
39
|
};
|
|
40
|
+
export const CLEANUP_GUARD_REASONS = CLEANUP_GUARD.REASONS;
|
|
41
|
+
export function evaluateCleanupGuards({ hasUncommittedChanges, hasUnpushedCommits, hasStamp, yamlStatus, ghAvailable, prMerged, }) {
|
|
42
|
+
if (hasUncommittedChanges) {
|
|
43
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.UNCOMMITTED_CHANGES };
|
|
44
|
+
}
|
|
45
|
+
if (hasUnpushedCommits) {
|
|
46
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.UNPUSHED_COMMITS };
|
|
47
|
+
}
|
|
48
|
+
if (yamlStatus !== WU_STATUS.DONE) {
|
|
49
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.STATUS_NOT_DONE };
|
|
50
|
+
}
|
|
51
|
+
if (!hasStamp) {
|
|
52
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.MISSING_STAMP };
|
|
53
|
+
}
|
|
54
|
+
if (ghAvailable && prMerged !== true) {
|
|
55
|
+
return { allowed: false, reason: CLEANUP_GUARD_REASONS.PR_NOT_MERGED };
|
|
56
|
+
}
|
|
57
|
+
return { allowed: true, reason: null };
|
|
58
|
+
}
|
|
45
59
|
// Help text is now auto-generated by commander via createWUParser
|
|
46
60
|
async function verifyPRMerged(laneBranch) {
|
|
47
|
-
|
|
61
|
+
if (!isGhCliAvailable()) {
|
|
62
|
+
return { merged: null, method: 'gh_unavailable' };
|
|
63
|
+
}
|
|
48
64
|
let ghResult;
|
|
49
65
|
try {
|
|
50
66
|
ghResult = execSync(`gh api repos/:owner/:repo/pulls -q '.[] | select(.head.ref == "${laneBranch}") | .merged'`, { encoding: FILE_SYSTEM.UTF8 }).trim();
|
|
51
67
|
}
|
|
52
68
|
catch {
|
|
53
|
-
ghResult =
|
|
69
|
+
ghResult = STRING_LITERALS.EMPTY;
|
|
54
70
|
}
|
|
55
71
|
if (ghResult === 'true') {
|
|
56
72
|
return { merged: true, method: 'gh_api' };
|
|
@@ -58,37 +74,7 @@ async function verifyPRMerged(laneBranch) {
|
|
|
58
74
|
if (ghResult === 'false') {
|
|
59
75
|
return { merged: false, method: 'gh_api' };
|
|
60
76
|
}
|
|
61
|
-
|
|
62
|
-
// Always fetch origin/main first for accurate merge-base check
|
|
63
|
-
await getGitForCwd().fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
|
|
64
|
-
const localBranchExists = await getGitForCwd().branchExists(laneBranch);
|
|
65
|
-
if (!localBranchExists) {
|
|
66
|
-
// Branch doesn't exist locally - check if it exists remotely
|
|
67
|
-
const remoteBranchExists = await getGitForCwd().raw([
|
|
68
|
-
'ls-remote',
|
|
69
|
-
'--heads',
|
|
70
|
-
REMOTES.ORIGIN,
|
|
71
|
-
laneBranch,
|
|
72
|
-
]);
|
|
73
|
-
if (!remoteBranchExists) {
|
|
74
|
-
// Branch is gone both locally and remotely - assume merged
|
|
75
|
-
return { merged: true, method: 'branch_deleted' };
|
|
76
|
-
}
|
|
77
|
-
// Branch exists remotely but not locally - need to fetch
|
|
78
|
-
await getGitForCwd().fetch(REMOTES.ORIGIN, laneBranch);
|
|
79
|
-
}
|
|
80
|
-
let isAncestor;
|
|
81
|
-
try {
|
|
82
|
-
await getGitForCwd().raw(['merge-base', '--is-ancestor', laneBranch, GIT_REFS.ORIGIN_MAIN]);
|
|
83
|
-
isAncestor = true;
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
isAncestor = false;
|
|
87
|
-
}
|
|
88
|
-
if (isAncestor) {
|
|
89
|
-
return { merged: true, method: 'git_merge_base' };
|
|
90
|
-
}
|
|
91
|
-
return { merged: false, method: 'git_merge_base' };
|
|
77
|
+
return { merged: null, method: 'gh_api' };
|
|
92
78
|
}
|
|
93
79
|
async function removeWorktree(worktreePath) {
|
|
94
80
|
if (!existsSync(worktreePath)) {
|
|
@@ -154,6 +140,31 @@ async function cleanupArtifactsInWorktree() {
|
|
|
154
140
|
}
|
|
155
141
|
console.log(`${LOG_PREFIX.CLEANUP} ✓ Build artifact cleanup complete`);
|
|
156
142
|
}
|
|
143
|
+
async function hasUncommittedChanges(worktreePath) {
|
|
144
|
+
if (!existsSync(worktreePath)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const git = createGitForPath(worktreePath);
|
|
148
|
+
const status = await git.getStatus();
|
|
149
|
+
return status.length > 0;
|
|
150
|
+
}
|
|
151
|
+
async function hasUnpushedCommits(worktreePath) {
|
|
152
|
+
if (!existsSync(worktreePath)) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const git = createGitForPath(worktreePath);
|
|
156
|
+
try {
|
|
157
|
+
const unpushed = await git.getUnpushedCommits();
|
|
158
|
+
return unpushed.length > 0;
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function hasStampFile(wuId) {
|
|
165
|
+
const stampPath = path.join(process.cwd(), WU_PATHS.STAMP(wuId));
|
|
166
|
+
return existsSync(stampPath);
|
|
167
|
+
}
|
|
157
168
|
async function main() {
|
|
158
169
|
const args = createWUParser({
|
|
159
170
|
name: 'wu-cleanup',
|
|
@@ -181,6 +192,7 @@ async function main() {
|
|
|
181
192
|
const idK = args.id.toLowerCase();
|
|
182
193
|
const laneBranch = `lane/${laneK}/${idK}`;
|
|
183
194
|
const worktreePath = path.join('worktrees', `${laneK}-${idK}`);
|
|
195
|
+
const absoluteWorktreePath = path.resolve(worktreePath);
|
|
184
196
|
console.log(`[wu-cleanup] Cleaning up ${args.id} (${wu.title})`);
|
|
185
197
|
console.log(`[wu-cleanup] Lane: ${wu.lane}`);
|
|
186
198
|
console.log(`[wu-cleanup] Branch: ${laneBranch}`);
|
|
@@ -200,25 +212,38 @@ async function main() {
|
|
|
200
212
|
console.error(BOX.BOT);
|
|
201
213
|
process.exit(EXIT_CODES.ERROR);
|
|
202
214
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
215
|
+
const cleanupCheck = {
|
|
216
|
+
hasUncommittedChanges: await hasUncommittedChanges(absoluteWorktreePath),
|
|
217
|
+
hasUnpushedCommits: await hasUnpushedCommits(absoluteWorktreePath),
|
|
218
|
+
hasStamp: hasStampFile(id),
|
|
219
|
+
yamlStatus: wu.status,
|
|
220
|
+
ghAvailable: isGhCliAvailable(),
|
|
221
|
+
prMerged: null,
|
|
222
|
+
};
|
|
223
|
+
if (cleanupCheck.ghAvailable) {
|
|
224
|
+
console.log(`${LOG_PREFIX.CLEANUP} ${CLEANUP_GUARD.PR_CHECK.START}`);
|
|
225
|
+
const { merged, method } = await verifyPRMerged(laneBranch);
|
|
226
|
+
cleanupCheck.prMerged = merged;
|
|
227
|
+
console.log(`${LOG_PREFIX.CLEANUP} ${CLEANUP_GUARD.PR_CHECK.RESULT} ${method}`);
|
|
228
|
+
console.log();
|
|
229
|
+
}
|
|
230
|
+
const guardResult = evaluateCleanupGuards(cleanupCheck);
|
|
231
|
+
if (!guardResult.allowed) {
|
|
207
232
|
console.error();
|
|
208
233
|
console.error(BOX.TOP);
|
|
209
|
-
console.error(
|
|
234
|
+
console.error(`${BOX.SIDE} ${CLEANUP_GUARD.TITLES.BLOCKED}`);
|
|
210
235
|
console.error(BOX.MID);
|
|
211
|
-
console.error(
|
|
212
|
-
console.error(
|
|
213
|
-
console.error(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
236
|
+
console.error(`${BOX.SIDE} ${CLEANUP_GUARD.MESSAGES[guardResult.reason]}`);
|
|
237
|
+
console.error(`${BOX.SIDE}`);
|
|
238
|
+
console.error(`${BOX.SIDE} ${CLEANUP_GUARD.TITLES.NEXT_STEPS}`);
|
|
239
|
+
const steps = CLEANUP_GUARD.NEXT_STEPS[guardResult.reason] || CLEANUP_GUARD.NEXT_STEPS.DEFAULT;
|
|
240
|
+
for (const step of steps) {
|
|
241
|
+
const line = step.appendId ? `${step.text} ${args.id}` : step.text;
|
|
242
|
+
console.error(`${BOX.SIDE} ${line}`);
|
|
243
|
+
}
|
|
217
244
|
console.error(BOX.BOT);
|
|
218
245
|
process.exit(EXIT_CODES.ERROR);
|
|
219
246
|
}
|
|
220
|
-
console.log(`[wu-cleanup] ✓ PR merged (verified via ${method})`);
|
|
221
|
-
console.log();
|
|
222
247
|
// 2. Remove worktree
|
|
223
248
|
await removeWorktree(worktreePath);
|
|
224
249
|
console.log();
|
|
@@ -235,6 +260,7 @@ async function main() {
|
|
|
235
260
|
}
|
|
236
261
|
// Guard main() for testability (WU-1366)
|
|
237
262
|
import { fileURLToPath } from 'node:url';
|
|
263
|
+
import { runCLI } from './cli-entry-point.js';
|
|
238
264
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
239
|
-
main
|
|
265
|
+
runCLI(main);
|
|
240
266
|
}
|
package/dist/wu-create.js
CHANGED
|
@@ -589,6 +589,7 @@ async function main() {
|
|
|
589
589
|
}
|
|
590
590
|
// Guard main() for testability (WU-1366)
|
|
591
591
|
import { fileURLToPath } from 'node:url';
|
|
592
|
+
import { runCLI } from './cli-entry-point.js';
|
|
592
593
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
593
|
-
main
|
|
594
|
+
runCLI(main);
|
|
594
595
|
}
|
package/dist/wu-deps.js
CHANGED
|
@@ -114,6 +114,7 @@ function renderGraphJSON(graph, rootId, depth, direction) {
|
|
|
114
114
|
}
|
|
115
115
|
// Guard main() for testability (WU-1366)
|
|
116
116
|
import { fileURLToPath } from 'node:url';
|
|
117
|
+
import { runCLI } from './cli-entry-point.js';
|
|
117
118
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
118
|
-
main
|
|
119
|
+
runCLI(main);
|
|
119
120
|
}
|
package/dist/wu-done.js
CHANGED
|
@@ -2214,6 +2214,7 @@ async function detectChangedDocPaths(worktreePath, baseBranch) {
|
|
|
2214
2214
|
// Guard main() execution for testability (WU-1366)
|
|
2215
2215
|
// When imported as a module for testing, main() should not auto-run
|
|
2216
2216
|
import { fileURLToPath } from 'node:url';
|
|
2217
|
+
import { runCLI } from './cli-entry-point.js';
|
|
2217
2218
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
2218
|
-
main
|
|
2219
|
+
runCLI(main);
|
|
2219
2220
|
}
|
package/dist/wu-infer-lane.js
CHANGED
|
@@ -93,7 +93,7 @@ function loadWuYaml(id) {
|
|
|
93
93
|
` 2. Fix YAML errors manually and retry`);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
-
function main() {
|
|
96
|
+
async function main() {
|
|
97
97
|
const args = parseArgs(process.argv);
|
|
98
98
|
let codePaths = [];
|
|
99
99
|
let description = '';
|
|
@@ -130,6 +130,7 @@ function main() {
|
|
|
130
130
|
}
|
|
131
131
|
// Guard main() for testability (WU-1366)
|
|
132
132
|
import { fileURLToPath } from 'node:url';
|
|
133
|
+
import { runCLI } from './cli-entry-point.js';
|
|
133
134
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
134
|
-
main
|
|
135
|
+
runCLI(main);
|
|
135
136
|
}
|
package/dist/wu-preflight.js
CHANGED
|
@@ -160,8 +160,9 @@ async function main() {
|
|
|
160
160
|
process.exit(EXIT_CODES.SUCCESS);
|
|
161
161
|
}
|
|
162
162
|
// Guard main() for testability
|
|
163
|
+
import { runCLI } from './cli-entry-point.js';
|
|
163
164
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
164
|
-
main
|
|
165
|
+
runCLI(main);
|
|
165
166
|
}
|
|
166
167
|
// Export for testing
|
|
167
168
|
export { parseArgs, detectWorktreePath };
|
package/dist/wu-prune.js
CHANGED
|
@@ -19,8 +19,8 @@ import { readWUYaml, validateBranchName, extractWUFromBranch, } from '@lumenflow
|
|
|
19
19
|
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
20
20
|
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
21
21
|
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
22
|
-
import { detectOrphanWorktrees, removeOrphanDirectory, } from '@lumenflow/core/dist/orphan-detector.js';
|
|
23
|
-
import { BRANCHES, WU_STATUS, CLI_FLAGS, EXIT_CODES, STRING_LITERALS, EMOJI, LOG_PREFIX, } from '@lumenflow/core/dist/wu-constants.js';
|
|
22
|
+
import { detectOrphanWorktrees, detectMissingTrackedWorktrees, removeOrphanDirectory, } from '@lumenflow/core/dist/orphan-detector.js';
|
|
23
|
+
import { BRANCHES, WU_STATUS, CLI_FLAGS, EXIT_CODES, STRING_LITERALS, EMOJI, LOG_PREFIX, WORKTREE_WARNINGS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
24
24
|
function parseArgs(argv) {
|
|
25
25
|
const args = { dryRun: true }; // Default to dry-run for safety
|
|
26
26
|
for (let i = 2; i < argv.length; i++) {
|
|
@@ -145,6 +145,14 @@ This tool:
|
|
|
145
145
|
}
|
|
146
146
|
console.log(`${PREFIX} Worktree Hygiene Check`);
|
|
147
147
|
console.log(`${PREFIX} =====================\n`);
|
|
148
|
+
const missingTracked = await detectMissingTrackedWorktrees(process.cwd());
|
|
149
|
+
if (missingTracked.length > 0) {
|
|
150
|
+
console.warn(`${PREFIX} ${EMOJI.WARNING} ${WORKTREE_WARNINGS.MISSING_TRACKED_HEADER}`);
|
|
151
|
+
for (const missingPath of missingTracked) {
|
|
152
|
+
console.warn(`${PREFIX} ${WORKTREE_WARNINGS.MISSING_TRACKED_LINE(missingPath)}`);
|
|
153
|
+
}
|
|
154
|
+
console.warn('');
|
|
155
|
+
}
|
|
148
156
|
if (args.dryRun) {
|
|
149
157
|
console.log(`${PREFIX} ${EMOJI.INFO} DRY-RUN MODE (use --execute to apply changes)\n`);
|
|
150
158
|
}
|
|
@@ -254,6 +262,7 @@ This tool:
|
|
|
254
262
|
}
|
|
255
263
|
// Guard main() for testability (WU-1366)
|
|
256
264
|
import { fileURLToPath } from 'node:url';
|
|
265
|
+
import { runCLI } from './cli-entry-point.js';
|
|
257
266
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
258
|
-
main
|
|
267
|
+
runCLI(main);
|
|
259
268
|
}
|
package/dist/wu-repair.js
CHANGED
|
@@ -219,8 +219,9 @@ async function main() {
|
|
|
219
219
|
}
|
|
220
220
|
// Guard main() for testability (WU-1366)
|
|
221
221
|
import { fileURLToPath } from 'node:url';
|
|
222
|
+
import { runCLI } from './cli-entry-point.js';
|
|
222
223
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
223
|
-
main
|
|
224
|
+
runCLI(main);
|
|
224
225
|
}
|
|
225
226
|
// Export for testing
|
|
226
227
|
export { normaliseWUId, isValidWUId };
|
package/dist/wu-spawn.js
CHANGED
|
@@ -1241,6 +1241,7 @@ async function main() {
|
|
|
1241
1241
|
}
|
|
1242
1242
|
// Guard main() for testability
|
|
1243
1243
|
import { fileURLToPath } from 'node:url';
|
|
1244
|
+
import { runCLI } from './cli-entry-point.js';
|
|
1244
1245
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
1245
|
-
main
|
|
1246
|
+
runCLI(main);
|
|
1246
1247
|
}
|
package/dist/wu-unlock-lane.js
CHANGED
|
@@ -155,4 +155,9 @@ async function main() {
|
|
|
155
155
|
console.log(`${PREFIX} Previous owner: ${result.previousLock?.wuId || 'unknown'}`);
|
|
156
156
|
console.log(`${PREFIX} Reason: ${result.reason}`);
|
|
157
157
|
}
|
|
158
|
-
main()
|
|
158
|
+
// Guard main() for testability (WU-1064)
|
|
159
|
+
import { fileURLToPath } from 'node:url';
|
|
160
|
+
import { runCLI } from './cli-entry-point.js';
|
|
161
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
162
|
+
runCLI(main);
|
|
163
|
+
}
|
package/dist/wu-validate.js
CHANGED
|
@@ -188,6 +188,7 @@ async function main() {
|
|
|
188
188
|
}
|
|
189
189
|
// Guard main() for testability
|
|
190
190
|
import { fileURLToPath } from 'node:url';
|
|
191
|
+
import { runCLI } from './cli-entry-point.js';
|
|
191
192
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
192
|
-
main
|
|
193
|
+
runCLI(main);
|
|
193
194
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumenflow/cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Command-line interface for LumenFlow workflow framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lumenflow",
|
|
@@ -85,11 +85,11 @@
|
|
|
85
85
|
"pretty-ms": "^9.2.0",
|
|
86
86
|
"simple-git": "^3.30.0",
|
|
87
87
|
"yaml": "^2.8.2",
|
|
88
|
-
"@lumenflow/
|
|
89
|
-
"@lumenflow/
|
|
90
|
-
"@lumenflow/
|
|
91
|
-
"@lumenflow/
|
|
92
|
-
"@lumenflow/
|
|
88
|
+
"@lumenflow/core": "1.3.2",
|
|
89
|
+
"@lumenflow/metrics": "1.3.2",
|
|
90
|
+
"@lumenflow/memory": "1.3.2",
|
|
91
|
+
"@lumenflow/initiatives": "1.3.2",
|
|
92
|
+
"@lumenflow/agent": "1.3.2"
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
95
|
"@vitest/coverage-v8": "^4.0.17",
|
package/dist/gates.d.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Quality Gates Runner
|
|
4
|
-
*
|
|
5
|
-
* Runs quality gates with support for docs-only mode and incremental linting.
|
|
6
|
-
*
|
|
7
|
-
* WU-1304: Optimise ESLint gates performance
|
|
8
|
-
* - Uses incremental linting (only files changed since branching from main)
|
|
9
|
-
* - Full lint coverage maintained via CI workflow
|
|
10
|
-
*
|
|
11
|
-
* WU-1433: Coverage gate with mode flag
|
|
12
|
-
* - Checks coverage thresholds for hex core files (≥90% for application layer)
|
|
13
|
-
* - Mode: block (default) fails the gate, warn logs warnings only
|
|
14
|
-
* WU-2334: Changed default from warn to block for TDD enforcement
|
|
15
|
-
*
|
|
16
|
-
* WU-1610: Supabase docs linter
|
|
17
|
-
* - Verifies every table in migrations is documented in schema.md
|
|
18
|
-
* - Fails if any table is missing documentation
|
|
19
|
-
*
|
|
20
|
-
* For type:documentation WUs:
|
|
21
|
-
* - ✅ Run: format:check, spec:linter, prompts:lint, backlog-sync
|
|
22
|
-
* - ❌ Skip: lint, typecheck, supabase-docs:linter, tests, coverage (no code changed)
|
|
23
|
-
*
|
|
24
|
-
* WU-1920: Incremental test execution
|
|
25
|
-
* - Uses Vitest's --changed flag to run only tests for changed files
|
|
26
|
-
* - Full test suite maintained via CI workflow and --full-tests flag
|
|
27
|
-
*
|
|
28
|
-
* WU-2062: Tiered test execution for faster wu:done
|
|
29
|
-
* - Safety-critical tests (PHI, escalation, red-flag) ALWAYS run
|
|
30
|
-
* - Docs-only WUs: lint/typecheck only (auto-detected or --docs-only flag)
|
|
31
|
-
* - High-risk WUs (auth, PHI, RLS, migrations): run integration tests
|
|
32
|
-
* - Standard WUs: changed tests + safety-critical tests
|
|
33
|
-
*
|
|
34
|
-
* Usage:
|
|
35
|
-
* node tools/gates.mjs # Tiered gates (default)
|
|
36
|
-
* node tools/gates.mjs --docs-only # Docs-only gates
|
|
37
|
-
* node tools/gates.mjs --full-lint # Full lint (bypass incremental)
|
|
38
|
-
* node tools/gates.mjs --full-tests # Full tests (bypass incremental)
|
|
39
|
-
* node tools/gates.mjs --coverage-mode=block # Coverage gate in block mode
|
|
40
|
-
*/
|
|
41
|
-
export {};
|
package/dist/gates.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG"}
|