@lumenflow/cli 3.1.3 → 3.2.1
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/agent-issues-query.js.map +1 -0
- package/dist/agent-log-issue.js.map +1 -0
- package/dist/agent-session-end.js.map +1 -0
- package/dist/agent-session.js.map +1 -0
- package/dist/backlog-prune.js.map +1 -0
- package/dist/cli-entry-point.js +139 -0
- package/dist/cli-entry-point.js.map +1 -0
- package/dist/commands/integrate.js.map +1 -0
- package/dist/commands.js.map +1 -0
- package/dist/config-get.js.map +1 -0
- package/dist/config-set.js.map +1 -0
- package/dist/constants.js +98 -0
- package/dist/constants.js.map +1 -0
- package/dist/delegation-list.js.map +1 -0
- package/dist/deps-add.js +259 -0
- package/dist/deps-add.js.map +1 -0
- package/dist/deps-remove.js +105 -0
- package/dist/deps-remove.js.map +1 -0
- package/dist/docs-sync.js.map +1 -0
- package/dist/doctor.js.map +1 -0
- package/dist/file-delete.js.map +1 -0
- package/dist/file-edit.js.map +1 -0
- package/dist/file-read.js.map +1 -0
- package/dist/file-write.js.map +1 -0
- package/dist/flow-bottlenecks.js.map +1 -0
- package/dist/flow-report.js.map +1 -0
- package/dist/formatters.js +151 -0
- package/dist/formatters.js.map +1 -0
- package/dist/gate-defaults.js +137 -0
- package/dist/gate-defaults.js.map +1 -0
- package/dist/gate-registry.js +73 -0
- package/dist/gate-registry.js.map +1 -0
- package/dist/gates-graceful-degradation.js +153 -0
- package/dist/gates-graceful-degradation.js.map +1 -0
- package/dist/gates-plan-resolvers.js +190 -0
- package/dist/gates-plan-resolvers.js.map +1 -0
- package/dist/gates-runners.js +545 -0
- package/dist/gates-runners.js.map +1 -0
- package/dist/gates-types.js +4 -0
- package/dist/gates-types.js.map +1 -0
- package/dist/gates-utils.js +333 -0
- package/dist/gates-utils.js.map +1 -0
- package/dist/gates.js.map +1 -0
- package/dist/git-branch.js.map +1 -0
- package/dist/git-diff.js.map +1 -0
- package/dist/git-log.js.map +1 -0
- package/dist/git-status.js.map +1 -0
- package/dist/guard-locked.js +172 -0
- package/dist/guard-locked.js.map +1 -0
- package/dist/guard-main-branch.js +217 -0
- package/dist/guard-main-branch.js.map +1 -0
- package/dist/guard-worktree-commit.js +163 -0
- package/dist/guard-worktree-commit.js.map +1 -0
- package/dist/hooks/auto-checkpoint-utils.js +54 -0
- package/dist/hooks/auto-checkpoint-utils.js.map +1 -0
- package/dist/hooks/enforcement-checks.js +399 -0
- package/dist/hooks/enforcement-checks.js.map +1 -0
- package/dist/hooks/enforcement-generator.js +139 -0
- package/dist/hooks/enforcement-generator.js.map +1 -0
- package/dist/hooks/enforcement-sync.js +380 -0
- package/dist/hooks/enforcement-sync.js.map +1 -0
- package/dist/hooks/generators/auto-checkpoint.js +125 -0
- package/dist/hooks/generators/auto-checkpoint.js.map +1 -0
- package/dist/hooks/generators/enforce-worktree.js +190 -0
- package/dist/hooks/generators/enforce-worktree.js.map +1 -0
- package/dist/hooks/generators/index.js +18 -0
- package/dist/hooks/generators/index.js.map +1 -0
- package/dist/hooks/generators/pre-compact-checkpoint.js +136 -0
- package/dist/hooks/generators/pre-compact-checkpoint.js.map +1 -0
- package/dist/hooks/generators/require-wu.js +117 -0
- package/dist/hooks/generators/require-wu.js.map +1 -0
- package/dist/hooks/generators/session-start-recovery.js +103 -0
- package/dist/hooks/generators/session-start-recovery.js.map +1 -0
- package/dist/hooks/generators/signal-utils.js +54 -0
- package/dist/hooks/generators/signal-utils.js.map +1 -0
- package/dist/hooks/generators/warn-incomplete.js +67 -0
- package/dist/hooks/generators/warn-incomplete.js.map +1 -0
- package/dist/hooks/index.js +10 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.js.map +1 -0
- package/dist/init-detection.js +232 -0
- package/dist/init-detection.js.map +1 -0
- package/dist/init-lane-validation.js +147 -0
- package/dist/init-lane-validation.js.map +1 -0
- package/dist/init-scaffolding.js +158 -0
- package/dist/init-scaffolding.js.map +1 -0
- package/dist/init-templates.js +1983 -0
- package/dist/init-templates.js.map +1 -0
- package/dist/init.js.map +1 -0
- package/dist/initiative-add-wu.js.map +1 -0
- package/dist/initiative-bulk-assign-wus.js.map +1 -0
- package/dist/initiative-create.js.map +1 -0
- package/dist/initiative-edit.js.map +1 -0
- package/dist/initiative-list.js.map +1 -0
- package/dist/initiative-plan.js.map +1 -0
- package/dist/initiative-remove-wu.js.map +1 -0
- package/dist/initiative-status.js.map +1 -0
- package/dist/lane-edit.js.map +1 -0
- package/dist/lane-health.js.map +1 -0
- package/dist/lane-lifecycle-process.js +381 -0
- package/dist/lane-lifecycle-process.js.map +1 -0
- package/dist/lane-lock.js.map +1 -0
- package/dist/lane-setup.js.map +1 -0
- package/dist/lane-status.js.map +1 -0
- package/dist/lane-suggest.js.map +1 -0
- package/dist/lane-validate.js.map +1 -0
- package/dist/lifecycle-regression-harness.js +181 -0
- package/dist/lifecycle-regression-harness.js.map +1 -0
- package/dist/lumenflow-upgrade.js +18 -10
- package/dist/lumenflow-upgrade.js.map +1 -0
- package/dist/mem-checkpoint.js.map +1 -0
- package/dist/mem-cleanup.js.map +1 -0
- package/dist/mem-context.js.map +1 -0
- package/dist/mem-create.js.map +1 -0
- package/dist/mem-delete.js.map +1 -0
- package/dist/mem-export.js.map +1 -0
- package/dist/mem-inbox.js.map +1 -0
- package/dist/mem-index.js +214 -0
- package/dist/mem-index.js.map +1 -0
- package/dist/mem-init.js.map +1 -0
- package/dist/mem-profile.js +210 -0
- package/dist/mem-profile.js.map +1 -0
- package/dist/mem-promote.js +257 -0
- package/dist/mem-promote.js.map +1 -0
- package/dist/mem-ready.js.map +1 -0
- package/dist/mem-recover.js.map +1 -0
- package/dist/mem-signal.js.map +1 -0
- package/dist/mem-start.js.map +1 -0
- package/dist/mem-summarize.js.map +1 -0
- package/dist/mem-triage.js.map +1 -0
- package/dist/merge-block.js +225 -0
- package/dist/merge-block.js.map +1 -0
- package/dist/metrics-cli.js.map +1 -0
- package/dist/metrics-snapshot.js.map +1 -0
- package/dist/object-guards.js +9 -0
- package/dist/object-guards.js.map +1 -0
- package/dist/onboard.js.map +1 -0
- package/dist/onboarding-smoke-test.js +432 -0
- package/dist/onboarding-smoke-test.js.map +1 -0
- package/dist/orchestrate-init-status.js.map +1 -0
- package/dist/orchestrate-initiative.js.map +1 -0
- package/dist/orchestrate-monitor.js.map +1 -0
- package/dist/pack-author.js.map +1 -0
- package/dist/pack-hash.js.map +1 -0
- package/dist/pack-install.js.map +1 -0
- package/dist/pack-publish.js.map +1 -0
- package/dist/pack-scaffold.js.map +1 -0
- package/dist/pack-search.js.map +1 -0
- package/dist/pack-validate.js.map +1 -0
- package/dist/plan-create.js.map +1 -0
- package/dist/plan-edit.js.map +1 -0
- package/dist/plan-link.js.map +1 -0
- package/dist/plan-promote.js.map +1 -0
- package/dist/public-manifest.js +931 -0
- package/dist/public-manifest.js.map +1 -0
- package/dist/release.js +664 -116
- package/dist/release.js.map +1 -0
- package/dist/rotate-progress.js +253 -0
- package/dist/rotate-progress.js.map +1 -0
- package/dist/session-coordinator.js +303 -0
- package/dist/session-coordinator.js.map +1 -0
- package/dist/shared-validators.js +81 -0
- package/dist/shared-validators.js.map +1 -0
- package/dist/signal-cleanup.js.map +1 -0
- package/dist/state-bootstrap.js.map +1 -0
- package/dist/state-cleanup.js.map +1 -0
- package/dist/state-doctor-fix.js +226 -0
- package/dist/state-doctor-fix.js.map +1 -0
- package/dist/state-doctor-stamps.js +23 -0
- package/dist/state-doctor-stamps.js.map +1 -0
- package/dist/state-doctor.js.map +1 -0
- package/dist/strict-progress.js +255 -0
- package/dist/strict-progress.js.map +1 -0
- package/dist/sync-templates.js.map +1 -0
- package/dist/task-claim.js.map +1 -0
- package/dist/trace-gen.js +401 -0
- package/dist/trace-gen.js.map +1 -0
- package/dist/validate-agent-skills.js +224 -0
- package/dist/validate-agent-skills.js.map +1 -0
- package/dist/validate-agent-sync.js +152 -0
- package/dist/validate-agent-sync.js.map +1 -0
- package/dist/validate-backlog-sync.js +77 -0
- package/dist/validate-backlog-sync.js.map +1 -0
- package/dist/validate-skills-spec.js +211 -0
- package/dist/validate-skills-spec.js.map +1 -0
- package/dist/validate.js.map +1 -0
- package/dist/validator-defaults.js +107 -0
- package/dist/validator-defaults.js.map +1 -0
- package/dist/validator-registry.js +71 -0
- package/dist/validator-registry.js.map +1 -0
- package/dist/workspace-init.js.map +1 -0
- package/dist/wu-block.js.map +1 -0
- package/dist/wu-brief.js.map +1 -0
- package/dist/wu-claim-branch.js +123 -0
- package/dist/wu-claim-branch.js.map +1 -0
- package/dist/wu-claim-cloud.js +79 -0
- package/dist/wu-claim-cloud.js.map +1 -0
- package/dist/wu-claim-mode.js +82 -0
- package/dist/wu-claim-mode.js.map +1 -0
- package/dist/wu-claim-output.js +85 -0
- package/dist/wu-claim-output.js.map +1 -0
- package/dist/wu-claim-repair-guidance.js +12 -0
- package/dist/wu-claim-repair-guidance.js.map +1 -0
- package/dist/wu-claim-resume-handler.js +87 -0
- package/dist/wu-claim-resume-handler.js.map +1 -0
- package/dist/wu-claim-state.js +581 -0
- package/dist/wu-claim-state.js.map +1 -0
- package/dist/wu-claim-validation.js +458 -0
- package/dist/wu-claim-validation.js.map +1 -0
- package/dist/wu-claim-worktree.js +238 -0
- package/dist/wu-claim-worktree.js.map +1 -0
- package/dist/wu-claim.js.map +1 -0
- package/dist/wu-cleanup-cloud.js +78 -0
- package/dist/wu-cleanup-cloud.js.map +1 -0
- package/dist/wu-cleanup.js.map +1 -0
- package/dist/wu-code-path-coverage.js +83 -0
- package/dist/wu-code-path-coverage.js.map +1 -0
- package/dist/wu-create-cloud.js +30 -0
- package/dist/wu-create-cloud.js.map +1 -0
- package/dist/wu-create-content.js +264 -0
- package/dist/wu-create-content.js.map +1 -0
- package/dist/wu-create-readiness.js +59 -0
- package/dist/wu-create-readiness.js.map +1 -0
- package/dist/wu-create-validation.js +128 -0
- package/dist/wu-create-validation.js.map +1 -0
- package/dist/wu-create.js.map +1 -0
- package/dist/wu-delegate.js.map +1 -0
- package/dist/wu-delete.js.map +1 -0
- package/dist/wu-deps.js.map +1 -0
- package/dist/wu-done-auto-cleanup.js +194 -0
- package/dist/wu-done-auto-cleanup.js.map +1 -0
- package/dist/wu-done-check.js +38 -0
- package/dist/wu-done-check.js.map +1 -0
- package/dist/wu-done-cloud.js +48 -0
- package/dist/wu-done-cloud.js.map +1 -0
- package/dist/wu-done-decay.js +83 -0
- package/dist/wu-done-decay.js.map +1 -0
- package/dist/wu-done.js.map +1 -0
- package/dist/wu-edit-operations.js +399 -0
- package/dist/wu-edit-operations.js.map +1 -0
- package/dist/wu-edit-validators.js +282 -0
- package/dist/wu-edit-validators.js.map +1 -0
- package/dist/wu-edit.js.map +1 -0
- package/dist/wu-infer-lane.js.map +1 -0
- package/dist/wu-preflight.js.map +1 -0
- package/dist/wu-prep.js.map +1 -0
- package/dist/wu-proto.js.map +1 -0
- package/dist/wu-prune.js.map +1 -0
- package/dist/wu-recover.js.map +1 -0
- package/dist/wu-release.js.map +1 -0
- package/dist/wu-repair.js.map +1 -0
- package/dist/wu-sandbox.js.map +1 -0
- package/dist/wu-spawn-completion.js +58 -0
- package/dist/wu-spawn-completion.js.map +1 -0
- package/dist/wu-spawn-prompt-builders.js +1190 -0
- package/dist/wu-spawn-prompt-builders.js.map +1 -0
- package/dist/wu-spawn-strategy-resolver.js +322 -0
- package/dist/wu-spawn-strategy-resolver.js.map +1 -0
- package/dist/wu-spawn.js +59 -0
- package/dist/wu-spawn.js.map +1 -0
- package/dist/wu-state-cloud.js +41 -0
- package/dist/wu-state-cloud.js.map +1 -0
- package/dist/wu-status.js.map +1 -0
- package/dist/wu-unblock.js.map +1 -0
- package/dist/wu-unlock-lane.js.map +1 -0
- package/dist/wu-validate.js.map +1 -0
- package/package.json +8 -10
package/dist/release.js
CHANGED
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* Release Command
|
|
6
6
|
*
|
|
7
|
-
* Orchestrates npm release for all @lumenflow/* packages
|
|
7
|
+
* Orchestrates npm release for all @lumenflow/* packages.
|
|
8
8
|
*
|
|
9
9
|
* Features:
|
|
10
10
|
* - Validates semver version format
|
|
11
11
|
* - Bumps all @lumenflow/* package versions atomically
|
|
12
|
-
* - Uses micro-worktree isolation for version commit (no main branch pollution)
|
|
13
12
|
* - Builds all packages via turbo
|
|
13
|
+
* - Validates packed artifacts against package contracts
|
|
14
14
|
* - Publishes to npm with proper auth (requires NPM_TOKEN)
|
|
15
15
|
* - Creates git tag vX.Y.Z
|
|
16
|
+
* - Cleanup-on-failure ensures main is never left dirty (WU-2062)
|
|
16
17
|
*
|
|
17
18
|
* Usage:
|
|
18
19
|
* pnpm release --release-version 1.3.0
|
|
@@ -25,21 +26,18 @@
|
|
|
25
26
|
* WU-1074: Add release command for npm publishing
|
|
26
27
|
*/
|
|
27
28
|
import { Command } from 'commander';
|
|
28
|
-
import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
|
|
29
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync, } from 'node:fs';
|
|
29
30
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
30
|
-
import { join } from 'node:path';
|
|
31
|
+
import { dirname, join } from 'node:path';
|
|
31
32
|
import { homedir } from 'node:os';
|
|
32
|
-
import { execSync } from 'node:child_process';
|
|
33
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
33
34
|
import { getGitForCwd } from '@lumenflow/core/git-adapter';
|
|
34
35
|
import { die } from '@lumenflow/core/error-handler';
|
|
35
|
-
import { withMicroWorktree } from '@lumenflow/core/micro-worktree';
|
|
36
36
|
import { ensureOnMain } from '@lumenflow/core/wu-helpers';
|
|
37
37
|
import { REMOTES, FILE_SYSTEM, PKG_MANAGER } from '@lumenflow/core/wu-constants';
|
|
38
38
|
import { runCLI } from './cli-entry-point.js';
|
|
39
39
|
/** Log prefix for console output */
|
|
40
40
|
const LOG_PREFIX = '[release]';
|
|
41
|
-
/** Micro-worktree operation name */
|
|
42
|
-
const OPERATION_NAME = 'release';
|
|
43
41
|
/** Directory containing @lumenflow packages */
|
|
44
42
|
const LUMENFLOW_PACKAGES_DIR = 'packages/@lumenflow';
|
|
45
43
|
/** Path to the bare lumenflow wrapper package (WU-1691) */
|
|
@@ -65,6 +63,52 @@ const CHANGESET_DIR = '.changeset';
|
|
|
65
63
|
const LUMENFLOW_FORCE_ENV = 'LUMENFLOW_FORCE';
|
|
66
64
|
/** Environment variable to provide reason for force bypass */
|
|
67
65
|
const LUMENFLOW_FORCE_REASON_ENV = 'LUMENFLOW_FORCE_REASON';
|
|
66
|
+
/** Release phase label for pre-release clean-tree validation */
|
|
67
|
+
const RELEASE_CLEAN_CHECK_PHASE_BEFORE_RELEASE = 'before release';
|
|
68
|
+
/** Command shown when release fails due to dirty working tree */
|
|
69
|
+
const GIT_STATUS_SHORT_COMMAND = 'git status --short';
|
|
70
|
+
/** Guidance shown when generated artifacts dirty the repository */
|
|
71
|
+
const CLEAN_TREE_RECOVERY_GUIDANCE = 'Commit, stash, or clean generated files before retrying release.';
|
|
72
|
+
/** Package manifest filename */
|
|
73
|
+
const PACKAGE_JSON_FILENAME = 'package.json';
|
|
74
|
+
/** Source directory name used for build sanity checks */
|
|
75
|
+
const SOURCE_DIR_NAME = 'src';
|
|
76
|
+
/** Dist directory name used for release artifacts */
|
|
77
|
+
const DIST_DIR_NAME = 'dist';
|
|
78
|
+
/** Relative path prefix used in package manifests */
|
|
79
|
+
const RELATIVE_PATH_PREFIX = './';
|
|
80
|
+
/** Previous-pack sanity threshold (current must be >= 10% of previous) */
|
|
81
|
+
const PREVIOUS_PACK_MIN_RATIO = 0.1;
|
|
82
|
+
/** Path prefix emitted by some pack tools (for example package/dist/index.js) */
|
|
83
|
+
const PACK_TOOL_PACKAGE_PREFIX = 'package/';
|
|
84
|
+
/** Path prefixes used when normalizing manifest paths */
|
|
85
|
+
const NODE_PROTOCOL_PREFIX = 'node:';
|
|
86
|
+
const HTTP_PROTOCOL_PREFIX = 'http://';
|
|
87
|
+
const HTTPS_PROTOCOL_PREFIX = 'https://';
|
|
88
|
+
const PARENT_RELATIVE_PREFIX = '../';
|
|
89
|
+
/** Path separator constants for normalization */
|
|
90
|
+
const POSIX_PATH_SEPARATOR = '/';
|
|
91
|
+
const WINDOWS_PATH_SEPARATOR = '\\';
|
|
92
|
+
/** Labels for release logging phases */
|
|
93
|
+
const PRE_FLIGHT_LABEL = 'preflight';
|
|
94
|
+
const PACK_VALIDATE_LABEL = 'pack:validate';
|
|
95
|
+
const PACK_BASELINE_LABEL = 'pack:baseline';
|
|
96
|
+
/** Pack validation error messages */
|
|
97
|
+
const PACK_EMPTY_OUTPUT_ERROR = 'pack command produced empty output';
|
|
98
|
+
const PACK_INVALID_JSON_ERROR = 'pack command produced invalid JSON payload';
|
|
99
|
+
const PACK_MISSING_FILES_ARRAY_ERROR = 'pack command JSON missing required files[]';
|
|
100
|
+
const PACK_INVALID_FILES_ENTRY_ERROR = 'pack command JSON has invalid files[] entry';
|
|
101
|
+
const PACK_ZERO_FILES_ERROR = 'pack dry-run returned zero files';
|
|
102
|
+
const DIST_EMPTY_ERROR = 'dist directory has no files after build';
|
|
103
|
+
const MISSING_CONTRACT_PREFIX = 'Missing packaged files declared by package.json contract: ';
|
|
104
|
+
const RELEASE_VALIDATION_FAILURE_HEADER = 'Release artifact validation failed. Refusing to publish broken tarballs.';
|
|
105
|
+
const RELEASE_VALIDATION_FAILURE_FOOTER = 'Fix the package exports/build outputs, then re-run release.';
|
|
106
|
+
const DIST_BUILD_INCOMPLETE_PREFIX = 'dist has fewer files than src';
|
|
107
|
+
const DIST_BUILD_INCOMPLETE_SUFFIX = 'build artifacts look incomplete';
|
|
108
|
+
const PACK_COUNT_BELOW_BASELINE_PREFIX = 'packed file count';
|
|
109
|
+
const PACK_COUNT_BELOW_BASELINE_MID = 'is below 10% of previous published version';
|
|
110
|
+
const SKIP_BUILD_SYMLINK_ERROR_PREFIX = 'Refusing release with --skip-build:';
|
|
111
|
+
const SKIP_BUILD_SYMLINK_ERROR_GUIDANCE = 'Run release without --skip-build so dist can be rebuilt as real files.';
|
|
68
112
|
/**
|
|
69
113
|
* Environment variable for WU tool identification (WU-1296)
|
|
70
114
|
* Pre-push hook checks this to allow approved tool operations
|
|
@@ -101,6 +145,22 @@ export async function withReleaseEnv(fn) {
|
|
|
101
145
|
}
|
|
102
146
|
}
|
|
103
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Assert the current git working tree is clean.
|
|
150
|
+
*
|
|
151
|
+
* @param git - Git adapter with cleanliness check
|
|
152
|
+
* @param phase - Human-readable release phase label
|
|
153
|
+
*/
|
|
154
|
+
export async function assertWorkingTreeClean(git, phase) {
|
|
155
|
+
const isClean = await git.isClean();
|
|
156
|
+
if (isClean) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
die(`Working directory has uncommitted changes ${phase}.\n\n` +
|
|
160
|
+
`Run this command to inspect unexpected artifacts:\n` +
|
|
161
|
+
` ${GIT_STATUS_SHORT_COMMAND}\n` +
|
|
162
|
+
`${CLEAN_TREE_RECOVERY_GUIDANCE}`);
|
|
163
|
+
}
|
|
104
164
|
/**
|
|
105
165
|
* Validate that a string is a valid semver version
|
|
106
166
|
*
|
|
@@ -128,7 +188,7 @@ export function findPackageJsonPaths(baseDir = process.cwd()) {
|
|
|
128
188
|
const entries = readdirSync(packagesDir);
|
|
129
189
|
for (const entry of entries) {
|
|
130
190
|
const entryPath = join(packagesDir, entry);
|
|
131
|
-
const packageJsonPath = join(entryPath,
|
|
191
|
+
const packageJsonPath = join(entryPath, PACKAGE_JSON_FILENAME);
|
|
132
192
|
if (statSync(entryPath).isDirectory() && existsSync(packageJsonPath)) {
|
|
133
193
|
// Read package.json to check if it's private
|
|
134
194
|
const content = JSON.parse(readFileSync(packageJsonPath, { encoding: FILE_SYSTEM.UTF8 }));
|
|
@@ -140,7 +200,7 @@ export function findPackageJsonPaths(baseDir = process.cwd()) {
|
|
|
140
200
|
}
|
|
141
201
|
}
|
|
142
202
|
// WU-1691: Include the bare lumenflow wrapper package
|
|
143
|
-
const wrapperPackageJson = join(baseDir, LUMENFLOW_WRAPPER_PACKAGE,
|
|
203
|
+
const wrapperPackageJson = join(baseDir, LUMENFLOW_WRAPPER_PACKAGE, PACKAGE_JSON_FILENAME);
|
|
144
204
|
if (existsSync(wrapperPackageJson)) {
|
|
145
205
|
const content = JSON.parse(readFileSync(wrapperPackageJson, { encoding: FILE_SYSTEM.UTF8 }));
|
|
146
206
|
if (!content.private) {
|
|
@@ -168,6 +228,227 @@ export async function updatePackageVersions(paths, version) {
|
|
|
168
228
|
await writeFile(packagePath, updated, { encoding: FILE_SYSTEM.ENCODING });
|
|
169
229
|
}
|
|
170
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Convert manifest file paths (for example "./dist/index.js") into package-relative paths.
|
|
233
|
+
*/
|
|
234
|
+
function normalizeManifestPath(filePath) {
|
|
235
|
+
const trimmed = filePath.trim();
|
|
236
|
+
if (!trimmed) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
if (trimmed.startsWith(NODE_PROTOCOL_PREFIX) ||
|
|
240
|
+
trimmed.startsWith(HTTP_PROTOCOL_PREFIX) ||
|
|
241
|
+
trimmed.startsWith(HTTPS_PROTOCOL_PREFIX)) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
if (trimmed.startsWith(RELATIVE_PATH_PREFIX)) {
|
|
245
|
+
return trimmed
|
|
246
|
+
.slice(RELATIVE_PATH_PREFIX.length)
|
|
247
|
+
.replaceAll(WINDOWS_PATH_SEPARATOR, POSIX_PATH_SEPARATOR);
|
|
248
|
+
}
|
|
249
|
+
if (trimmed.startsWith(PARENT_RELATIVE_PREFIX) || trimmed.includes(POSIX_PATH_SEPARATOR)) {
|
|
250
|
+
return trimmed.replaceAll(WINDOWS_PATH_SEPARATOR, POSIX_PATH_SEPARATOR);
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Normalize packed tarball paths across npm/pnpm variants.
|
|
256
|
+
*
|
|
257
|
+
* Some pack tools emit "package/dist/index.js" while others emit "dist/index.js".
|
|
258
|
+
* Contract comparisons use package-relative paths, so strip the optional prefix.
|
|
259
|
+
*/
|
|
260
|
+
function normalizePackedPath(filePath) {
|
|
261
|
+
const normalized = normalizeManifestPath(filePath);
|
|
262
|
+
if (!normalized) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
if (normalized.startsWith(PACK_TOOL_PACKAGE_PREFIX)) {
|
|
266
|
+
return normalized.slice(PACK_TOOL_PACKAGE_PREFIX.length);
|
|
267
|
+
}
|
|
268
|
+
return normalized;
|
|
269
|
+
}
|
|
270
|
+
function buildDistCountMismatchError(distFileCount, srcFileCount) {
|
|
271
|
+
return `${DIST_BUILD_INCOMPLETE_PREFIX} (${distFileCount} < ${srcFileCount}), ${DIST_BUILD_INCOMPLETE_SUFFIX}`;
|
|
272
|
+
}
|
|
273
|
+
function buildPackBaselineThresholdError(packedFileCount, previousPackedFileCount) {
|
|
274
|
+
return `${PACK_COUNT_BELOW_BASELINE_PREFIX} ${packedFileCount} ${PACK_COUNT_BELOW_BASELINE_MID} (${previousPackedFileCount})`;
|
|
275
|
+
}
|
|
276
|
+
function buildSkipBuildSymlinkError(relativeDistPath) {
|
|
277
|
+
return (`${SKIP_BUILD_SYMLINK_ERROR_PREFIX} ${relativeDistPath} is a symlink.\n` +
|
|
278
|
+
`${SKIP_BUILD_SYMLINK_ERROR_GUIDANCE}`);
|
|
279
|
+
}
|
|
280
|
+
function buildWorkspacePackDryRunCommand(packageName) {
|
|
281
|
+
return `${PKG_MANAGER} --filter "${packageName}" pack --json --dry-run`;
|
|
282
|
+
}
|
|
283
|
+
function buildLatestPublishedPackDryRunCommand(packageName) {
|
|
284
|
+
return `npm pack "${packageName}@latest" --json --dry-run`;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Collect all string leaf values from nested export conditions.
|
|
288
|
+
*/
|
|
289
|
+
function collectLeafStringValues(value, collector) {
|
|
290
|
+
if (typeof value === 'string') {
|
|
291
|
+
collector.push(value);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (Array.isArray(value)) {
|
|
295
|
+
for (const entry of value) {
|
|
296
|
+
collectLeafStringValues(entry, collector);
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (value && typeof value === 'object') {
|
|
301
|
+
for (const entry of Object.values(value)) {
|
|
302
|
+
collectLeafStringValues(entry, collector);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Derive package-file contract paths from package.json fields.
|
|
308
|
+
*
|
|
309
|
+
* Contract source of truth:
|
|
310
|
+
* - exports map (all subpath leaf values)
|
|
311
|
+
* - main/types
|
|
312
|
+
* - bin targets
|
|
313
|
+
*/
|
|
314
|
+
export function extractPackageContractPaths(manifest) {
|
|
315
|
+
const rawPaths = [];
|
|
316
|
+
if (typeof manifest.main === 'string') {
|
|
317
|
+
rawPaths.push(manifest.main);
|
|
318
|
+
}
|
|
319
|
+
if (typeof manifest.types === 'string') {
|
|
320
|
+
rawPaths.push(manifest.types);
|
|
321
|
+
}
|
|
322
|
+
if (typeof manifest.bin === 'string') {
|
|
323
|
+
rawPaths.push(manifest.bin);
|
|
324
|
+
}
|
|
325
|
+
else if (manifest.bin && typeof manifest.bin === 'object') {
|
|
326
|
+
rawPaths.push(...Object.values(manifest.bin));
|
|
327
|
+
}
|
|
328
|
+
if (manifest.exports !== undefined) {
|
|
329
|
+
collectLeafStringValues(manifest.exports, rawPaths);
|
|
330
|
+
}
|
|
331
|
+
const deduped = new Set();
|
|
332
|
+
for (const rawPath of rawPaths) {
|
|
333
|
+
const normalized = normalizeManifestPath(rawPath);
|
|
334
|
+
if (normalized) {
|
|
335
|
+
deduped.add(normalized);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return [...deduped];
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Determine if this package publishes dist artifacts.
|
|
342
|
+
*/
|
|
343
|
+
function packageExpectsDist(manifest, contractPaths) {
|
|
344
|
+
const files = manifest.files ?? [];
|
|
345
|
+
const includesDistInFiles = files.some((entry) => {
|
|
346
|
+
const normalized = entry
|
|
347
|
+
.trim()
|
|
348
|
+
.replaceAll(WINDOWS_PATH_SEPARATOR, POSIX_PATH_SEPARATOR)
|
|
349
|
+
.replace(/\/+$/, '');
|
|
350
|
+
return normalized === DIST_DIR_NAME || normalized.startsWith(`${DIST_DIR_NAME}/`);
|
|
351
|
+
});
|
|
352
|
+
return (includesDistInFiles || contractPaths.some((entry) => entry.startsWith(`${DIST_DIR_NAME}/`)));
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Count files recursively for sanity checks.
|
|
356
|
+
*/
|
|
357
|
+
function countFilesRecursive(pathToCount) {
|
|
358
|
+
if (!existsSync(pathToCount)) {
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
const stat = lstatSync(pathToCount);
|
|
362
|
+
if (!stat.isDirectory()) {
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
let fileCount = 0;
|
|
366
|
+
const entries = readdirSync(pathToCount, { withFileTypes: true });
|
|
367
|
+
for (const entry of entries) {
|
|
368
|
+
const absolutePath = join(pathToCount, entry.name);
|
|
369
|
+
if (entry.isDirectory()) {
|
|
370
|
+
fileCount += countFilesRecursive(absolutePath);
|
|
371
|
+
}
|
|
372
|
+
else if (entry.isFile()) {
|
|
373
|
+
fileCount += 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return fileCount;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Validate packed artifacts against package contract paths and dynamic sanity checks.
|
|
380
|
+
*/
|
|
381
|
+
export function validatePackedArtifacts(input) {
|
|
382
|
+
const contractPaths = extractPackageContractPaths(input.manifest);
|
|
383
|
+
const normalizedPackedFiles = input.packedFiles
|
|
384
|
+
.map((entry) => normalizePackedPath(entry))
|
|
385
|
+
.filter((entry) => entry !== null);
|
|
386
|
+
const packedSet = new Set(normalizedPackedFiles);
|
|
387
|
+
const missingContractPaths = contractPaths.filter((entry) => !packedSet.has(entry));
|
|
388
|
+
const errors = [];
|
|
389
|
+
if (input.packedFiles.length === 0) {
|
|
390
|
+
errors.push(PACK_ZERO_FILES_ERROR);
|
|
391
|
+
}
|
|
392
|
+
if (missingContractPaths.length > 0) {
|
|
393
|
+
errors.push(`${MISSING_CONTRACT_PREFIX}${missingContractPaths.join(', ')}`);
|
|
394
|
+
}
|
|
395
|
+
const expectsDist = packageExpectsDist(input.manifest, contractPaths);
|
|
396
|
+
if (expectsDist) {
|
|
397
|
+
if (input.distFileCount === 0) {
|
|
398
|
+
errors.push(DIST_EMPTY_ERROR);
|
|
399
|
+
}
|
|
400
|
+
else if (input.srcFileCount > 0 && input.distFileCount < input.srcFileCount) {
|
|
401
|
+
errors.push(buildDistCountMismatchError(input.distFileCount, input.srcFileCount));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (input.previousPackedFileCount !== undefined && input.previousPackedFileCount > 0) {
|
|
405
|
+
const minimumExpected = Math.max(1, Math.ceil(input.previousPackedFileCount * PREVIOUS_PACK_MIN_RATIO));
|
|
406
|
+
if (input.packedFiles.length < minimumExpected) {
|
|
407
|
+
errors.push(buildPackBaselineThresholdError(input.packedFiles.length, input.previousPackedFileCount));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
ok: errors.length === 0,
|
|
412
|
+
packageName: input.packageName,
|
|
413
|
+
contractPaths,
|
|
414
|
+
missingContractPaths,
|
|
415
|
+
errors,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Replace symlinked dist directories with real directories.
|
|
420
|
+
*
|
|
421
|
+
* This avoids npm pack/publish inconsistencies from cross-worktree dist symlinks.
|
|
422
|
+
*/
|
|
423
|
+
export function ensureDistPathsMaterialized(packageDirs, options = {}) {
|
|
424
|
+
const { skipBuild = false, dryRun = false } = options;
|
|
425
|
+
let checkedCount = 0;
|
|
426
|
+
let materializedCount = 0;
|
|
427
|
+
for (const packageDir of packageDirs) {
|
|
428
|
+
const distPath = join(packageDir, DIST_DIR_NAME);
|
|
429
|
+
if (!existsSync(distPath)) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const distStat = lstatSync(distPath);
|
|
433
|
+
if (!distStat.isSymbolicLink()) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
checkedCount += 1;
|
|
437
|
+
const relativeDistPath = distPath.replace(`${process.cwd()}/`, '');
|
|
438
|
+
if (skipBuild) {
|
|
439
|
+
die(buildSkipBuildSymlinkError(relativeDistPath));
|
|
440
|
+
}
|
|
441
|
+
if (dryRun) {
|
|
442
|
+
console.log(`${LOG_PREFIX} [${PRE_FLIGHT_LABEL}] Would materialize symlinked dist at ${relativeDistPath}`);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
rmSync(distPath, { recursive: true, force: true });
|
|
446
|
+
mkdirSync(distPath, { recursive: true });
|
|
447
|
+
materializedCount += 1;
|
|
448
|
+
console.log(`${LOG_PREFIX} [${PRE_FLIGHT_LABEL}] Materialized symlinked dist at ${relativeDistPath}`);
|
|
449
|
+
}
|
|
450
|
+
return { checkedCount, materializedCount };
|
|
451
|
+
}
|
|
171
452
|
/**
|
|
172
453
|
* Build commit message for version bump
|
|
173
454
|
*
|
|
@@ -186,16 +467,6 @@ export function buildCommitMessage(version) {
|
|
|
186
467
|
export function buildTagName(version) {
|
|
187
468
|
return `v${version}`;
|
|
188
469
|
}
|
|
189
|
-
/**
|
|
190
|
-
* Get relative path from worktree root
|
|
191
|
-
*
|
|
192
|
-
* @param absolutePath - Absolute file path
|
|
193
|
-
* @param worktreePath - Worktree root path
|
|
194
|
-
* @returns Relative path
|
|
195
|
-
*/
|
|
196
|
-
function getRelativePath(absolutePath, worktreePath) {
|
|
197
|
-
return absolutePath.replace(worktreePath + '/', '');
|
|
198
|
-
}
|
|
199
470
|
/**
|
|
200
471
|
* Execute a shell command and handle errors
|
|
201
472
|
*
|
|
@@ -221,6 +492,188 @@ function runCommand(cmd, options = {}) {
|
|
|
221
492
|
throw new Error(`Command failed: ${cmd}`, { cause: error });
|
|
222
493
|
}
|
|
223
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* Execute a shell command and capture stdout.
|
|
497
|
+
*/
|
|
498
|
+
function runCommandCapture(cmd, options = {}) {
|
|
499
|
+
const { cwd = process.cwd(), label } = options;
|
|
500
|
+
const prefix = label ? `[${label}] ` : '';
|
|
501
|
+
console.log(`${LOG_PREFIX} ${prefix}Running: ${cmd}`);
|
|
502
|
+
try {
|
|
503
|
+
return execSync(cmd, {
|
|
504
|
+
cwd,
|
|
505
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
506
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
throw new Error(`Command failed: ${cmd}`, { cause: error });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/** Characters that can start a valid JSON value from pack commands */
|
|
514
|
+
const JSON_ARRAY_START = '[';
|
|
515
|
+
const JSON_OBJECT_START = '{';
|
|
516
|
+
/**
|
|
517
|
+
* Find the index of the first real JSON-start sequence in a string.
|
|
518
|
+
*
|
|
519
|
+
* pnpm lifecycle scripts can prepend non-JSON output to stdout before the
|
|
520
|
+
* actual JSON payload — including log-style brackets like `[sync:bundled-packs]`.
|
|
521
|
+
* A bare `[` is ambiguous, so this function peeks at the next non-whitespace
|
|
522
|
+
* character to distinguish JSON arrays (`[{`, `["`, `[]`) and objects (`{"`, `{}`)
|
|
523
|
+
* from log prefixes.
|
|
524
|
+
*
|
|
525
|
+
* WU-2062: Replaces naive indexOf('[') which matched log brackets.
|
|
526
|
+
*
|
|
527
|
+
* @returns Index of the first valid JSON start, or 0 if not found (let JSON.parse report the error)
|
|
528
|
+
*/
|
|
529
|
+
export function findJsonStartIndex(raw) {
|
|
530
|
+
for (let i = 0; i < raw.length; i++) {
|
|
531
|
+
const ch = raw[i];
|
|
532
|
+
if (ch !== JSON_ARRAY_START && ch !== JSON_OBJECT_START) {
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
// Peek past optional whitespace to verify this starts a JSON value,
|
|
536
|
+
// not a log-style bracket like [sync:bundled-packs].
|
|
537
|
+
let j = i + 1;
|
|
538
|
+
while (j < raw.length &&
|
|
539
|
+
(raw[j] === ' ' || raw[j] === '\t' || raw[j] === '\n' || raw[j] === '\r')) {
|
|
540
|
+
j++;
|
|
541
|
+
}
|
|
542
|
+
if (j >= raw.length) {
|
|
543
|
+
return i;
|
|
544
|
+
}
|
|
545
|
+
const next = raw[j];
|
|
546
|
+
// Valid JSON array starts: [{ , [" , [] (empty)
|
|
547
|
+
if (ch === JSON_ARRAY_START && (next === JSON_OBJECT_START || next === '"' || next === ']')) {
|
|
548
|
+
return i;
|
|
549
|
+
}
|
|
550
|
+
// Valid JSON object starts: {" , {} (empty)
|
|
551
|
+
if (ch === JSON_OBJECT_START && (next === '"' || next === '}')) {
|
|
552
|
+
return i;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Not found — return 0 so JSON.parse reports the actual error
|
|
556
|
+
return 0;
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Parse JSON output from npm/pnpm pack dry runs.
|
|
560
|
+
*
|
|
561
|
+
* pnpm lifecycle scripts (prepack/postpack) can emit non-JSON text to stdout
|
|
562
|
+
* before the actual JSON payload. This function strips such prefix noise by
|
|
563
|
+
* locating the first `[` or `{` character — the start of valid JSON.
|
|
564
|
+
*/
|
|
565
|
+
export function parsePackDryRunMetadata(rawOutput) {
|
|
566
|
+
const trimmed = rawOutput.trim();
|
|
567
|
+
if (!trimmed) {
|
|
568
|
+
throw new Error(PACK_EMPTY_OUTPUT_ERROR);
|
|
569
|
+
}
|
|
570
|
+
// pnpm lifecycle scripts (prepack/postpack) can emit text to stdout before
|
|
571
|
+
// the JSON payload. Strip everything before the first JSON-start character.
|
|
572
|
+
const jsonStartIndex = findJsonStartIndex(trimmed);
|
|
573
|
+
const jsonPayload = jsonStartIndex > 0 ? trimmed.slice(jsonStartIndex) : trimmed;
|
|
574
|
+
const parsed = JSON.parse(jsonPayload);
|
|
575
|
+
const normalized = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
576
|
+
if (!normalized || typeof normalized !== 'object') {
|
|
577
|
+
throw new Error(PACK_INVALID_JSON_ERROR);
|
|
578
|
+
}
|
|
579
|
+
const files = normalized.files;
|
|
580
|
+
if (!Array.isArray(files)) {
|
|
581
|
+
throw new Error(PACK_MISSING_FILES_ARRAY_ERROR);
|
|
582
|
+
}
|
|
583
|
+
for (const file of files) {
|
|
584
|
+
if (!file ||
|
|
585
|
+
typeof file !== 'object' ||
|
|
586
|
+
typeof file.path !== 'string') {
|
|
587
|
+
throw new Error(PACK_INVALID_FILES_ENTRY_ERROR);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const entryCount = normalized.entryCount;
|
|
591
|
+
return {
|
|
592
|
+
files: files,
|
|
593
|
+
entryCount: typeof entryCount === 'number' ? entryCount : undefined,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Read package manifest for release validation.
|
|
598
|
+
*/
|
|
599
|
+
function readPackageManifest(packageJsonPath) {
|
|
600
|
+
return JSON.parse(readFileSync(packageJsonPath, {
|
|
601
|
+
encoding: FILE_SYSTEM.UTF8,
|
|
602
|
+
}));
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Resolve packed file list for a workspace package via pnpm pack --dry-run.
|
|
606
|
+
*/
|
|
607
|
+
function getWorkspacePackedFiles(packageName) {
|
|
608
|
+
const output = runCommandCapture(buildWorkspacePackDryRunCommand(packageName), {
|
|
609
|
+
label: PACK_VALIDATE_LABEL,
|
|
610
|
+
});
|
|
611
|
+
const metadata = parsePackDryRunMetadata(output);
|
|
612
|
+
return metadata.files
|
|
613
|
+
.map((entry) => normalizePackedPath(entry.path))
|
|
614
|
+
.filter((entry) => entry !== null);
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Resolve packed file count for the currently published npm version.
|
|
618
|
+
*/
|
|
619
|
+
function getPreviousPublishedPackFileCount(packageName) {
|
|
620
|
+
try {
|
|
621
|
+
const output = runCommandCapture(buildLatestPublishedPackDryRunCommand(packageName), {
|
|
622
|
+
label: PACK_BASELINE_LABEL,
|
|
623
|
+
});
|
|
624
|
+
const metadata = parsePackDryRunMetadata(output);
|
|
625
|
+
const entryCount = metadata.entryCount;
|
|
626
|
+
if (typeof entryCount === 'number') {
|
|
627
|
+
return entryCount;
|
|
628
|
+
}
|
|
629
|
+
return metadata.files.length;
|
|
630
|
+
}
|
|
631
|
+
catch (error) {
|
|
632
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
633
|
+
console.warn(`${LOG_PREFIX} [${PACK_BASELINE_LABEL}] Skipping previous-version count for ${packageName}: ${message}`);
|
|
634
|
+
return undefined;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Validate release package artifacts before tag/publish.
|
|
639
|
+
*/
|
|
640
|
+
function validateReleaseArtifactsForPublish(packageJsonPaths, dryRun) {
|
|
641
|
+
if (dryRun) {
|
|
642
|
+
console.log(`${LOG_PREFIX} [${PACK_VALIDATE_LABEL}] Would validate packed artifacts against package contracts`);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const failures = [];
|
|
646
|
+
for (const packageJsonPath of packageJsonPaths) {
|
|
647
|
+
const packageDir = dirname(packageJsonPath);
|
|
648
|
+
const manifest = readPackageManifest(packageJsonPath);
|
|
649
|
+
const packageName = manifest.name ?? packageDir;
|
|
650
|
+
const packedFiles = getWorkspacePackedFiles(packageName);
|
|
651
|
+
const previousPackedFileCount = getPreviousPublishedPackFileCount(packageName);
|
|
652
|
+
const srcFileCount = countFilesRecursive(join(packageDir, SOURCE_DIR_NAME));
|
|
653
|
+
const distFileCount = countFilesRecursive(join(packageDir, DIST_DIR_NAME));
|
|
654
|
+
const result = validatePackedArtifacts({
|
|
655
|
+
packageName,
|
|
656
|
+
packageDir,
|
|
657
|
+
manifest,
|
|
658
|
+
packedFiles,
|
|
659
|
+
srcFileCount,
|
|
660
|
+
distFileCount,
|
|
661
|
+
previousPackedFileCount,
|
|
662
|
+
});
|
|
663
|
+
if (!result.ok) {
|
|
664
|
+
failures.push(result);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (failures.length > 0) {
|
|
668
|
+
const details = failures
|
|
669
|
+
.map((failure) => `- ${failure.packageName}\n` + failure.errors.map((error) => ` - ${error}`).join('\n'))
|
|
670
|
+
.join('\n');
|
|
671
|
+
die(`${RELEASE_VALIDATION_FAILURE_HEADER}\n\n` +
|
|
672
|
+
`${details}\n\n` +
|
|
673
|
+
`${RELEASE_VALIDATION_FAILURE_FOOTER}`);
|
|
674
|
+
}
|
|
675
|
+
console.log(`${LOG_PREFIX} ✅ Packed artifact validation passed for ${packageJsonPaths.length} packages`);
|
|
676
|
+
}
|
|
224
677
|
/**
|
|
225
678
|
* Check if npm authentication is available
|
|
226
679
|
*
|
|
@@ -313,6 +766,63 @@ export async function pushTagWithForce(git, tagName, reason = 'release: tag push
|
|
|
313
766
|
}
|
|
314
767
|
}
|
|
315
768
|
}
|
|
769
|
+
/**
|
|
770
|
+
* Clean up release artifacts after a failed release attempt.
|
|
771
|
+
*
|
|
772
|
+
* WU-2062: Ensures main is never left dirty by a failed release. Handles:
|
|
773
|
+
* 1. Materialized dist directories (symlinks replaced with real dirs by preflight)
|
|
774
|
+
* 2. Generated packs/ directories (from prepack lifecycle scripts)
|
|
775
|
+
* 3. Version-bumped package.json files (from Phase 2)
|
|
776
|
+
*
|
|
777
|
+
* After cleanup, all tracked files are restored to HEAD state and untracked
|
|
778
|
+
* generated files are removed.
|
|
779
|
+
*/
|
|
780
|
+
async function cleanupFailedRelease(packageDirs) {
|
|
781
|
+
console.error(`\n${LOG_PREFIX} ⚠️ Release failed — cleaning up artifacts on main...`);
|
|
782
|
+
try {
|
|
783
|
+
// 1. Remove materialized dist directories so git can restore symlinks.
|
|
784
|
+
// ensureDistPathsMaterialized replaces symlinks with real dirs; build fills
|
|
785
|
+
// them with untracked output. Must delete before git checkout can recreate
|
|
786
|
+
// the tracked symlink.
|
|
787
|
+
for (const packageDir of packageDirs) {
|
|
788
|
+
const distPath = join(packageDir, DIST_DIR_NAME);
|
|
789
|
+
if (existsSync(distPath) && !lstatSync(distPath).isSymbolicLink()) {
|
|
790
|
+
rmSync(distPath, { recursive: true, force: true });
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
// 2. Remove untracked generated files (e.g., packs/ from prepack lifecycle scripts).
|
|
794
|
+
// These are created by sync-bundled-packs.mjs during pack --dry-run but may not
|
|
795
|
+
// be cleaned up if the release script fails before postpack runs.
|
|
796
|
+
for (const packageDir of packageDirs) {
|
|
797
|
+
const packsDir = join(packageDir, 'packs');
|
|
798
|
+
if (existsSync(packsDir)) {
|
|
799
|
+
rmSync(packsDir, { recursive: true, force: true });
|
|
800
|
+
const relativePath = packsDir.replace(`${process.cwd()}/`, '');
|
|
801
|
+
console.error(`${LOG_PREFIX} Removed generated ${relativePath}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
// 3. Restore all tracked files to HEAD state (dist symlinks + package.json versions).
|
|
805
|
+
// Safe because assertWorkingTreeClean verified the tree was clean before we started.
|
|
806
|
+
const git = getGitForCwd();
|
|
807
|
+
await git.raw(['checkout', '--', '.']);
|
|
808
|
+
// 4. Reinstall dependencies to restore workspace symlinks.
|
|
809
|
+
// WU-2063: git checkout -- . can change package.json files and dist symlinks,
|
|
810
|
+
// which invalidates pnpm's link state. Running pnpm install ensures
|
|
811
|
+
// workspace package links in node_modules/ are consistent.
|
|
812
|
+
console.error(`${LOG_PREFIX} Restoring workspace links...`);
|
|
813
|
+
execFileSync(PKG_MANAGER, ['install'], {
|
|
814
|
+
cwd: process.cwd(),
|
|
815
|
+
stdio: 'inherit',
|
|
816
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
817
|
+
});
|
|
818
|
+
console.error(`${LOG_PREFIX} ✅ Cleanup complete — main is clean`);
|
|
819
|
+
}
|
|
820
|
+
catch (cleanupError) {
|
|
821
|
+
const message = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
822
|
+
console.error(`${LOG_PREFIX} ⚠️ Automatic cleanup failed: ${message}`);
|
|
823
|
+
console.error(`${LOG_PREFIX} Manual recovery: git checkout -- . && git clean -fd`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
316
826
|
/**
|
|
317
827
|
* Main release function
|
|
318
828
|
* WU-1085: Renamed --version to --release-version to avoid conflict with CLI --version flag
|
|
@@ -345,13 +855,7 @@ export async function main() {
|
|
|
345
855
|
const git = getGitForCwd();
|
|
346
856
|
await ensureOnMain(git);
|
|
347
857
|
// Check for uncommitted changes
|
|
348
|
-
|
|
349
|
-
if (!isClean) {
|
|
350
|
-
die(`Working directory has uncommitted changes.\n\n` +
|
|
351
|
-
`Commit or stash changes before releasing:\n` +
|
|
352
|
-
` git status\n` +
|
|
353
|
-
` git stash # or git commit`);
|
|
354
|
-
}
|
|
858
|
+
await assertWorkingTreeClean(git, RELEASE_CLEAN_CHECK_PHASE_BEFORE_RELEASE);
|
|
355
859
|
// Find all @lumenflow/* packages to update
|
|
356
860
|
const packagePaths = findPackageJsonPaths();
|
|
357
861
|
if (packagePaths.length === 0) {
|
|
@@ -361,6 +865,7 @@ export async function main() {
|
|
|
361
865
|
for (const p of packagePaths) {
|
|
362
866
|
console.log(` - ${p.replace(process.cwd() + '/', '')}`);
|
|
363
867
|
}
|
|
868
|
+
const packageDirs = packagePaths.map((path) => dirname(path));
|
|
364
869
|
// Check npm authentication for publish
|
|
365
870
|
if (!skipPublish && !dryRun && !hasNpmAuth()) {
|
|
366
871
|
die(`npm authentication not found.\n\n` +
|
|
@@ -370,102 +875,145 @@ export async function main() {
|
|
|
370
875
|
`Get a token at: https://www.npmjs.com/settings/tokens\n` +
|
|
371
876
|
`Or use --skip-publish to only bump versions and create tag.`);
|
|
372
877
|
}
|
|
373
|
-
//
|
|
374
|
-
if (
|
|
375
|
-
console.log(`${LOG_PREFIX} Would bump versions to ${version} using micro-worktree isolation`);
|
|
376
|
-
console.log(`${LOG_PREFIX} Would commit: ${buildCommitMessage(version)}`);
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
console.log(`${LOG_PREFIX} Bumping versions using micro-worktree isolation...`);
|
|
380
|
-
// WU-1296: Use withReleaseEnv to set LUMENFLOW_WU_TOOL=release
|
|
381
|
-
// This allows the micro-worktree push to main without LUMENFLOW_FORCE
|
|
382
|
-
await withReleaseEnv(async () => {
|
|
383
|
-
await withMicroWorktree({
|
|
384
|
-
operation: OPERATION_NAME,
|
|
385
|
-
id: `v${version}`,
|
|
386
|
-
logPrefix: LOG_PREFIX,
|
|
387
|
-
execute: async ({ worktreePath }) => {
|
|
388
|
-
// Check and exit changeset pre mode if active
|
|
389
|
-
if (isInChangesetPreMode(worktreePath)) {
|
|
390
|
-
console.log(`${LOG_PREFIX} Detected changeset pre-release mode, exiting...`);
|
|
391
|
-
exitChangesetPreMode(worktreePath);
|
|
392
|
-
console.log(`${LOG_PREFIX} ✅ Exited changeset pre mode`);
|
|
393
|
-
}
|
|
394
|
-
// Find package paths within the worktree
|
|
395
|
-
const worktreePackagePaths = findPackageJsonPaths(worktreePath);
|
|
396
|
-
// Update versions
|
|
397
|
-
console.log(`${LOG_PREFIX} Updating ${worktreePackagePaths.length} package versions...`);
|
|
398
|
-
await updatePackageVersions(worktreePackagePaths, version);
|
|
399
|
-
// Get relative paths for commit
|
|
400
|
-
const relativePaths = worktreePackagePaths.map((p) => getRelativePath(p, worktreePath));
|
|
401
|
-
// If we exited pre mode, include the deleted pre.json in files to commit
|
|
402
|
-
// (the deletion will be staged automatically by git add -A behavior)
|
|
403
|
-
const _changesetPrePath = join(CHANGESET_DIR, CHANGESET_PRE_JSON);
|
|
404
|
-
const filesToCommit = [...relativePaths];
|
|
405
|
-
// Note: Deletion of pre.json is handled by git detecting the missing file
|
|
406
|
-
console.log(`${LOG_PREFIX} ✅ Versions updated to ${version}`);
|
|
407
|
-
return {
|
|
408
|
-
commitMessage: buildCommitMessage(version),
|
|
409
|
-
files: filesToCommit,
|
|
410
|
-
};
|
|
411
|
-
},
|
|
412
|
-
});
|
|
413
|
-
});
|
|
414
|
-
console.log(`${LOG_PREFIX} ✅ Version bump committed and pushed`);
|
|
415
|
-
}
|
|
416
|
-
// Build packages
|
|
417
|
-
if (!skipBuild) {
|
|
418
|
-
runCommand(`${PKG_MANAGER} build`, { dryRun, label: 'build' });
|
|
419
|
-
console.log(`${LOG_PREFIX} ✅ Build complete`);
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
console.log(`${LOG_PREFIX} Skipping build (--skip-build)`);
|
|
423
|
-
}
|
|
424
|
-
// Create git tag
|
|
425
|
-
const tagName = buildTagName(version);
|
|
426
|
-
if (dryRun) {
|
|
427
|
-
console.log(`${LOG_PREFIX} Would create tag: ${tagName}`);
|
|
428
|
-
console.log(`${LOG_PREFIX} Would push tag to ${REMOTES.ORIGIN}`);
|
|
429
|
-
}
|
|
430
|
-
else {
|
|
431
|
-
console.log(`${LOG_PREFIX} Creating tag ${tagName}...`);
|
|
432
|
-
await git.raw(['tag', '-a', tagName, '-m', `Release ${tagName}`]);
|
|
433
|
-
console.log(`${LOG_PREFIX} ✅ Tag created: ${tagName}`);
|
|
434
|
-
console.log(`${LOG_PREFIX} Pushing tag to ${REMOTES.ORIGIN}...`);
|
|
435
|
-
await pushTagWithForce(git, tagName, 'release: pushing version tag');
|
|
436
|
-
console.log(`${LOG_PREFIX} ✅ Tag pushed`);
|
|
437
|
-
}
|
|
438
|
-
// Publish to npm
|
|
439
|
-
if (!skipPublish) {
|
|
878
|
+
// Exit changeset pre mode if active
|
|
879
|
+
if (isInChangesetPreMode()) {
|
|
440
880
|
if (dryRun) {
|
|
441
|
-
console.log(`${LOG_PREFIX} Would
|
|
881
|
+
console.log(`${LOG_PREFIX} Would exit changeset pre-release mode`);
|
|
442
882
|
}
|
|
443
883
|
else {
|
|
444
|
-
console.log(`${LOG_PREFIX}
|
|
445
|
-
|
|
446
|
-
console.log(`${LOG_PREFIX} ✅
|
|
884
|
+
console.log(`${LOG_PREFIX} Detected changeset pre-release mode, exiting...`);
|
|
885
|
+
exitChangesetPreMode();
|
|
886
|
+
console.log(`${LOG_PREFIX} ✅ Exited changeset pre mode`);
|
|
447
887
|
}
|
|
448
888
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
//
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
889
|
+
// WU-2062: Wrap all release phases in try/finally to ensure main is never
|
|
890
|
+
// left dirty. If anything fails before npm publish, cleanup restores main
|
|
891
|
+
// to its pre-release state. If npm publish succeeded but git push failed,
|
|
892
|
+
// we print manual recovery instructions instead of undoing the publish.
|
|
893
|
+
let npmPublished = false;
|
|
894
|
+
let releaseComplete = false;
|
|
895
|
+
try {
|
|
896
|
+
// ── Phase 1: Build & Validate (no writes to origin) ────────────────
|
|
897
|
+
// WU-2061: Build and validate BEFORE bumping versions or pushing anything.
|
|
898
|
+
// If any step here fails, main is untouched.
|
|
899
|
+
// Materialize symlinked dist directories for reliable pack/publish
|
|
900
|
+
const distPreparation = ensureDistPathsMaterialized(packageDirs, { skipBuild, dryRun });
|
|
901
|
+
if (distPreparation.checkedCount > 0 && !dryRun && distPreparation.materializedCount > 0) {
|
|
902
|
+
console.log(`${LOG_PREFIX} [${PRE_FLIGHT_LABEL}] Materialized ${distPreparation.materializedCount} symlinked dist directories`);
|
|
903
|
+
}
|
|
904
|
+
if (!skipBuild) {
|
|
905
|
+
runCommand(`${PKG_MANAGER} build`, { dryRun, label: 'build' });
|
|
906
|
+
console.log(`${LOG_PREFIX} ✅ Build complete`);
|
|
907
|
+
}
|
|
908
|
+
else {
|
|
909
|
+
console.log(`${LOG_PREFIX} Skipping build (--skip-build)`);
|
|
910
|
+
}
|
|
911
|
+
if (!skipPublish) {
|
|
912
|
+
validateReleaseArtifactsForPublish(packagePaths, Boolean(dryRun));
|
|
913
|
+
}
|
|
914
|
+
else if (dryRun) {
|
|
915
|
+
console.log(`${LOG_PREFIX} [${PACK_VALIDATE_LABEL}] Would skip artifact validation (--skip-publish)`);
|
|
916
|
+
}
|
|
917
|
+
// ── Phase 2: Version bump (local only, no push yet) ────────────────
|
|
918
|
+
// WU-2061: Bump versions locally on main. No micro-worktree needed — if
|
|
919
|
+
// anything fails after this, cleanup restores package.json files.
|
|
920
|
+
console.log(`${LOG_PREFIX} Bumping versions to ${version}...`);
|
|
921
|
+
if (dryRun) {
|
|
922
|
+
console.log(`${LOG_PREFIX} Would update ${packagePaths.length} package versions`);
|
|
923
|
+
console.log(`${LOG_PREFIX} Would commit: ${buildCommitMessage(version)}`);
|
|
924
|
+
}
|
|
925
|
+
else {
|
|
926
|
+
await updatePackageVersions(packagePaths, version);
|
|
927
|
+
console.log(`${LOG_PREFIX} ✅ Versions updated to ${version}`);
|
|
928
|
+
}
|
|
929
|
+
// ── Phase 3: Publish to npm ────────────────────────────────────────
|
|
930
|
+
// Publish BEFORE committing to git. If publish fails, cleanup restores
|
|
931
|
+
// package.json files and main stays clean.
|
|
932
|
+
if (!skipPublish) {
|
|
933
|
+
if (dryRun) {
|
|
934
|
+
console.log(`${LOG_PREFIX} Would publish packages to npm`);
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
console.log(`${LOG_PREFIX} Publishing packages to npm...`);
|
|
938
|
+
runCommand(`${PKG_MANAGER} -r publish --access public --no-git-checks`, {
|
|
939
|
+
label: 'publish',
|
|
940
|
+
});
|
|
941
|
+
npmPublished = true;
|
|
942
|
+
console.log(`${LOG_PREFIX} ✅ Packages published to npm`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
console.log(`${LOG_PREFIX} Skipping npm publish (--skip-publish)`);
|
|
947
|
+
}
|
|
948
|
+
// ── Phase 4: Commit, tag, push (only after everything succeeds) ────
|
|
949
|
+
// WU-2061: This is the point of no return. Build passed, validation
|
|
950
|
+
// passed, npm publish succeeded. Now commit the version bump to main.
|
|
951
|
+
const tagName = buildTagName(version);
|
|
952
|
+
if (dryRun) {
|
|
953
|
+
console.log(`${LOG_PREFIX} Would commit: ${buildCommitMessage(version)}`);
|
|
954
|
+
console.log(`${LOG_PREFIX} Would create tag: ${tagName}`);
|
|
955
|
+
console.log(`${LOG_PREFIX} Would push to ${REMOTES.ORIGIN}`);
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
// Format changed files before committing
|
|
959
|
+
const relativePaths = packagePaths.map((p) => p.replace(process.cwd() + '/', ''));
|
|
960
|
+
runCommand(`${PKG_MANAGER} prettier --write ${relativePaths.join(' ')}`, {
|
|
961
|
+
label: 'format',
|
|
962
|
+
});
|
|
963
|
+
// Stage and commit
|
|
964
|
+
await git.add(relativePaths);
|
|
965
|
+
await git.commit(buildCommitMessage(version));
|
|
966
|
+
console.log(`${LOG_PREFIX} ✅ Committed: ${buildCommitMessage(version)}`);
|
|
967
|
+
// Create and push tag
|
|
968
|
+
console.log(`${LOG_PREFIX} Creating tag ${tagName}...`);
|
|
969
|
+
await git.raw(['tag', '-a', tagName, '-m', `Release ${tagName}`]);
|
|
970
|
+
console.log(`${LOG_PREFIX} ✅ Tag created: ${tagName}`);
|
|
971
|
+
// Push commit + tag together
|
|
972
|
+
console.log(`${LOG_PREFIX} Pushing to ${REMOTES.ORIGIN}...`);
|
|
973
|
+
await withReleaseEnv(async () => {
|
|
974
|
+
await git.push(REMOTES.ORIGIN, 'main');
|
|
975
|
+
await git.push(REMOTES.ORIGIN, tagName);
|
|
976
|
+
});
|
|
977
|
+
console.log(`${LOG_PREFIX} ✅ Pushed to ${REMOTES.ORIGIN}`);
|
|
978
|
+
}
|
|
979
|
+
releaseComplete = true;
|
|
980
|
+
// Summary
|
|
981
|
+
console.log(`\n${LOG_PREFIX} 🎉 Release complete!`);
|
|
982
|
+
console.log(`${LOG_PREFIX} Version: ${version}`);
|
|
983
|
+
console.log(`${LOG_PREFIX} Tag: ${tagName}`);
|
|
984
|
+
if (!skipPublish && !dryRun) {
|
|
985
|
+
console.log(`${LOG_PREFIX} npm: https://www.npmjs.com/org/lumenflow`);
|
|
986
|
+
}
|
|
987
|
+
console.log(`\n${LOG_PREFIX} Next steps:`);
|
|
988
|
+
if (dryRun) {
|
|
989
|
+
console.log(` - Run without --dry-run to execute the release`);
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
console.log(` - Create GitHub release: gh release create ${tagName} --title "Release ${tagName}"`);
|
|
993
|
+
if (skipPublish) {
|
|
994
|
+
console.log(` - Publish to npm: ${PKG_MANAGER} -r publish --access public --no-git-checks`);
|
|
995
|
+
}
|
|
996
|
+
console.log(` - Verify packages: npm view @lumenflow/cli version`);
|
|
997
|
+
}
|
|
462
998
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
|
|
999
|
+
finally {
|
|
1000
|
+
// WU-2062: Cleanup on failure — never leave main dirty
|
|
1001
|
+
if (!releaseComplete && !dryRun) {
|
|
1002
|
+
if (!npmPublished) {
|
|
1003
|
+
// Nothing reached npm — safe to fully restore main
|
|
1004
|
+
await cleanupFailedRelease(packageDirs);
|
|
1005
|
+
}
|
|
1006
|
+
else {
|
|
1007
|
+
// npm packages are published but git commit/push failed.
|
|
1008
|
+
// Do NOT undo the version bump — the published packages reference this version.
|
|
1009
|
+
console.error(`\n${LOG_PREFIX} ⚠️ npm publish succeeded but git commit/push failed.`);
|
|
1010
|
+
console.error(`${LOG_PREFIX} The version bump is still in your working tree.`);
|
|
1011
|
+
console.error(`${LOG_PREFIX} To complete manually:`);
|
|
1012
|
+
console.error(`${LOG_PREFIX} git add -A && git commit -m "${buildCommitMessage(version)}"`);
|
|
1013
|
+
console.error(`${LOG_PREFIX} git tag -a ${buildTagName(version)} -m "Release ${buildTagName(version)}"`);
|
|
1014
|
+
console.error(`${LOG_PREFIX} LUMENFLOW_WU_TOOL=release git push origin main ${buildTagName(version)}`);
|
|
1015
|
+
}
|
|
467
1016
|
}
|
|
468
|
-
console.log(` - Verify packages: npm view @lumenflow/cli version`);
|
|
469
1017
|
}
|
|
470
1018
|
}
|
|
471
1019
|
// Guard main() for testability
|