@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
3
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
3
4
|
/**
|
|
4
5
|
* Initiative Add WU Command (WU-1389)
|
|
5
6
|
*
|
|
@@ -30,13 +31,118 @@ import { INIT_PATTERNS, INIT_COMMIT_FORMATS, INIT_LOG_PREFIX, } from '@lumenflow
|
|
|
30
31
|
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
31
32
|
import { PATTERNS } from '@lumenflow/core/dist/wu-constants.js';
|
|
32
33
|
import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
|
|
33
|
-
import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
|
|
34
|
+
import { withMicroWorktree, isRetryExhaustionError as coreIsRetryExhaustionError, formatRetryExhaustionError as coreFormatRetryExhaustionError, } from '@lumenflow/core/dist/micro-worktree.js';
|
|
34
35
|
import { readWU, writeWU } from '@lumenflow/core/dist/wu-yaml.js';
|
|
35
36
|
import { readInitiative, writeInitiative } from '@lumenflow/initiatives/dist/initiative-yaml.js';
|
|
37
|
+
import { validateSingleWU } from '@lumenflow/core/dist/validators/wu-tasks.js';
|
|
36
38
|
/** Log prefix for console output */
|
|
37
39
|
const LOG_PREFIX = INIT_LOG_PREFIX.ADD_WU;
|
|
38
40
|
/** Micro-worktree operation name */
|
|
39
41
|
const OPERATION_NAME = 'initiative-add-wu';
|
|
42
|
+
/**
|
|
43
|
+
* Validate WU spec for linking to an initiative (WU-1330)
|
|
44
|
+
*
|
|
45
|
+
* Performs validation including:
|
|
46
|
+
* - Schema validation (required fields, formats) - BLOCKING
|
|
47
|
+
* - Placeholder detection ([PLACEHOLDER] markers) - BLOCKING
|
|
48
|
+
* - Minimum description length (50 chars) - BLOCKING
|
|
49
|
+
* - Acceptance criteria present - BLOCKING
|
|
50
|
+
* - Completeness warnings (notes, spec_refs) - NON-BLOCKING
|
|
51
|
+
*
|
|
52
|
+
* Uses non-strict mode: schema errors block, completeness warnings don't.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} wuId - WU ID to validate
|
|
55
|
+
* @returns {WULinkValidationResult} Validation result with errors and warnings
|
|
56
|
+
*/
|
|
57
|
+
export function validateWUForLinking(wuId) {
|
|
58
|
+
const wuPath = WU_PATHS.WU(wuId);
|
|
59
|
+
// Check if file exists first
|
|
60
|
+
if (!existsSync(wuPath)) {
|
|
61
|
+
return {
|
|
62
|
+
valid: false,
|
|
63
|
+
errors: [`WU file not found: ${wuPath}`],
|
|
64
|
+
warnings: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Use the core validator with non-strict mode
|
|
68
|
+
// Schema errors (required fields, format, placeholders) -> block
|
|
69
|
+
// Completeness warnings (notes, spec_refs) -> don't block
|
|
70
|
+
const result = validateSingleWU(wuPath, { strict: false });
|
|
71
|
+
return {
|
|
72
|
+
valid: result.valid,
|
|
73
|
+
errors: result.errors,
|
|
74
|
+
warnings: result.warnings,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* WU-1333/WU-1336: Check if an error is a retry exhaustion error
|
|
79
|
+
*
|
|
80
|
+
* Detects when micro-worktree push retries have been exhausted.
|
|
81
|
+
* Delegates to the shared helper from @lumenflow/core.
|
|
82
|
+
*
|
|
83
|
+
* @param {Error} error - Error to check
|
|
84
|
+
* @returns {boolean} True if this is a retry exhaustion error
|
|
85
|
+
*/
|
|
86
|
+
export function isRetryExhaustionError(error) {
|
|
87
|
+
return coreIsRetryExhaustionError(error);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* WU-1333/WU-1336: Format retry exhaustion error with actionable next steps
|
|
91
|
+
*
|
|
92
|
+
* When push retries are exhausted, provides clear guidance on how to proceed.
|
|
93
|
+
* Delegates to the shared helper from @lumenflow/core with command-specific options.
|
|
94
|
+
*
|
|
95
|
+
* @param {Error} error - The retry exhaustion error
|
|
96
|
+
* @param {string} wuId - WU ID being linked
|
|
97
|
+
* @param {string} initId - Initiative ID being linked to
|
|
98
|
+
* @returns {string} Formatted error message with next steps
|
|
99
|
+
*/
|
|
100
|
+
export function formatRetryExhaustionError(error, wuId, initId) {
|
|
101
|
+
return coreFormatRetryExhaustionError(error, {
|
|
102
|
+
command: `pnpm initiative:add-wu --wu ${wuId} --initiative ${initId}`,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Format validation errors for display to user (WU-1330)
|
|
107
|
+
*
|
|
108
|
+
* Creates a human-readable error message with all validation issues.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} wuId - WU ID that failed validation
|
|
111
|
+
* @param {string[]} errors - Array of error messages
|
|
112
|
+
* @returns {string} Formatted error message
|
|
113
|
+
*/
|
|
114
|
+
export function formatValidationErrors(wuId, errors) {
|
|
115
|
+
const errorList = errors.map((e) => ` - ${e}`).join('\n');
|
|
116
|
+
return (`WU ${wuId} failed validation:\n\n${errorList}\n\n` +
|
|
117
|
+
`Fix the WU spec before linking to an initiative:\n` +
|
|
118
|
+
` pnpm wu:edit --id ${wuId} ...\n\n` +
|
|
119
|
+
`Or validate the WU:\n` +
|
|
120
|
+
` pnpm wu:validate --id ${wuId}`);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check if WU exists and is valid for linking (WU-1330)
|
|
124
|
+
*
|
|
125
|
+
* Combines existence check with strict validation.
|
|
126
|
+
* Throws with aggregated errors if validation fails.
|
|
127
|
+
*
|
|
128
|
+
* @param {string} wuId - WU ID to check and validate
|
|
129
|
+
* @returns {object} WU document if validation passes
|
|
130
|
+
* @throws {Error} If WU doesn't exist or validation fails
|
|
131
|
+
*/
|
|
132
|
+
export function checkWUExistsAndValidate(wuId) {
|
|
133
|
+
const wuPath = WU_PATHS.WU(wuId);
|
|
134
|
+
// Check existence
|
|
135
|
+
if (!existsSync(wuPath)) {
|
|
136
|
+
die(`WU not found: ${wuId}\n\nFile does not exist: ${wuPath}`);
|
|
137
|
+
}
|
|
138
|
+
// Validate WU
|
|
139
|
+
const validation = validateWUForLinking(wuId);
|
|
140
|
+
if (!validation.valid) {
|
|
141
|
+
die(formatValidationErrors(wuId, validation.errors));
|
|
142
|
+
}
|
|
143
|
+
// Return the document if validation passes
|
|
144
|
+
return readWU(wuPath, wuId);
|
|
145
|
+
}
|
|
40
146
|
/**
|
|
41
147
|
* Validate Initiative ID format
|
|
42
148
|
* @param {string} id - Initiative ID to validate
|
|
@@ -56,18 +162,6 @@ function validateWuIdFormat(id) {
|
|
|
56
162
|
die(`Invalid WU ID format: "${id}"\n\nExpected format: WU-<number> (e.g., WU-123)`);
|
|
57
163
|
}
|
|
58
164
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Check if WU exists
|
|
61
|
-
* @param {string} wuId - WU ID to check
|
|
62
|
-
* @returns {object} WU document
|
|
63
|
-
*/
|
|
64
|
-
function checkWUExists(wuId) {
|
|
65
|
-
const wuPath = WU_PATHS.WU(wuId);
|
|
66
|
-
if (!existsSync(wuPath)) {
|
|
67
|
-
die(`WU not found: ${wuId}\n\nFile does not exist: ${wuPath}`);
|
|
68
|
-
}
|
|
69
|
-
return readWU(wuPath, wuId);
|
|
70
|
-
}
|
|
71
165
|
/**
|
|
72
166
|
* Check if Initiative exists
|
|
73
167
|
* @param {string} initId - Initiative ID to check
|
|
@@ -164,10 +258,12 @@ async function main() {
|
|
|
164
258
|
const wuId = args.wu;
|
|
165
259
|
const initId = args.initiative;
|
|
166
260
|
console.log(`${LOG_PREFIX} Linking ${wuId} to ${initId}...`);
|
|
167
|
-
// Pre-flight validation
|
|
261
|
+
// Pre-flight validation: ID formats
|
|
168
262
|
validateInitIdFormat(initId);
|
|
169
263
|
validateWuIdFormat(wuId);
|
|
170
|
-
|
|
264
|
+
// WU-1330: Validate WU spec before linking
|
|
265
|
+
// This ensures only valid, complete WUs can be linked to initiatives
|
|
266
|
+
const wuDoc = checkWUExistsAndValidate(wuId);
|
|
171
267
|
const initDoc = checkInitiativeExists(initId);
|
|
172
268
|
// Check for conflicting links
|
|
173
269
|
checkConflictingLink(wuDoc, initId);
|
|
@@ -232,5 +328,5 @@ async function main() {
|
|
|
232
328
|
// path but import.meta.url resolves to the real path - they never match
|
|
233
329
|
import { runCLI } from './cli-entry-point.js';
|
|
234
330
|
if (import.meta.main) {
|
|
235
|
-
runCLI(main);
|
|
331
|
+
void runCLI(main);
|
|
236
332
|
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
3
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
4
|
+
/**
|
|
5
|
+
* Initiative Remove WU Command (WU-1328)
|
|
6
|
+
*
|
|
7
|
+
* Unlinks a WU from an initiative bidirectionally:
|
|
8
|
+
* 1. Removes `initiative: INIT-NNN` field from WU YAML
|
|
9
|
+
* 2. Removes WU ID from initiative `wus: []` array
|
|
10
|
+
*
|
|
11
|
+
* Uses micro-worktree isolation for atomic operations.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* pnpm initiative:remove-wu --initiative INIT-001 --wu WU-123
|
|
15
|
+
*
|
|
16
|
+
* Features:
|
|
17
|
+
* - Validates both WU and initiative exist before modifying
|
|
18
|
+
* - Idempotent: no error if link does not exist
|
|
19
|
+
* - Handles partial state gracefully (WU has initiative but not in initiative's wus list)
|
|
20
|
+
* - Atomic: both files updated in single commit
|
|
21
|
+
*
|
|
22
|
+
* Context: WU-1328 (initial implementation)
|
|
23
|
+
*/
|
|
24
|
+
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
25
|
+
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
26
|
+
import { existsSync } from 'node:fs';
|
|
27
|
+
import { join } from 'node:path';
|
|
28
|
+
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
29
|
+
import { INIT_PATHS } from '@lumenflow/initiatives/dist/initiative-paths.js';
|
|
30
|
+
import { INIT_PATTERNS, INIT_COMMIT_FORMATS, INIT_LOG_PREFIX, } from '@lumenflow/initiatives/dist/initiative-constants.js';
|
|
31
|
+
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
32
|
+
import { PATTERNS } from '@lumenflow/core/dist/wu-constants.js';
|
|
33
|
+
import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
|
|
34
|
+
import { withMicroWorktree, isRetryExhaustionError as coreIsRetryExhaustionError, formatRetryExhaustionError as coreFormatRetryExhaustionError, } from '@lumenflow/core/dist/micro-worktree.js';
|
|
35
|
+
import { readWU, writeWU } from '@lumenflow/core/dist/wu-yaml.js';
|
|
36
|
+
import { readInitiative, writeInitiative } from '@lumenflow/initiatives/dist/initiative-yaml.js';
|
|
37
|
+
/** Log prefix for console output */
|
|
38
|
+
export const LOG_PREFIX = INIT_LOG_PREFIX.REMOVE_WU;
|
|
39
|
+
/** Micro-worktree operation name */
|
|
40
|
+
export const OPERATION_NAME = 'initiative-remove-wu';
|
|
41
|
+
/**
|
|
42
|
+
* WU-1333/WU-1336: Check if an error is a retry exhaustion error
|
|
43
|
+
*
|
|
44
|
+
* Detects when micro-worktree push retries have been exhausted.
|
|
45
|
+
* Delegates to the shared helper from @lumenflow/core.
|
|
46
|
+
*
|
|
47
|
+
* @param {Error} error - Error to check
|
|
48
|
+
* @returns {boolean} True if this is a retry exhaustion error
|
|
49
|
+
*/
|
|
50
|
+
export function isRetryExhaustionError(error) {
|
|
51
|
+
return coreIsRetryExhaustionError(error);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* WU-1333/WU-1336: Format retry exhaustion error with actionable next steps
|
|
55
|
+
*
|
|
56
|
+
* When push retries are exhausted, provides clear guidance on how to proceed.
|
|
57
|
+
* Delegates to the shared helper from @lumenflow/core with command-specific options.
|
|
58
|
+
*
|
|
59
|
+
* @param {Error} error - The retry exhaustion error
|
|
60
|
+
* @param {string} wuId - WU ID being unlinked
|
|
61
|
+
* @param {string} initId - Initiative ID being unlinked from
|
|
62
|
+
* @returns {string} Formatted error message with next steps
|
|
63
|
+
*/
|
|
64
|
+
export function formatRetryExhaustionError(error, wuId, initId) {
|
|
65
|
+
return coreFormatRetryExhaustionError(error, {
|
|
66
|
+
command: `pnpm initiative:remove-wu --wu ${wuId} --initiative ${initId}`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Validate Initiative ID format
|
|
71
|
+
* @param {string} id - Initiative ID to validate
|
|
72
|
+
*/
|
|
73
|
+
export function validateInitIdFormat(id) {
|
|
74
|
+
if (!INIT_PATTERNS.INIT_ID.test(id)) {
|
|
75
|
+
die(`Invalid Initiative ID format: "${id}"\n\n` +
|
|
76
|
+
`Expected format: INIT-<number> or INIT-<NAME> (e.g., INIT-001, INIT-TOOLING)`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Validate WU ID format
|
|
81
|
+
* @param {string} id - WU ID to validate
|
|
82
|
+
*/
|
|
83
|
+
export function validateWuIdFormat(id) {
|
|
84
|
+
if (!PATTERNS.WU_ID.test(id)) {
|
|
85
|
+
die(`Invalid WU ID format: "${id}"\n\nExpected format: WU-<number> (e.g., WU-123)`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Check if WU exists
|
|
90
|
+
* @param {string} wuId - WU ID to check
|
|
91
|
+
* @returns {object} WU document
|
|
92
|
+
*/
|
|
93
|
+
export function checkWUExists(wuId) {
|
|
94
|
+
const wuPath = WU_PATHS.WU(wuId);
|
|
95
|
+
if (!existsSync(wuPath)) {
|
|
96
|
+
die(`WU not found: ${wuId}\n\nFile does not exist: ${wuPath}`);
|
|
97
|
+
}
|
|
98
|
+
return readWU(wuPath, wuId);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Check if Initiative exists
|
|
102
|
+
* @param {string} initId - Initiative ID to check
|
|
103
|
+
* @returns {object} Initiative document
|
|
104
|
+
*/
|
|
105
|
+
export function checkInitiativeExists(initId) {
|
|
106
|
+
const initPath = INIT_PATHS.INITIATIVE(initId);
|
|
107
|
+
if (!existsSync(initPath)) {
|
|
108
|
+
die(`Initiative not found: ${initId}\n\nFile does not exist: ${initPath}`);
|
|
109
|
+
}
|
|
110
|
+
return readInitiative(initPath, initId);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Check if WU is currently linked to the initiative
|
|
114
|
+
* @param {object} wuDoc - WU document
|
|
115
|
+
* @param {object} initDoc - Initiative document
|
|
116
|
+
* @param {string} wuId - WU ID
|
|
117
|
+
* @param {string} initId - Initiative ID
|
|
118
|
+
* @returns {boolean} True if WU is linked to the initiative (both sides)
|
|
119
|
+
*/
|
|
120
|
+
export function checkWUIsLinked(wuDoc, initDoc, wuId, initId) {
|
|
121
|
+
const wuHasInit = wuDoc.initiative === initId;
|
|
122
|
+
const initHasWu = Array.isArray(initDoc.wus) && initDoc.wus.includes(wuId);
|
|
123
|
+
return wuHasInit && initHasWu;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Update WU YAML in micro-worktree (remove initiative field)
|
|
127
|
+
* @param {string} worktreePath - Path to micro-worktree
|
|
128
|
+
* @param {string} wuId - WU ID
|
|
129
|
+
* @param {string} initId - Initiative ID to remove
|
|
130
|
+
* @returns {boolean} True if changes were made
|
|
131
|
+
*/
|
|
132
|
+
export function updateWUInWorktree(worktreePath, wuId, initId) {
|
|
133
|
+
const wuRelPath = WU_PATHS.WU(wuId);
|
|
134
|
+
const wuAbsPath = join(worktreePath, wuRelPath);
|
|
135
|
+
const doc = readWU(wuAbsPath, wuId);
|
|
136
|
+
// Skip if no initiative field or different initiative (idempotent)
|
|
137
|
+
if (!doc.initiative || doc.initiative !== initId) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
// Remove initiative field
|
|
141
|
+
delete doc.initiative;
|
|
142
|
+
writeWU(wuAbsPath, doc);
|
|
143
|
+
console.log(`${LOG_PREFIX} Removed initiative: ${initId} from ${wuId}`);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Update Initiative YAML in micro-worktree (remove WU from wus list)
|
|
148
|
+
* @param {string} worktreePath - Path to micro-worktree
|
|
149
|
+
* @param {string} initId - Initiative ID
|
|
150
|
+
* @param {string} wuId - WU ID to remove
|
|
151
|
+
* @returns {boolean} True if changes were made
|
|
152
|
+
*/
|
|
153
|
+
export function updateInitiativeInWorktree(worktreePath, initId, wuId) {
|
|
154
|
+
const initRelPath = INIT_PATHS.INITIATIVE(initId);
|
|
155
|
+
const initAbsPath = join(worktreePath, initRelPath);
|
|
156
|
+
const doc = readInitiative(initAbsPath, initId);
|
|
157
|
+
// Skip if no wus array or WU not in list (idempotent)
|
|
158
|
+
if (!Array.isArray(doc.wus) || !doc.wus.includes(wuId)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
// Remove WU from list
|
|
162
|
+
doc.wus = doc.wus.filter((id) => id !== wuId);
|
|
163
|
+
writeInitiative(initAbsPath, doc);
|
|
164
|
+
console.log(`${LOG_PREFIX} Removed ${wuId} from ${initId} wus list`);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
export async function main() {
|
|
168
|
+
const args = createWUParser({
|
|
169
|
+
name: 'initiative-remove-wu',
|
|
170
|
+
description: 'Unlink a WU from an initiative bidirectionally',
|
|
171
|
+
options: [WU_OPTIONS.initiative, WU_OPTIONS.wu],
|
|
172
|
+
required: ['initiative', 'wu'],
|
|
173
|
+
allowPositionalId: false,
|
|
174
|
+
});
|
|
175
|
+
// Normalize args
|
|
176
|
+
const wuId = args.wu;
|
|
177
|
+
const initId = args.initiative;
|
|
178
|
+
console.log(`${LOG_PREFIX} Unlinking ${wuId} from ${initId}...`);
|
|
179
|
+
// Pre-flight validation
|
|
180
|
+
validateInitIdFormat(initId);
|
|
181
|
+
validateWuIdFormat(wuId);
|
|
182
|
+
const wuDoc = checkWUExists(wuId);
|
|
183
|
+
const initDoc = checkInitiativeExists(initId);
|
|
184
|
+
// Check if link exists - if not, report success (idempotent)
|
|
185
|
+
const wuHasInit = wuDoc.initiative === initId;
|
|
186
|
+
const initHasWu = Array.isArray(initDoc.wus) && initDoc.wus.includes(wuId);
|
|
187
|
+
if (!wuHasInit && !initHasWu) {
|
|
188
|
+
console.log(`${LOG_PREFIX} Link does not exist (idempotent - no changes needed)`);
|
|
189
|
+
console.log(`\n${LOG_PREFIX} ${wuId} is not linked to ${initId}`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
// Ensure on main branch
|
|
193
|
+
await ensureOnMain(getGitForCwd());
|
|
194
|
+
// Transaction: micro-worktree isolation
|
|
195
|
+
try {
|
|
196
|
+
await withMicroWorktree({
|
|
197
|
+
operation: OPERATION_NAME,
|
|
198
|
+
id: `${wuId}-${initId}`.toLowerCase(),
|
|
199
|
+
logPrefix: LOG_PREFIX,
|
|
200
|
+
pushOnly: true,
|
|
201
|
+
execute: async ({ worktreePath }) => {
|
|
202
|
+
const files = [];
|
|
203
|
+
// Update WU YAML (remove initiative field)
|
|
204
|
+
const wuChanged = updateWUInWorktree(worktreePath, wuId, initId);
|
|
205
|
+
if (wuChanged) {
|
|
206
|
+
files.push(WU_PATHS.WU(wuId));
|
|
207
|
+
}
|
|
208
|
+
// Update Initiative YAML (remove WU from wus list)
|
|
209
|
+
const initChanged = updateInitiativeInWorktree(worktreePath, initId, wuId);
|
|
210
|
+
if (initChanged) {
|
|
211
|
+
files.push(INIT_PATHS.INITIATIVE(initId));
|
|
212
|
+
}
|
|
213
|
+
// If no changes, this is idempotent (race condition handling)
|
|
214
|
+
if (files.length === 0) {
|
|
215
|
+
console.log(`${LOG_PREFIX} No changes detected (concurrent unlink operation)`);
|
|
216
|
+
// Still need to return something for the commit
|
|
217
|
+
return {
|
|
218
|
+
commitMessage: INIT_COMMIT_FORMATS.UNLINK_WU(wuId, initId),
|
|
219
|
+
files: [WU_PATHS.WU(wuId), INIT_PATHS.INITIATIVE(initId)],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
commitMessage: INIT_COMMIT_FORMATS.UNLINK_WU(wuId, initId),
|
|
224
|
+
files,
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
console.log(`\n${LOG_PREFIX} Transaction complete!`);
|
|
229
|
+
console.log(`\nLink Removed:`);
|
|
230
|
+
console.log(` WU: ${wuId}`);
|
|
231
|
+
console.log(` Initiative: ${initId}`);
|
|
232
|
+
console.log(`\nNext steps:`);
|
|
233
|
+
console.log(` - View initiative status: pnpm initiative:status ${initId}`);
|
|
234
|
+
console.log(` - View WU: cat ${WU_PATHS.WU(wuId)}`);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
die(`Transaction failed: ${error.message}\n\n` +
|
|
238
|
+
`Micro-worktree cleanup was attempted automatically.\n` +
|
|
239
|
+
`If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
// WU-1181: Use import.meta.main instead of process.argv[1] comparison
|
|
243
|
+
// The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
|
|
244
|
+
// path but import.meta.url resolves to the real path - they never match
|
|
245
|
+
import { runCLI } from './cli-entry-point.js';
|
|
246
|
+
if (import.meta.main) {
|
|
247
|
+
void runCLI(main);
|
|
248
|
+
}
|