@lumenflow/cli 2.4.0 → 2.5.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__/init-config-lanes.test.js +131 -0
- package/dist/__tests__/init-docs-structure.test.js +119 -0
- package/dist/__tests__/init-lane-inference.test.js +125 -0
- package/dist/__tests__/init-onboarding-docs.test.js +132 -0
- package/dist/__tests__/init-quick-ref.test.js +145 -0
- package/dist/__tests__/init-scripts.test.js +96 -0
- package/dist/__tests__/init-template-portability.test.js +97 -0
- package/dist/__tests__/init.test.js +7 -2
- package/dist/__tests__/initiative-add-wu.test.js +420 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +162 -0
- package/dist/__tests__/initiative-remove-wu.test.js +458 -0
- package/dist/__tests__/onboarding-smoke-test.test.js +211 -0
- package/dist/__tests__/path-centralization-cli.test.js +234 -0
- package/dist/__tests__/plan-create.test.js +126 -0
- package/dist/__tests__/plan-edit.test.js +157 -0
- package/dist/__tests__/plan-link.test.js +239 -0
- package/dist/__tests__/plan-promote.test.js +181 -0
- package/dist/__tests__/wu-create-strict.test.js +118 -0
- package/dist/__tests__/wu-edit-strict.test.js +109 -0
- package/dist/__tests__/wu-validate-strict.test.js +113 -0
- package/dist/flow-bottlenecks.js +4 -2
- package/dist/gates.js +22 -0
- package/dist/init.js +580 -87
- package/dist/initiative-add-wu.js +112 -16
- package/dist/initiative-remove-wu.js +248 -0
- package/dist/onboarding-smoke-test.js +400 -0
- package/dist/plan-create.js +199 -0
- package/dist/plan-edit.js +235 -0
- package/dist/plan-link.js +233 -0
- package/dist/plan-promote.js +231 -0
- package/dist/wu-block.js +16 -5
- package/dist/wu-claim.js +15 -9
- package/dist/wu-create.js +50 -2
- package/dist/wu-deps.js +3 -1
- package/dist/wu-done.js +14 -5
- package/dist/wu-edit.js +35 -0
- package/dist/wu-spawn.js +8 -0
- package/dist/wu-unblock.js +34 -2
- package/dist/wu-validate.js +25 -17
- package/package.json +10 -6
- package/templates/core/AGENTS.md.template +2 -2
- package/dist/__tests__/init-plan.test.js +0 -340
- package/dist/agent-issues-query.d.ts +0 -16
- package/dist/agent-log-issue.d.ts +0 -10
- package/dist/agent-session-end.d.ts +0 -10
- package/dist/agent-session.d.ts +0 -10
- package/dist/backlog-prune.d.ts +0 -84
- package/dist/cli-entry-point.d.ts +0 -8
- package/dist/deps-add.d.ts +0 -91
- package/dist/deps-remove.d.ts +0 -17
- package/dist/docs-sync.d.ts +0 -50
- package/dist/file-delete.d.ts +0 -84
- package/dist/file-edit.d.ts +0 -82
- package/dist/file-read.d.ts +0 -92
- package/dist/file-write.d.ts +0 -90
- package/dist/flow-bottlenecks.d.ts +0 -16
- package/dist/flow-report.d.ts +0 -16
- package/dist/gates.d.ts +0 -94
- package/dist/git-branch.d.ts +0 -65
- package/dist/git-diff.d.ts +0 -58
- package/dist/git-log.d.ts +0 -69
- package/dist/git-status.d.ts +0 -58
- package/dist/guard-locked.d.ts +0 -62
- package/dist/guard-main-branch.d.ts +0 -50
- package/dist/guard-worktree-commit.d.ts +0 -59
- package/dist/index.d.ts +0 -10
- package/dist/init-plan.d.ts +0 -80
- package/dist/init-plan.js +0 -337
- package/dist/init.d.ts +0 -46
- package/dist/initiative-add-wu.d.ts +0 -22
- package/dist/initiative-bulk-assign-wus.d.ts +0 -16
- package/dist/initiative-create.d.ts +0 -28
- package/dist/initiative-edit.d.ts +0 -34
- package/dist/initiative-list.d.ts +0 -12
- package/dist/initiative-status.d.ts +0 -11
- package/dist/lumenflow-upgrade.d.ts +0 -103
- package/dist/mem-checkpoint.d.ts +0 -16
- package/dist/mem-cleanup.d.ts +0 -29
- package/dist/mem-create.d.ts +0 -17
- package/dist/mem-export.d.ts +0 -10
- package/dist/mem-inbox.d.ts +0 -35
- package/dist/mem-init.d.ts +0 -15
- package/dist/mem-ready.d.ts +0 -16
- package/dist/mem-signal.d.ts +0 -16
- package/dist/mem-start.d.ts +0 -16
- package/dist/mem-summarize.d.ts +0 -22
- package/dist/mem-triage.d.ts +0 -22
- package/dist/metrics-cli.d.ts +0 -90
- package/dist/metrics-snapshot.d.ts +0 -18
- package/dist/orchestrate-init-status.d.ts +0 -11
- package/dist/orchestrate-initiative.d.ts +0 -12
- package/dist/orchestrate-monitor.d.ts +0 -11
- package/dist/release.d.ts +0 -117
- package/dist/rotate-progress.d.ts +0 -48
- package/dist/session-coordinator.d.ts +0 -74
- package/dist/spawn-list.d.ts +0 -16
- package/dist/state-bootstrap.d.ts +0 -92
- package/dist/sync-templates.d.ts +0 -52
- package/dist/trace-gen.d.ts +0 -84
- package/dist/validate-agent-skills.d.ts +0 -50
- package/dist/validate-agent-sync.d.ts +0 -36
- package/dist/validate-backlog-sync.d.ts +0 -37
- package/dist/validate-skills-spec.d.ts +0 -40
- package/dist/validate.d.ts +0 -60
- package/dist/wu-block.d.ts +0 -16
- package/dist/wu-claim.d.ts +0 -74
- package/dist/wu-cleanup.d.ts +0 -35
- package/dist/wu-create.d.ts +0 -69
- package/dist/wu-delete.d.ts +0 -21
- package/dist/wu-deps.d.ts +0 -13
- package/dist/wu-done.d.ts +0 -225
- package/dist/wu-edit.d.ts +0 -63
- package/dist/wu-infer-lane.d.ts +0 -17
- package/dist/wu-preflight.d.ts +0 -47
- package/dist/wu-prune.d.ts +0 -16
- package/dist/wu-recover.d.ts +0 -37
- package/dist/wu-release.d.ts +0 -19
- package/dist/wu-repair.d.ts +0 -60
- package/dist/wu-spawn-completion.d.ts +0 -10
- package/dist/wu-spawn.d.ts +0 -192
- package/dist/wu-status.d.ts +0 -25
- package/dist/wu-unblock.d.ts +0 -16
- package/dist/wu-unlock-lane.d.ts +0 -19
- package/dist/wu-validate.d.ts +0 -16
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
3
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
4
|
+
/**
|
|
5
|
+
* Plan Edit Command (WU-1313)
|
|
6
|
+
*
|
|
7
|
+
* Edits existing plan files in the repo-native plansDir.
|
|
8
|
+
* Supports updating or appending to specific sections.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* pnpm plan:edit --id WU-1313 --section Goal --content "New goal content"
|
|
12
|
+
* pnpm plan:edit --id WU-1313 --section Risks --append "- New risk"
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
* - Updates specific sections by name
|
|
16
|
+
* - Supports append mode for list sections (Risks, etc.)
|
|
17
|
+
* - Uses micro-worktree isolation for atomic commits
|
|
18
|
+
* - Validates section exists before editing
|
|
19
|
+
*
|
|
20
|
+
* Context: WU-1313 (INIT-013 Plan Tooling)
|
|
21
|
+
*/
|
|
22
|
+
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
23
|
+
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
27
|
+
import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
|
|
28
|
+
import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
|
|
29
|
+
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
30
|
+
import { LOG_PREFIX as CORE_LOG_PREFIX } from '@lumenflow/core/dist/wu-constants.js';
|
|
31
|
+
/** Log prefix for console output */
|
|
32
|
+
export const LOG_PREFIX = CORE_LOG_PREFIX.PLAN_EDIT ?? '[plan:edit]';
|
|
33
|
+
/** Micro-worktree operation name */
|
|
34
|
+
const OPERATION_NAME = 'plan-edit';
|
|
35
|
+
/** WU ID pattern */
|
|
36
|
+
const WU_ID_PATTERN = /^WU-\d+$/;
|
|
37
|
+
/** Initiative ID pattern */
|
|
38
|
+
const INIT_ID_PATTERN = /^INIT-[A-Z0-9]+$/i;
|
|
39
|
+
/**
|
|
40
|
+
* Get the path to a plan file from its ID
|
|
41
|
+
*
|
|
42
|
+
* @param id - WU or Initiative ID
|
|
43
|
+
* @returns Path to plan file
|
|
44
|
+
* @throws Error if plan not found
|
|
45
|
+
*/
|
|
46
|
+
export function getPlanPath(id) {
|
|
47
|
+
const plansDir = WU_PATHS.PLANS_DIR();
|
|
48
|
+
const planPath = join(plansDir, `${id}-plan.md`);
|
|
49
|
+
if (!existsSync(planPath)) {
|
|
50
|
+
die(`Plan not found for ${id}\n\n` +
|
|
51
|
+
`Expected path: ${planPath}\n\n` +
|
|
52
|
+
`Create it first with: pnpm plan:create --id ${id} --title "Title"`);
|
|
53
|
+
}
|
|
54
|
+
return planPath;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Update a section in a plan file
|
|
58
|
+
*
|
|
59
|
+
* Replaces all content between the section heading and the next heading.
|
|
60
|
+
*
|
|
61
|
+
* @param planPath - Path to plan file
|
|
62
|
+
* @param section - Section name (without ##)
|
|
63
|
+
* @param content - New content for the section
|
|
64
|
+
* @returns True if changes were made, false if section not found
|
|
65
|
+
*/
|
|
66
|
+
export function updatePlanSection(planPath, section, content) {
|
|
67
|
+
if (!existsSync(planPath)) {
|
|
68
|
+
die(`Plan file not found: ${planPath}`);
|
|
69
|
+
}
|
|
70
|
+
const text = readFileSync(planPath, { encoding: 'utf-8' });
|
|
71
|
+
const lines = text.split('\n');
|
|
72
|
+
// Find the section heading
|
|
73
|
+
const sectionHeading = `## ${section}`;
|
|
74
|
+
const headingIndex = lines.findIndex((line) => line.trim() === sectionHeading);
|
|
75
|
+
if (headingIndex === -1) {
|
|
76
|
+
console.log(`${LOG_PREFIX} Section "${section}" not found in plan`);
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
// Find the next heading (end of this section)
|
|
80
|
+
let nextHeadingIndex = lines.findIndex((line, i) => i > headingIndex && line.trim().startsWith('## '));
|
|
81
|
+
if (nextHeadingIndex === -1) {
|
|
82
|
+
nextHeadingIndex = lines.length;
|
|
83
|
+
}
|
|
84
|
+
// Replace the section content (keep the heading, replace content until next heading)
|
|
85
|
+
const beforeSection = lines.slice(0, headingIndex + 1);
|
|
86
|
+
const afterSection = lines.slice(nextHeadingIndex);
|
|
87
|
+
const newLines = [...beforeSection, '', content, '', ...afterSection];
|
|
88
|
+
writeFileSync(planPath, newLines.join('\n'), { encoding: 'utf-8' });
|
|
89
|
+
console.log(`${LOG_PREFIX} Updated "${section}" section in plan`);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Append content to a section in a plan file
|
|
94
|
+
*
|
|
95
|
+
* Adds content at the end of the section, before the next heading.
|
|
96
|
+
*
|
|
97
|
+
* @param planPath - Path to plan file
|
|
98
|
+
* @param section - Section name (without ##)
|
|
99
|
+
* @param content - Content to append
|
|
100
|
+
* @returns True if changes were made, false if section not found
|
|
101
|
+
*/
|
|
102
|
+
export function appendToSection(planPath, section, content) {
|
|
103
|
+
if (!existsSync(planPath)) {
|
|
104
|
+
die(`Plan file not found: ${planPath}`);
|
|
105
|
+
}
|
|
106
|
+
const text = readFileSync(planPath, { encoding: 'utf-8' });
|
|
107
|
+
const lines = text.split('\n');
|
|
108
|
+
// Find the section heading
|
|
109
|
+
const sectionHeading = `## ${section}`;
|
|
110
|
+
const headingIndex = lines.findIndex((line) => line.trim() === sectionHeading);
|
|
111
|
+
if (headingIndex === -1) {
|
|
112
|
+
console.log(`${LOG_PREFIX} Section "${section}" not found in plan`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
// Find the next heading (end of this section)
|
|
116
|
+
let nextHeadingIndex = lines.findIndex((line, i) => i > headingIndex && line.trim().startsWith('## '));
|
|
117
|
+
if (nextHeadingIndex === -1) {
|
|
118
|
+
nextHeadingIndex = lines.length;
|
|
119
|
+
}
|
|
120
|
+
// Find the last non-empty line before the next heading
|
|
121
|
+
let insertIndex = nextHeadingIndex;
|
|
122
|
+
for (let i = nextHeadingIndex - 1; i > headingIndex; i--) {
|
|
123
|
+
if (lines[i].trim() !== '') {
|
|
124
|
+
insertIndex = i + 1;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Insert the new content
|
|
129
|
+
lines.splice(insertIndex, 0, content);
|
|
130
|
+
writeFileSync(planPath, lines.join('\n'), { encoding: 'utf-8' });
|
|
131
|
+
console.log(`${LOG_PREFIX} Appended to "${section}" section in plan`);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Generate commit message for plan edit operation
|
|
136
|
+
*
|
|
137
|
+
* @param id - WU or Initiative ID
|
|
138
|
+
* @param section - Section that was edited
|
|
139
|
+
* @returns Commit message
|
|
140
|
+
*/
|
|
141
|
+
export function getCommitMessage(id, section) {
|
|
142
|
+
const idLower = id.toLowerCase();
|
|
143
|
+
return `docs: update ${section} section in ${idLower} plan`;
|
|
144
|
+
}
|
|
145
|
+
async function main() {
|
|
146
|
+
const SECTION_OPTION = {
|
|
147
|
+
name: 'section',
|
|
148
|
+
flags: '--section <name>',
|
|
149
|
+
description: 'Section name to edit (Goal, Scope, Approach, etc.)',
|
|
150
|
+
};
|
|
151
|
+
const CONTENT_OPTION = {
|
|
152
|
+
name: 'content',
|
|
153
|
+
flags: '--content <text>',
|
|
154
|
+
description: 'New content for the section',
|
|
155
|
+
};
|
|
156
|
+
const APPEND_OPTION = {
|
|
157
|
+
name: 'append',
|
|
158
|
+
flags: '--append <text>',
|
|
159
|
+
description: 'Content to append to the section (instead of replace)',
|
|
160
|
+
};
|
|
161
|
+
const args = createWUParser({
|
|
162
|
+
name: 'plan-edit',
|
|
163
|
+
description: 'Edit a section in a plan file',
|
|
164
|
+
options: [WU_OPTIONS.id, SECTION_OPTION, CONTENT_OPTION, APPEND_OPTION],
|
|
165
|
+
required: ['id', 'section'],
|
|
166
|
+
allowPositionalId: true,
|
|
167
|
+
});
|
|
168
|
+
const id = args.id;
|
|
169
|
+
const section = args.section;
|
|
170
|
+
const content = args.content;
|
|
171
|
+
const appendContent = args.append;
|
|
172
|
+
// Validate we have either content or append
|
|
173
|
+
if (!content && !appendContent) {
|
|
174
|
+
die('Either --content or --append is required');
|
|
175
|
+
}
|
|
176
|
+
// Validate ID format
|
|
177
|
+
if (!WU_ID_PATTERN.test(id) && !INIT_ID_PATTERN.test(id)) {
|
|
178
|
+
die(`Invalid ID format: "${id}"\n\n` + `Expected format: WU-XXX or INIT-XXX`);
|
|
179
|
+
}
|
|
180
|
+
console.log(`${LOG_PREFIX} Editing plan for ${id}...`);
|
|
181
|
+
// Ensure on main for micro-worktree operations
|
|
182
|
+
await ensureOnMain(getGitForCwd());
|
|
183
|
+
try {
|
|
184
|
+
await withMicroWorktree({
|
|
185
|
+
operation: OPERATION_NAME,
|
|
186
|
+
id,
|
|
187
|
+
logPrefix: LOG_PREFIX,
|
|
188
|
+
pushOnly: true,
|
|
189
|
+
execute: async ({ worktreePath }) => {
|
|
190
|
+
const planRelPath = join(WU_PATHS.PLANS_DIR(), `${id}-plan.md`);
|
|
191
|
+
const planAbsPath = join(worktreePath, planRelPath);
|
|
192
|
+
if (!existsSync(planAbsPath)) {
|
|
193
|
+
die(`Plan not found for ${id}\n\n` +
|
|
194
|
+
`Expected path: ${planRelPath}\n\n` +
|
|
195
|
+
`Create it first with: pnpm plan:create --id ${id} --title "Title"`);
|
|
196
|
+
}
|
|
197
|
+
let changed;
|
|
198
|
+
if (appendContent) {
|
|
199
|
+
changed = appendToSection(planAbsPath, section, appendContent);
|
|
200
|
+
}
|
|
201
|
+
else if (content) {
|
|
202
|
+
changed = updatePlanSection(planAbsPath, section, content);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// This shouldn't happen due to earlier validation, but satisfy TS
|
|
206
|
+
changed = false;
|
|
207
|
+
}
|
|
208
|
+
if (!changed) {
|
|
209
|
+
console.warn(`${LOG_PREFIX} Section "${section}" not found - no changes made`);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
commitMessage: getCommitMessage(id, section),
|
|
213
|
+
files: [planRelPath],
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
console.log(`\n${LOG_PREFIX} Plan edited successfully!`);
|
|
218
|
+
console.log(`\nEdit Details:`);
|
|
219
|
+
console.log(` ID: ${id}`);
|
|
220
|
+
console.log(` Section: ${section}`);
|
|
221
|
+
console.log(` Mode: ${appendContent ? 'append' : 'replace'}`);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
die(`Plan edit failed: ${error.message}\n\n` +
|
|
225
|
+
`Micro-worktree cleanup was attempted automatically.\n` +
|
|
226
|
+
`If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Guard main() for testability
|
|
230
|
+
import { runCLI } from './cli-entry-point.js';
|
|
231
|
+
if (import.meta.main) {
|
|
232
|
+
void runCLI(main);
|
|
233
|
+
}
|
|
234
|
+
// Export for testing
|
|
235
|
+
export { main };
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
3
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
4
|
+
/**
|
|
5
|
+
* Plan Link Command (WU-1313)
|
|
6
|
+
*
|
|
7
|
+
* Links plan files to WUs (via spec_refs) or initiatives (via related_plan).
|
|
8
|
+
* This replaces the initiative:plan command for linking to initiatives.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* pnpm plan:link --id WU-1313 --plan lumenflow://plans/WU-1313-plan.md
|
|
12
|
+
* pnpm plan:link --id INIT-001 --plan lumenflow://plans/INIT-001-plan.md
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
* - Auto-detects target type (WU or Initiative) from ID format
|
|
16
|
+
* - Updates spec_refs for WUs, related_plan for initiatives
|
|
17
|
+
* - Validates plan file exists before linking
|
|
18
|
+
* - Uses micro-worktree isolation for atomic commits
|
|
19
|
+
* - Idempotent: no error if already linked
|
|
20
|
+
*
|
|
21
|
+
* Context: WU-1313 (INIT-013 Plan Tooling)
|
|
22
|
+
*/
|
|
23
|
+
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
24
|
+
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
25
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
28
|
+
import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
|
|
29
|
+
import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
|
|
30
|
+
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
31
|
+
import { INIT_PATHS } from '@lumenflow/initiatives/dist/initiative-paths.js';
|
|
32
|
+
import { parseYAML, stringifyYAML } from '@lumenflow/core/dist/wu-yaml.js';
|
|
33
|
+
import { LOG_PREFIX as CORE_LOG_PREFIX } from '@lumenflow/core/dist/wu-constants.js';
|
|
34
|
+
/** Log prefix for console output */
|
|
35
|
+
export const LOG_PREFIX = CORE_LOG_PREFIX.PLAN_LINK ?? '[plan:link]';
|
|
36
|
+
/** Micro-worktree operation name */
|
|
37
|
+
const OPERATION_NAME = 'plan-link';
|
|
38
|
+
/** LumenFlow URI scheme for plan references */
|
|
39
|
+
const PLAN_URI_SCHEME = 'lumenflow://plans/';
|
|
40
|
+
/** WU ID pattern */
|
|
41
|
+
const WU_ID_PATTERN = /^WU-\d+$/;
|
|
42
|
+
/** Initiative ID pattern */
|
|
43
|
+
const INIT_ID_PATTERN = /^INIT-[A-Z0-9]+$/i;
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the target type from an ID
|
|
46
|
+
*
|
|
47
|
+
* @param id - ID to check (WU-XXX or INIT-XXX)
|
|
48
|
+
* @returns 'wu' or 'initiative'
|
|
49
|
+
* @throws Error if ID format is invalid
|
|
50
|
+
*/
|
|
51
|
+
export function resolveTargetType(id) {
|
|
52
|
+
if (!id) {
|
|
53
|
+
die(`ID is required\n\nExpected format: WU-XXX or INIT-XXX`);
|
|
54
|
+
}
|
|
55
|
+
if (WU_ID_PATTERN.test(id)) {
|
|
56
|
+
return 'wu';
|
|
57
|
+
}
|
|
58
|
+
if (INIT_ID_PATTERN.test(id)) {
|
|
59
|
+
return 'initiative';
|
|
60
|
+
}
|
|
61
|
+
die(`Invalid ID format: "${id}"\n\n` +
|
|
62
|
+
`Expected format:\n` +
|
|
63
|
+
` - WU ID: WU-<number> (e.g., WU-1313)\n` +
|
|
64
|
+
` - Initiative ID: INIT-<alphanumeric> (e.g., INIT-001, INIT-TOOLING)`);
|
|
65
|
+
// TypeScript requires a return here even though die() never returns
|
|
66
|
+
return 'wu';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Validate that the plan file exists
|
|
70
|
+
*
|
|
71
|
+
* @param worktreePath - Path to repo root or worktree
|
|
72
|
+
* @param planUri - Plan URI (lumenflow://plans/...)
|
|
73
|
+
* @throws Error if plan file doesn't exist
|
|
74
|
+
*/
|
|
75
|
+
export function validatePlanExists(worktreePath, planUri) {
|
|
76
|
+
// Extract filename from URI
|
|
77
|
+
const filename = planUri.replace(PLAN_URI_SCHEME, '');
|
|
78
|
+
const plansDir = join(worktreePath, WU_PATHS.PLANS_DIR());
|
|
79
|
+
const planPath = join(plansDir, filename);
|
|
80
|
+
if (!existsSync(planPath)) {
|
|
81
|
+
die(`Plan file not found: ${planPath}\n\n` +
|
|
82
|
+
`Create it first with: pnpm plan:create --id <ID> --title "Title"`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Link a plan to a WU by updating spec_refs
|
|
87
|
+
*
|
|
88
|
+
* @param worktreePath - Path to repo root or worktree
|
|
89
|
+
* @param wuId - WU ID
|
|
90
|
+
* @param planUri - Plan URI
|
|
91
|
+
* @returns True if changes were made, false if already linked
|
|
92
|
+
*/
|
|
93
|
+
export function linkPlanToWU(worktreePath, wuId, planUri) {
|
|
94
|
+
const wuRelPath = WU_PATHS.WU(wuId);
|
|
95
|
+
const wuAbsPath = join(worktreePath, wuRelPath);
|
|
96
|
+
if (!existsSync(wuAbsPath)) {
|
|
97
|
+
die(`WU not found: ${wuId}\n\nFile does not exist: ${wuAbsPath}`);
|
|
98
|
+
}
|
|
99
|
+
// Read raw YAML to preserve all fields
|
|
100
|
+
const rawText = readFileSync(wuAbsPath, { encoding: 'utf-8' });
|
|
101
|
+
const doc = parseYAML(rawText);
|
|
102
|
+
// Check for existing spec_refs
|
|
103
|
+
let specRefs = doc.spec_refs;
|
|
104
|
+
if (!specRefs) {
|
|
105
|
+
specRefs = [];
|
|
106
|
+
}
|
|
107
|
+
// Check if already linked
|
|
108
|
+
if (specRefs.includes(planUri)) {
|
|
109
|
+
console.log(`${LOG_PREFIX} Plan already linked to ${wuId} (idempotent)`);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
// Add new plan URI
|
|
113
|
+
specRefs.push(planUri);
|
|
114
|
+
doc.spec_refs = specRefs;
|
|
115
|
+
// Write back
|
|
116
|
+
const out = stringifyYAML(doc, { lineWidth: -1 });
|
|
117
|
+
writeFileSync(wuAbsPath, out, { encoding: 'utf-8' });
|
|
118
|
+
console.log(`${LOG_PREFIX} Linked plan to ${wuId}: ${planUri}`);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Link a plan to an initiative by updating related_plan
|
|
123
|
+
*
|
|
124
|
+
* @param worktreePath - Path to repo root or worktree
|
|
125
|
+
* @param initId - Initiative ID
|
|
126
|
+
* @param planUri - Plan URI
|
|
127
|
+
* @returns True if changes were made, false if already linked
|
|
128
|
+
*/
|
|
129
|
+
export function linkPlanToInitiative(worktreePath, initId, planUri) {
|
|
130
|
+
const initRelPath = INIT_PATHS.INITIATIVE(initId);
|
|
131
|
+
const initAbsPath = join(worktreePath, initRelPath);
|
|
132
|
+
if (!existsSync(initAbsPath)) {
|
|
133
|
+
die(`Initiative not found: ${initId}\n\nFile does not exist: ${initAbsPath}`);
|
|
134
|
+
}
|
|
135
|
+
// Read raw YAML to preserve all fields
|
|
136
|
+
const rawText = readFileSync(initAbsPath, { encoding: 'utf-8' });
|
|
137
|
+
const doc = parseYAML(rawText);
|
|
138
|
+
// Check for existing related_plan
|
|
139
|
+
const existingPlan = doc.related_plan;
|
|
140
|
+
if (existingPlan === planUri) {
|
|
141
|
+
console.log(`${LOG_PREFIX} Plan already linked to ${initId} (idempotent)`);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if (existingPlan && existingPlan !== planUri) {
|
|
145
|
+
console.warn(`${LOG_PREFIX} Replacing existing related_plan: ${existingPlan} -> ${planUri}`);
|
|
146
|
+
}
|
|
147
|
+
// Update related_plan
|
|
148
|
+
doc.related_plan = planUri;
|
|
149
|
+
// Write back
|
|
150
|
+
const out = stringifyYAML(doc, { lineWidth: -1 });
|
|
151
|
+
writeFileSync(initAbsPath, out, { encoding: 'utf-8' });
|
|
152
|
+
console.log(`${LOG_PREFIX} Linked plan to ${initId}: ${planUri}`);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Generate commit message for plan link operation
|
|
157
|
+
*
|
|
158
|
+
* @param id - WU or Initiative ID
|
|
159
|
+
* @param planUri - Plan URI
|
|
160
|
+
* @returns Commit message
|
|
161
|
+
*/
|
|
162
|
+
export function getCommitMessage(id, planUri) {
|
|
163
|
+
const idLower = id.toLowerCase();
|
|
164
|
+
const filename = planUri.replace(PLAN_URI_SCHEME, '');
|
|
165
|
+
return `docs: link plan ${filename} to ${idLower}`;
|
|
166
|
+
}
|
|
167
|
+
async function main() {
|
|
168
|
+
const PLAN_OPTION = {
|
|
169
|
+
name: 'plan',
|
|
170
|
+
flags: '--plan <uri>',
|
|
171
|
+
description: 'Plan URI (lumenflow://plans/...)',
|
|
172
|
+
};
|
|
173
|
+
const args = createWUParser({
|
|
174
|
+
name: 'plan-link',
|
|
175
|
+
description: 'Link a plan file to a WU or initiative',
|
|
176
|
+
options: [WU_OPTIONS.id, PLAN_OPTION],
|
|
177
|
+
required: ['id', 'plan'],
|
|
178
|
+
allowPositionalId: true,
|
|
179
|
+
});
|
|
180
|
+
const id = args.id;
|
|
181
|
+
const planUri = args.plan;
|
|
182
|
+
// Resolve target type
|
|
183
|
+
const targetType = resolveTargetType(id);
|
|
184
|
+
console.log(`${LOG_PREFIX} Linking plan to ${targetType === 'wu' ? 'WU' : 'initiative'} ${id}...`);
|
|
185
|
+
// Ensure on main for micro-worktree operations
|
|
186
|
+
await ensureOnMain(getGitForCwd());
|
|
187
|
+
try {
|
|
188
|
+
await withMicroWorktree({
|
|
189
|
+
operation: OPERATION_NAME,
|
|
190
|
+
id,
|
|
191
|
+
logPrefix: LOG_PREFIX,
|
|
192
|
+
pushOnly: true,
|
|
193
|
+
execute: async ({ worktreePath }) => {
|
|
194
|
+
// Validate plan exists
|
|
195
|
+
validatePlanExists(worktreePath, planUri);
|
|
196
|
+
// Link plan based on target type
|
|
197
|
+
let changed;
|
|
198
|
+
let filePath;
|
|
199
|
+
if (targetType === 'wu') {
|
|
200
|
+
changed = linkPlanToWU(worktreePath, id, planUri);
|
|
201
|
+
filePath = WU_PATHS.WU(id);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
changed = linkPlanToInitiative(worktreePath, id, planUri);
|
|
205
|
+
filePath = INIT_PATHS.INITIATIVE(id);
|
|
206
|
+
}
|
|
207
|
+
if (!changed) {
|
|
208
|
+
console.log(`${LOG_PREFIX} No changes needed (already linked)`);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
commitMessage: getCommitMessage(id, planUri),
|
|
212
|
+
files: [filePath],
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
console.log(`\n${LOG_PREFIX} Plan linked successfully!`);
|
|
217
|
+
console.log(`\nLink Details:`);
|
|
218
|
+
console.log(` Target: ${id} (${targetType})`);
|
|
219
|
+
console.log(` Plan: ${planUri}`);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
die(`Plan linking failed: ${error.message}\n\n` +
|
|
223
|
+
`Micro-worktree cleanup was attempted automatically.\n` +
|
|
224
|
+
`If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Guard main() for testability
|
|
228
|
+
import { runCLI } from './cli-entry-point.js';
|
|
229
|
+
if (import.meta.main) {
|
|
230
|
+
void runCLI(main);
|
|
231
|
+
}
|
|
232
|
+
// Export for testing
|
|
233
|
+
export { main };
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
3
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
4
|
+
/**
|
|
5
|
+
* Plan Promote Command (WU-1313)
|
|
6
|
+
*
|
|
7
|
+
* Promotes a plan from draft to approved status.
|
|
8
|
+
* Validates that required sections are complete before approving.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* pnpm plan:promote --id WU-1313
|
|
12
|
+
* pnpm plan:promote --id INIT-001 --force # Skip validation
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
* - Validates plan completeness (non-empty required sections)
|
|
16
|
+
* - Adds approved status and timestamp
|
|
17
|
+
* - Uses micro-worktree isolation for atomic commits
|
|
18
|
+
* - Idempotent: no error if already approved
|
|
19
|
+
*
|
|
20
|
+
* Context: WU-1313 (INIT-013 Plan Tooling)
|
|
21
|
+
*/
|
|
22
|
+
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
23
|
+
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
24
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
27
|
+
import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
|
|
28
|
+
import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
|
|
29
|
+
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
30
|
+
import { todayISO } from '@lumenflow/core/dist/date-utils.js';
|
|
31
|
+
import { LOG_PREFIX as CORE_LOG_PREFIX } from '@lumenflow/core/dist/wu-constants.js';
|
|
32
|
+
/** Log prefix for console output */
|
|
33
|
+
export const LOG_PREFIX = CORE_LOG_PREFIX.PLAN_PROMOTE ?? '[plan:promote]';
|
|
34
|
+
/** Micro-worktree operation name */
|
|
35
|
+
const OPERATION_NAME = 'plan-promote';
|
|
36
|
+
/** WU ID pattern */
|
|
37
|
+
const WU_ID_PATTERN = /^WU-\d+$/;
|
|
38
|
+
/** Initiative ID pattern */
|
|
39
|
+
const INIT_ID_PATTERN = /^INIT-[A-Z0-9]+$/i;
|
|
40
|
+
/** Required sections that must have content */
|
|
41
|
+
const REQUIRED_SECTIONS = ['Goal', 'Scope', 'Approach'];
|
|
42
|
+
/** Status marker patterns */
|
|
43
|
+
const STATUS_APPROVED_PATTERN = /^Status:\s*approved/im;
|
|
44
|
+
/**
|
|
45
|
+
* Get the path to a plan file from its ID
|
|
46
|
+
*
|
|
47
|
+
* @param id - WU or Initiative ID
|
|
48
|
+
* @returns Path to plan file
|
|
49
|
+
* @throws Error if plan not found
|
|
50
|
+
*/
|
|
51
|
+
export function getPlanPath(id) {
|
|
52
|
+
const plansDir = WU_PATHS.PLANS_DIR();
|
|
53
|
+
const planPath = join(plansDir, `${id}-plan.md`);
|
|
54
|
+
if (!existsSync(planPath)) {
|
|
55
|
+
die(`Plan not found for ${id}\n\n` +
|
|
56
|
+
`Expected path: ${planPath}\n\n` +
|
|
57
|
+
`Create it first with: pnpm plan:create --id ${id} --title "Title"`);
|
|
58
|
+
}
|
|
59
|
+
return planPath;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Validate that a plan has all required sections with content
|
|
63
|
+
*
|
|
64
|
+
* @param planPath - Path to plan file
|
|
65
|
+
* @returns Validation result with valid flag and errors array
|
|
66
|
+
*/
|
|
67
|
+
export function validatePlanComplete(planPath) {
|
|
68
|
+
if (!existsSync(planPath)) {
|
|
69
|
+
return { valid: false, errors: ['Plan file not found'] };
|
|
70
|
+
}
|
|
71
|
+
const text = readFileSync(planPath, { encoding: 'utf-8' });
|
|
72
|
+
const errors = [];
|
|
73
|
+
for (const section of REQUIRED_SECTIONS) {
|
|
74
|
+
const sectionHeading = `## ${section}`;
|
|
75
|
+
const headingIndex = text.indexOf(sectionHeading);
|
|
76
|
+
if (headingIndex === -1) {
|
|
77
|
+
errors.push(`Missing required section: ${section}`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// Find the content between this heading and the next
|
|
81
|
+
const afterHeading = text.substring(headingIndex + sectionHeading.length);
|
|
82
|
+
const nextHeadingIndex = afterHeading.indexOf('\n## ');
|
|
83
|
+
const sectionContent = nextHeadingIndex >= 0 ? afterHeading.substring(0, nextHeadingIndex) : afterHeading;
|
|
84
|
+
// Check if content is just whitespace, empty, or only comments
|
|
85
|
+
const trimmedContent = sectionContent
|
|
86
|
+
.split('\n')
|
|
87
|
+
.filter((line) => !line.trim().startsWith('<!--') && line.trim() !== '-->')
|
|
88
|
+
.join('\n')
|
|
89
|
+
.trim();
|
|
90
|
+
if (trimmedContent === '' || trimmedContent.length < 10) {
|
|
91
|
+
errors.push(`${section} section is empty or too short`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
valid: errors.length === 0,
|
|
96
|
+
errors,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Promote a plan to approved status
|
|
101
|
+
*
|
|
102
|
+
* Adds Status: approved and Approved: date after the Created line.
|
|
103
|
+
*
|
|
104
|
+
* @param planPath - Path to plan file
|
|
105
|
+
* @returns True if changes were made, false if already approved
|
|
106
|
+
*/
|
|
107
|
+
export function promotePlan(planPath) {
|
|
108
|
+
if (!existsSync(planPath)) {
|
|
109
|
+
die(`Plan file not found: ${planPath}`);
|
|
110
|
+
}
|
|
111
|
+
const text = readFileSync(planPath, { encoding: 'utf-8' });
|
|
112
|
+
// Check if already approved
|
|
113
|
+
if (STATUS_APPROVED_PATTERN.test(text)) {
|
|
114
|
+
console.log(`${LOG_PREFIX} Plan already approved (idempotent)`);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
// Find the Created: line and insert status after it
|
|
118
|
+
// Use specific pattern to avoid backtracking (sonarjs/slow-regex)
|
|
119
|
+
const createdPattern = /^Created:\s*\S.*$/m;
|
|
120
|
+
const createdMatch = createdPattern.exec(text);
|
|
121
|
+
const today = todayISO();
|
|
122
|
+
const statusLines = `Status: approved\nApproved: ${today}`;
|
|
123
|
+
let newText;
|
|
124
|
+
if (createdMatch && createdMatch.index !== undefined) {
|
|
125
|
+
// Insert after Created: line
|
|
126
|
+
const insertPos = createdMatch.index + createdMatch[0].length;
|
|
127
|
+
newText = text.substring(0, insertPos) + '\n' + statusLines + text.substring(insertPos);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// No Created: line found, insert after first heading
|
|
131
|
+
const firstHeadingPattern = /^# .+$/m;
|
|
132
|
+
const firstHeadingMatch = firstHeadingPattern.exec(text);
|
|
133
|
+
if (firstHeadingMatch && firstHeadingMatch.index !== undefined) {
|
|
134
|
+
const insertPos = firstHeadingMatch.index + firstHeadingMatch[0].length;
|
|
135
|
+
newText =
|
|
136
|
+
text.substring(0, insertPos) + '\n\n' + statusLines + '\n' + text.substring(insertPos);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// Fallback: prepend to file
|
|
140
|
+
newText = statusLines + '\n\n' + text;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
writeFileSync(planPath, newText, { encoding: 'utf-8' });
|
|
144
|
+
console.log(`${LOG_PREFIX} Plan promoted to approved status`);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Generate commit message for plan promote operation
|
|
149
|
+
*
|
|
150
|
+
* @param id - WU or Initiative ID
|
|
151
|
+
* @returns Commit message
|
|
152
|
+
*/
|
|
153
|
+
export function getCommitMessage(id) {
|
|
154
|
+
const idLower = id.toLowerCase();
|
|
155
|
+
return `docs: promote ${idLower} plan to approved`;
|
|
156
|
+
}
|
|
157
|
+
async function main() {
|
|
158
|
+
const FORCE_OPTION = {
|
|
159
|
+
name: 'force',
|
|
160
|
+
flags: '-f, --force',
|
|
161
|
+
description: 'Skip validation and promote anyway',
|
|
162
|
+
};
|
|
163
|
+
const args = createWUParser({
|
|
164
|
+
name: 'plan-promote',
|
|
165
|
+
description: 'Promote a plan to approved status',
|
|
166
|
+
options: [WU_OPTIONS.id, FORCE_OPTION],
|
|
167
|
+
required: ['id'],
|
|
168
|
+
allowPositionalId: true,
|
|
169
|
+
});
|
|
170
|
+
const id = args.id;
|
|
171
|
+
const force = args.force;
|
|
172
|
+
// Validate ID format
|
|
173
|
+
if (!WU_ID_PATTERN.test(id) && !INIT_ID_PATTERN.test(id)) {
|
|
174
|
+
die(`Invalid ID format: "${id}"\n\n` + `Expected format: WU-XXX or INIT-XXX`);
|
|
175
|
+
}
|
|
176
|
+
console.log(`${LOG_PREFIX} Promoting plan for ${id}...`);
|
|
177
|
+
// Ensure on main for micro-worktree operations
|
|
178
|
+
await ensureOnMain(getGitForCwd());
|
|
179
|
+
try {
|
|
180
|
+
await withMicroWorktree({
|
|
181
|
+
operation: OPERATION_NAME,
|
|
182
|
+
id,
|
|
183
|
+
logPrefix: LOG_PREFIX,
|
|
184
|
+
pushOnly: true,
|
|
185
|
+
execute: async ({ worktreePath }) => {
|
|
186
|
+
const planRelPath = join(WU_PATHS.PLANS_DIR(), `${id}-plan.md`);
|
|
187
|
+
const planAbsPath = join(worktreePath, planRelPath);
|
|
188
|
+
if (!existsSync(planAbsPath)) {
|
|
189
|
+
die(`Plan not found for ${id}\n\n` +
|
|
190
|
+
`Expected path: ${planRelPath}\n\n` +
|
|
191
|
+
`Create it first with: pnpm plan:create --id ${id} --title "Title"`);
|
|
192
|
+
}
|
|
193
|
+
// Validate plan completeness (unless force)
|
|
194
|
+
if (!force) {
|
|
195
|
+
const validation = validatePlanComplete(planAbsPath);
|
|
196
|
+
if (!validation.valid) {
|
|
197
|
+
const errorList = validation.errors.map((e) => ` - ${e}`).join('\n');
|
|
198
|
+
die(`Plan validation failed:\n\n${errorList}\n\n` +
|
|
199
|
+
`Fix these issues or use --force to skip validation.`);
|
|
200
|
+
}
|
|
201
|
+
console.log(`${LOG_PREFIX} Plan validation passed`);
|
|
202
|
+
}
|
|
203
|
+
// Promote the plan
|
|
204
|
+
const changed = promotePlan(planAbsPath);
|
|
205
|
+
if (!changed) {
|
|
206
|
+
console.log(`${LOG_PREFIX} No changes needed (already approved)`);
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
commitMessage: getCommitMessage(id),
|
|
210
|
+
files: [planRelPath],
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
console.log(`\n${LOG_PREFIX} Plan promoted successfully!`);
|
|
215
|
+
console.log(`\nPromotion Details:`);
|
|
216
|
+
console.log(` ID: ${id}`);
|
|
217
|
+
console.log(` Status: approved`);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
die(`Plan promotion failed: ${error.message}\n\n` +
|
|
221
|
+
`Micro-worktree cleanup was attempted automatically.\n` +
|
|
222
|
+
`If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Guard main() for testability
|
|
226
|
+
import { runCLI } from './cli-entry-point.js';
|
|
227
|
+
if (import.meta.main) {
|
|
228
|
+
void runCLI(main);
|
|
229
|
+
}
|
|
230
|
+
// Export for testing
|
|
231
|
+
export { main };
|