#!/usr/bin/env node
// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * LumenFlow Upgrade CLI Command
 *
 * Updates all @lumenflow/* packages to a specified version or latest.
 * Uses micro-worktree pattern for atomic changes to main without requiring
 * users to be in a worktree.
 *
 * WU-1112: INIT-003 Phase 6 - Migrate remaining Tier 1 tools
 * WU-1127: Use micro-worktree isolation pattern (fixes user blocking issue)
 *
 * Key requirements:
 * - Uses micro-worktree pattern (atomic changes, no user worktree needed)
 * - Runs from main checkout (not inside a worktree)
 * - Checks all 7 @lumenflow/* packages
 *
 * Usage:
 *   pnpm lumenflow:upgrade --version 1.5.0
 *   pnpm lumenflow:upgrade --latest
 *   pnpm lumenflow:upgrade --latest --dry-run
 *   pnpm lumenflow:upgrade --force      # re-run sync flow on the same version
 */
import { createHash } from 'node:crypto';
import { execSync, execFileSync } from 'node:child_process';
import { existsSync, readFileSync, mkdtempSync, writeFileSync, rmSync, mkdirSync, readdirSync, } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { STDIO_MODES, EXIT_CODES, PKG_MANAGER, PKG_COMMANDS, PKG_FLAGS, BRANCHES, FILE_EXTENSIONS, LUMENFLOW_PATHS, } from '@lumenflow/core/wu-constants';
import { getGitForCwd } from '@lumenflow/core/git-adapter';
import { withMicroWorktree } from '@lumenflow/core/micro-worktree';
import YAML from 'yaml';
import { WORKSPACE_CONFIG_FILE_NAME } from '@lumenflow/core/config';
import { runCLI } from './cli-entry-point.js';
import { generateScriptsFromManifest } from './public-manifest.js';
import { loadTemplate, processTemplate, CORE_DOC_TEMPLATE_PATHS, buildCoreDocTokens, executeDocsSyncInDir, } from './docs-sync.js';
/** Log prefix for console output */
const LOG_PREFIX = '[lumenflow:upgrade]';
/** Operation name for micro-worktree */
const OPERATION_NAME = 'lumenflow-upgrade';
/** CLI argument names */
const ARG_HELP = '--help';
const ARG_HELP_SHORT = '-h';
const ARG_VERSION = '--version';
const ARG_VERSION_SHORT = '-v';
const ARG_LATEST = '--latest';
const ARG_LATEST_SHORT = '-l';
const ARG_DRY_RUN = '--dry-run';
const ARG_DRY_RUN_SHORT = '-n';
const ARG_NO_BOOTSTRAP = '--no-bootstrap';
const ARG_FORCE = '--force';
const ARG_FORCE_SHORT = '-f';
const PNPM_WORKSPACE_FILE = 'pnpm-workspace.yaml';
const UPGRADE_MARKER_FILENAME = `lumenflow-upgrade-marker${FILE_EXTENSIONS.JSON}`;
export const UPGRADE_MARKER_RELATIVE_PATH = path.posix.join(LUMENFLOW_PATHS.STATE_DIR, UPGRADE_MARKER_FILENAME);
/**
 * All @lumenflow/* packages that should be upgraded together
 *
 * WU-1112: Must include all 7 packages (not just 4 as before)
 * Kept in alphabetical order for consistency
 */
export const LUMENFLOW_PACKAGES = [
    '@lumenflow/agent',
    '@lumenflow/cli',
    '@lumenflow/core',
    '@lumenflow/initiatives',
    '@lumenflow/memory',
    '@lumenflow/metrics',
    '@lumenflow/shims',
];
export function createUpgradeMarker(version) {
    return {
        kind: 'lumenflow-upgrade',
        status: 'pending',
        version,
        created_at: new Date().toISOString(),
    };
}
function writeUpgradeMarker(worktreePath, version) {
    const markerPath = path.join(worktreePath, UPGRADE_MARKER_RELATIVE_PATH);
    mkdirSync(path.dirname(markerPath), { recursive: true });
    writeFileSync(markerPath, JSON.stringify(createUpgradeMarker(version), null, 2) + '\n', 'utf-8');
}
/**
 * Detect whether a project is a pnpm workspace root.
 */
export function isWorkspaceRoot(cwd = process.cwd()) {
    return existsSync(path.join(cwd, PNPM_WORKSPACE_FILE));
}
/**
 * Parse command line arguments for lumenflow-upgrade
 *
 * @param argv - Process argv array
 * @returns Parsed arguments
 */
export function parseUpgradeArgs(argv) {
    const args = {};
    // Skip node and script name
    const cliArgs = argv.slice(2);
    for (let i = 0; i < cliArgs.length; i++) {
        const arg = cliArgs[i];
        if (arg === ARG_HELP || arg === ARG_HELP_SHORT) {
            args.help = true;
        }
        else if (arg === ARG_VERSION || arg === ARG_VERSION_SHORT) {
            args.version = cliArgs[++i];
        }
        else if (arg === ARG_LATEST || arg === ARG_LATEST_SHORT) {
            args.latest = true;
        }
        else if (arg === ARG_DRY_RUN || arg === ARG_DRY_RUN_SHORT) {
            args.dryRun = true;
        }
        else if (arg === ARG_NO_BOOTSTRAP) {
            args.noBootstrap = true;
        }
        else if (arg === ARG_FORCE || arg === ARG_FORCE_SHORT) {
            args.force = true;
        }
    }
    return args;
}
/**
 * Build the upgrade commands based on arguments
 *
 * Creates pnpm add command for all @lumenflow/* packages.
 * Uses --save-dev since these are development dependencies.
 *
 * @param args - Parsed upgrade arguments
 * @returns Object containing the commands to run
 */
export function buildUpgradeCommands(args, options = {}) {
    // Determine version specifier
    const versionSpec = args.latest ? 'latest' : args.version || 'latest';
    // Build package list with version
    const packages = LUMENFLOW_PACKAGES.map((pkg) => `${pkg}@${versionSpec}`);
    // Build pnpm add command using array pattern (matches deps-add.ts convention).
    // -w is required in workspace roots but fails in single-package repos.
    const parts = [PKG_MANAGER, PKG_COMMANDS.ADD, PKG_FLAGS.SAVE_DEV];
    const shouldUseWorkspaceRootFlag = isWorkspaceRoot(options.cwd ?? process.cwd());
    if (shouldUseWorkspaceRootFlag) {
        parts.push(PKG_FLAGS.WORKSPACE_ROOT);
    }
    parts.push(...packages);
    return {
        addCommand: parts.join(' '),
        versionSpec,
    };
}
/**
 * WU-2087: Get the installed @lumenflow/cli version from its package.json.
 *
 * Uses import.meta.resolve to locate the package.json relative to this module,
 * which works correctly regardless of whether we're in the main checkout or a
 * micro-worktree, and handles both symlinked and real dist directories.
 *
 * @returns Semver version string (e.g., '3.2.1') or null if not determinable
 */
export function getInstalledCliVersion() {
    // Strategy 1: Resolve relative to this file (works in dev monorepo and tests)
    try {
        const thisDir = path.dirname(new URL(import.meta.url).pathname);
        // In dist: packages/@lumenflow/cli/dist/ → ../package.json
        // In src:  packages/@lumenflow/cli/src/  → ../package.json
        const localPkg = path.join(thisDir, '..', 'package.json');
        if (existsSync(localPkg)) {
            const pkg = JSON.parse(readFileSync(localPkg, 'utf8'));
            if (pkg.version)
                return pkg.version;
        }
    }
    catch {
        // Fall through to strategy 2
    }
    // Strategy 2: Use import.meta.resolve (works for installed npm packages)
    try {
        const pkgUrl = import.meta.resolve('@lumenflow/cli/package.json');
        const pkgPath = pkgUrl.startsWith('file://') ? new URL(pkgUrl).pathname : pkgUrl;
        const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
        return pkg.version || null;
    }
    catch {
        return null;
    }
}
/**
 * WU-2087: Resolve the concrete target version for the upgrade.
 *
 * - For --version X: returns X directly (no network call)
 * - For --latest: queries the npm registry via `npm view` to resolve the
 *   current latest tag to a concrete semver version
 * - Returns null on failure (offline, registry error) — callers should
 *   degrade gracefully and proceed with the installed version
 *
 * @param args - Parsed upgrade arguments
 * @returns Concrete semver string or null
 */
export async function resolveTargetVersion(args) {
    if (args.version)
        return args.version;
    if (args.latest) {
        try {
            // execFileSync avoids shell injection — all args are array elements
            // eslint-disable-next-line sonarjs/no-os-command-from-path -- npm is a system binary
            const result = execFileSync('npm', ['view', '@lumenflow/cli', 'version'], {
                encoding: 'utf8',
                stdio: ['pipe', 'pipe', 'pipe'],
            }).trim();
            return result || null;
        }
        catch {
            return null;
        }
    }
    return null;
}
export function shouldShortCircuitUpgrade(installedVersion, targetVersion, args) {
    if (args.force) {
        return { shortCircuit: false };
    }
    if (!installedVersion || !targetVersion) {
        return { shortCircuit: false };
    }
    if (installedVersion === targetVersion) {
        return { shortCircuit: true, version: installedVersion };
    }
    return { shortCircuit: false };
}
/**
 * WU-2087: Build the command to delegate to the target version's upgrade script.
 *
 * Creates a { script, args } pair that runs the target version's
 * lumenflow-upgrade.js from a temp bootstrap directory.
 *
 * The args are constructed by:
 * 1. Dropping 'node' and the script path from original argv
 * 2. Replacing --latest with --version <resolved> (avoids re-resolving)
 * 3. Adding --no-bootstrap to prevent infinite recursion
 *
 * @param bootstrapDir - Temp dir where target CLI was npm-installed
 * @param targetVersion - Resolved concrete version (e.g., '3.5.0')
 * @param originalArgv - process.argv from the current invocation
 * @returns { script, args } for execFileSync
 */
export function buildBootstrapCommand(bootstrapDir, targetVersion, originalArgv) {
    const script = path.join(bootstrapDir, 'node_modules', '@lumenflow', 'cli', 'dist', 'lumenflow-upgrade.js');
    // Forward original CLI args, replacing --latest with --version <resolved>
    const cliArgs = originalArgv.slice(2).filter((a) => a !== ARG_NO_BOOTSTRAP);
    const args = [];
    for (let i = 0; i < cliArgs.length; i++) {
        const arg = cliArgs[i];
        if (arg === ARG_LATEST || arg === ARG_LATEST_SHORT) {
            // Replace --latest with --version <resolved> so the target script
            // doesn't need to re-resolve from the registry
            args.push(ARG_VERSION, targetVersion);
        }
        else {
            args.push(arg);
        }
    }
    args.push(ARG_NO_BOOTSTRAP);
    return { script, args };
}
/**
 * WU-2087: Self-bootstrap by installing the target version in a temp dir
 * and delegating the upgrade to it.
 *
 * This solves the chicken-and-egg problem: when the installed version has
 * bugs (e.g., unconditional -w flag), the target version's script runs
 * instead. Node ESM resolves imports relative to the script's file location,
 * so @lumenflow/core etc. are loaded from the temp dir's node_modules —
 * NOT from the consumer's stale node_modules.
 *
 * The temp dir is always cleaned up, even on failure.
 *
 * @param targetVersion - Concrete version to bootstrap (e.g., '3.5.0')
 * @param originalArgv - process.argv to forward (with --no-bootstrap added)
 */
export function selfBootstrap(targetVersion, originalArgv) {
    const bootstrapDir = mkdtempSync(path.join(tmpdir(), 'lf-upgrade-bootstrap-'));
    try {
        console.log(`${LOG_PREFIX} Self-bootstrapping to @lumenflow/cli@${targetVersion}...`);
        // Create minimal package.json so npm install works
        writeFileSync(path.join(bootstrapDir, 'package.json'), JSON.stringify({ name: 'lf-bootstrap', private: true }));
        // Install target version — execFileSync prevents shell injection
        console.log(`${LOG_PREFIX} Installing @lumenflow/cli@${targetVersion} in temp environment...`);
        // eslint-disable-next-line sonarjs/no-os-command-from-path -- npm is a system binary
        execFileSync('npm', ['install', '--no-save', `@lumenflow/cli@${targetVersion}`], {
            cwd: bootstrapDir,
            stdio: STDIO_MODES.PIPE,
        });
        // Build and run the target version's upgrade script
        const { script, args } = buildBootstrapCommand(bootstrapDir, targetVersion, originalArgv);
        console.log(`${LOG_PREFIX} Delegating to target version's upgrade script...`);
        // eslint-disable-next-line sonarjs/no-os-command-from-path -- node is a system binary
        execFileSync('node', [script, ...args], {
            cwd: process.cwd(),
            stdio: STDIO_MODES.INHERIT,
        });
    }
    finally {
        rmSync(bootstrapDir, { recursive: true, force: true });
    }
}
/**
 * WU-2572: Normalize a `git rev-parse --git-dir` / `--git-common-dir` value
 * for equality comparison.
 *
 * - Trim trailing whitespace from the git raw output
 * - Resolve relative paths against the current working directory so a main
 *   checkout reporting `.git` compares equal to an absolute path reporting
 *   `/abs/path/to/.git`
 */
function resolveGitDir(raw) {
    const trimmed = raw.trim();
    return path.isAbsolute(trimmed) ? trimmed : path.resolve(process.cwd(), trimmed);
}
/**
 * WU-1127: Validate that the command is run from main checkout
 *
 * The micro-worktree pattern requires the command to be run from the main
 * checkout (not inside a worktree). This is the inverse of the old behavior
 * which required users to be IN a worktree.
 *
 * WU-2572: Worktree detection previously used `cwd.includes('/worktrees/')`,
 * a fragile substring check that false-positived on any repo whose path
 * contained the literal substring `/worktrees/` (nested clones, build
 * caches, downstream consumer projects with a sibling `worktrees` directory
 * above the repo root, etc.). Replaced with a git-native detection that
 * compares `--git-dir` against `--git-common-dir`. In a linked git worktree
 * the former resolves to `.git/worktrees/<name>` while the latter resolves
 * to the main repo's `.git` — two different paths. In a main checkout both
 * resolve to the same `.git` directory. This check is path-independent and
 * works the same regardless of where the consumer repo is cloned.
 *
 * @returns Validation result with error and fix command if invalid
 */
export async function validateMainCheckout() {
    // Check if we're inside a linked git worktree using git's own metadata.
    // Fail-open on git errors: if git is unavailable or the cwd is not a git
    // repo at all, downstream checks will produce a clearer error than this
    // one would.
    try {
        const git = getGitForCwd();
        const [gitDir, gitCommonDir] = await Promise.all([
            git.raw(['rev-parse', '--git-dir']),
            git.raw(['rev-parse', '--git-common-dir']),
        ]);
        if (resolveGitDir(gitDir) !== resolveGitDir(gitCommonDir)) {
            return {
                valid: false,
                error: `Cannot run lumenflow:upgrade from inside a linked git worktree.\n\n` +
                    `This command must be run from the main checkout because it uses\n` +
                    `micro-worktree isolation to atomically update package.json and lockfile.`,
                fixCommand: `cd to main checkout and re-run:\n  cd <main-checkout>\n  pnpm lumenflow:upgrade --latest`,
            };
        }
    }
    catch {
        // Git failures here are non-fatal: either not a git repo, or git is
        // unavailable. Let the downstream branch check produce the authoritative
        // error message if the environment is actually broken.
    }
    // Check if we're on main branch
    try {
        const git = getGitForCwd();
        const currentBranch = await git.raw(['rev-parse', '--abbrev-ref', 'HEAD']);
        const branchName = currentBranch.trim();
        if (branchName !== BRANCHES.MAIN) {
            return {
                valid: false,
                error: `lumenflow:upgrade must be run from main checkout (on main branch).\n\n` +
                    `Current branch: ${branchName}\n` +
                    `Expected branch: main`,
                fixCommand: `Switch to main branch:\n  git checkout main\n  pnpm lumenflow:upgrade --latest`,
            };
        }
    }
    catch (error) {
        // If git fails, assume we're not in a valid git repo
        return {
            valid: false,
            error: `Failed to detect git branch. Ensure you're in a git repository.`,
        };
    }
    return { valid: true };
}
/**
 * WU-2226: Sync pnpm script entries from the CLI public-manifest into
 * the consumer project's package.json.
 *
 * Adds missing scripts without overwriting or removing existing entries.
 * Uses the same script generation logic as `lumenflow init` to ensure
 * consistency between init-scaffolded and upgrade-synced scripts.
 *
 * @param dir - Directory containing the package.json to update
 * @returns ScriptSyncResult indicating what was changed
 */
export function syncScriptsToPackageJson(dir) {
    const pkgPath = path.join(dir, 'package.json');
    if (!existsSync(pkgPath)) {
        return { added: [], modified: false };
    }
    const content = readFileSync(pkgPath, 'utf-8');
    const packageJson = JSON.parse(content);
    // Ensure scripts object exists
    if (!packageJson.scripts || typeof packageJson.scripts !== 'object') {
        packageJson.scripts = {};
    }
    const scripts = packageJson.scripts;
    const manifestScripts = generateScriptsFromManifest();
    const added = [];
    for (const [name, command] of Object.entries(manifestScripts)) {
        if (!(name in scripts)) {
            scripts[name] = command;
            added.push(name);
        }
    }
    const modified = added.length > 0;
    if (modified) {
        writeFileSync(pkgPath, JSON.stringify(packageJson, null, 2) + '\n');
    }
    return { added, modified };
}
/**
 * WU-2438: Built-in SD pack pin for pre-pack project auto-registration.
 */
const BUILTIN_SD_PACK_ID = 'software-delivery';
/**
 * WU-2438: Ensure built-in packs are registered in workspace.yaml.
 *
 * Pre-pack projects (initialized before the pack system) have `packs: []` but
 * use `software_delivery` config. This function adds the SD pack pin if missing,
 * so `config:set` can resolve `software_delivery` via the normal pack routing.
 *
 * Pure function: takes workspace data, returns modified data + what was added.
 */
export function ensureBuiltinPacksRegistered(workspace, versionSpec) {
    const packs = workspace.packs;
    if (!Array.isArray(packs)) {
        workspace.packs = [];
    }
    const packsArray = workspace.packs;
    const added = [];
    // Add SD pack if workspace has software_delivery but no SD pack pin
    const hasSDPack = packsArray.some((p) => p.id === BUILTIN_SD_PACK_ID);
    if (!hasSDPack && 'software_delivery' in workspace) {
        packsArray.push({
            id: BUILTIN_SD_PACK_ID,
            version: versionSpec,
            integrity: 'dev',
            source: 'local',
        });
        added.push(BUILTIN_SD_PACK_ID);
    }
    return { added, modified: added.length > 0 };
}
/**
 * WU-1127: Execute the upgrade in a micro-worktree
 *
 * Uses the shared micro-worktree pattern (like wu:create, wu:edit) to:
 * 1. Create a temporary worktree without switching main checkout
 * 2. Run pnpm add in the temporary worktree
 * 3. Commit the changes
 * 4. FF-only merge to main
 * 5. Push to origin
 * 6. Cleanup
 *
 * @param args - Parsed upgrade arguments
 * @returns Promise resolving when upgrade is complete
 */
export async function executeUpgradeInMicroWorktree(args) {
    const { addCommand, versionSpec } = buildUpgradeCommands(args);
    // Generate unique ID for this upgrade operation using timestamp
    const upgradeId = `upgrade-${Date.now()}`;
    console.log(`${LOG_PREFIX} Using micro-worktree isolation (WU-1127)`);
    console.log(`${LOG_PREFIX} Upgrading @lumenflow/* packages to ${versionSpec}`);
    console.log(`${LOG_PREFIX} Packages: ${LUMENFLOW_PACKAGES.length} packages`);
    await withMicroWorktree({
        operation: OPERATION_NAME,
        id: upgradeId,
        logPrefix: LOG_PREFIX,
        execute: async ({ worktreePath }) => {
            console.log(`${LOG_PREFIX} Running: ${addCommand}`);
            // Execute pnpm add in the micro-worktree
            execSync(addCommand, {
                stdio: STDIO_MODES.INHERIT,
                cwd: worktreePath,
            });
            console.log(`${LOG_PREFIX} Package installation complete`);
            // WU-2226: Sync missing pnpm script entries from the public manifest.
            // New CLI versions may introduce new commands (e.g., wu:escalate) that
            // need corresponding script entries in the consumer's package.json.
            const scriptSync = syncScriptsToPackageJson(worktreePath);
            if (scriptSync.modified) {
                console.log(`${LOG_PREFIX} Added ${scriptSync.added.length} new script entries: ${scriptSync.added.join(', ')}`);
            }
            else {
                console.log(`${LOG_PREFIX} All script entries already present`);
            }
            // WU-2438: Auto-register built-in packs for pre-pack projects.
            // Projects initialized before the pack system have packs: [] but use
            // software_delivery config. Register the SD pack so config:set works.
            const wsPath = path.join(worktreePath, WORKSPACE_CONFIG_FILE_NAME);
            if (existsSync(wsPath)) {
                const wsContent = readFileSync(wsPath, 'utf-8');
                const wsData = YAML.parse(wsContent);
                const packReg = ensureBuiltinPacksRegistered(wsData, versionSpec);
                if (packReg.modified) {
                    writeFileSync(wsPath, YAML.stringify(wsData, { lineWidth: 120 }), 'utf-8');
                    console.log(`${LOG_PREFIX} Auto-registered built-in pack(s): ${packReg.added.join(', ')}`);
                }
            }
            // Write marker consumed by lumenflow:pre-commit-check when @lumenflow versions change.
            writeUpgradeMarker(worktreePath, versionSpec);
            // WU-2373: Sync core docs inside the upgrade transaction instead of
            // advising users to run docs:sync separately after upgrade.
            // WU-2383: force=false — syncCoreDocs handles managed vs bootstrap docs
            // internally. Managed docs (LUMENFLOW.md, constraints.md) are always
            // written. Bootstrap docs (AGENTS.md) use merge-block markers.
            // Onboarding docs and skills use their own force/allowlist logic.
            console.log(`${LOG_PREFIX} Syncing core docs...`);
            const docsSyncResult = await executeDocsSyncInDir(worktreePath, {
                force: false,
                refreshManagedOnboarding: true,
            });
            if (docsSyncResult.created.length > 0) {
                console.log(`${LOG_PREFIX} Synced ${docsSyncResult.created.length} doc file(s)`);
            }
            // Return files to stage and commit message
            return {
                commitMessage: `chore: upgrade @lumenflow packages to ${versionSpec}`,
                files: [
                    'package.json',
                    'pnpm-lock.yaml',
                    WORKSPACE_CONFIG_FILE_NAME,
                    UPGRADE_MARKER_RELATIVE_PATH,
                    ...docsSyncResult.allFiles,
                ],
            };
        },
    });
    // WU-1622: Sync main checkout's node_modules after merge.
    // The micro-worktree updated package.json + lockfile and merged to main,
    // but main's node_modules still has the old packages. Without this step,
    // git hooks (pre-push, pre-commit) that import from @lumenflow/* would
    // crash because they resolve from the stale node_modules.
    // Note: execSync is safe here — no user input in the command string.
    console.log(`${LOG_PREFIX} Syncing node_modules with updated lockfile...`);
    execSync(`${PKG_MANAGER} install --frozen-lockfile`, {
        stdio: STDIO_MODES.INHERIT,
    });
    console.log(`${LOG_PREFIX} ✅ node_modules synced`);
    console.log(`\n${LOG_PREFIX} Upgrade complete!`);
    console.log(`${LOG_PREFIX} Upgraded to ${versionSpec}`);
    console.log(`\n${LOG_PREFIX} Next steps:`);
    // WU-2567: Pick the right rebuild command for the consumer's language/tooling.
    // Hardcoded 'pnpm build' broke every non-JS consumer in v4.12.0.
    const build = resolvePostUpgradeBuildCommand(process.cwd());
    if (build.source === 'workspace') {
        console.log(`  1. Run '${build.instruction}' (from workspace.yaml build_command)`);
    }
    else if (build.source === 'language-detected') {
        console.log(`  1. Run '${build.instruction}' (detected project stack)`);
    }
    else if (build.source === 'package-json') {
        console.log(`  1. Run '${build.instruction}' to rebuild with new versions`);
    }
    else {
        console.log(`  1. ${build.instruction}`);
    }
    console.log(`  2. Run 'pnpm gates' to verify everything works`);
}
const PACKAGE_MANAGER_BUILD_COMMANDS = {
    pnpm: 'pnpm build',
    npm: 'npm run build',
    yarn: 'yarn build',
    bun: 'bun run build',
};
const NON_NODE_PRESETS = new Set([
    'dotnet',
    'go',
    'rust',
    'java',
    'php',
    'python',
    'ruby',
]);
const DETECTION_IGNORED_DIRECTORIES = new Set([
    '.git',
    '.idea',
    '.next',
    '.turbo',
    '.venv',
    '.yarn',
    'bin',
    'build',
    'coverage',
    'dist',
    'node_modules',
    'obj',
    'target',
    'vendor',
]);
function normalizePackageManager(value) {
    if (typeof value !== 'string') {
        return null;
    }
    const trimmed = value.trim().toLowerCase();
    if (trimmed === 'pnpm' || trimmed.startsWith('pnpm@')) {
        return 'pnpm';
    }
    if (trimmed === 'npm' || trimmed.startsWith('npm@')) {
        return 'npm';
    }
    if (trimmed === 'yarn' || trimmed.startsWith('yarn@')) {
        return 'yarn';
    }
    if (trimmed === 'bun' || trimmed.startsWith('bun@')) {
        return 'bun';
    }
    return null;
}
function readWorkspaceBuildCommand(projectDir) {
    const wsPath = path.join(projectDir, WORKSPACE_CONFIG_FILE_NAME);
    if (!existsSync(wsPath)) {
        return null;
    }
    let parsed;
    try {
        parsed = YAML.parse(readFileSync(wsPath, 'utf-8'));
    }
    catch {
        return null;
    }
    if (!parsed || typeof parsed !== 'object') {
        return null;
    }
    const wsObj = parsed;
    const cmd = wsObj.software_delivery?.build_command;
    if (typeof cmd !== 'string') {
        return null;
    }
    const trimmed = cmd.trim();
    return trimmed.length > 0 ? trimmed : null;
}
function readWorkspacePackageManager(projectDir) {
    const wsPath = path.join(projectDir, WORKSPACE_CONFIG_FILE_NAME);
    if (!existsSync(wsPath)) {
        return null;
    }
    let parsed;
    try {
        parsed = YAML.parse(readFileSync(wsPath, 'utf-8'));
    }
    catch {
        return null;
    }
    if (!parsed || typeof parsed !== 'object') {
        return null;
    }
    const wsObj = parsed;
    return normalizePackageManager(wsObj.software_delivery?.package_manager);
}
function normalizeNonNodePreset(value) {
    if (typeof value !== 'string') {
        return null;
    }
    const trimmed = value.trim().toLowerCase();
    return NON_NODE_PRESETS.has(trimmed) ? trimmed : null;
}
function readWorkspaceExecutionPreset(projectDir) {
    const wsPath = path.join(projectDir, WORKSPACE_CONFIG_FILE_NAME);
    if (!existsSync(wsPath)) {
        return null;
    }
    let parsed;
    try {
        parsed = YAML.parse(readFileSync(wsPath, 'utf-8'));
    }
    catch {
        return null;
    }
    if (!parsed || typeof parsed !== 'object') {
        return null;
    }
    const wsObj = parsed;
    return normalizeNonNodePreset(wsObj.software_delivery?.gates?.execution?.preset);
}
function hasPackageJsonBuildScript(projectDir) {
    return hasPackageJsonScript(projectDir, 'build');
}
/**
 * WU-2568: Return true if the project's package.json has a non-empty
 * `scripts.<scriptName>` entry. Missing package.json, malformed JSON, and
 * absent `scripts` blocks all return false.
 *
 * Used by {@link resolvePostUpgradeBuildCommand} to validate that a
 * configured `pnpm <script>` build_command is actually runnable in the
 * consumer's project before we surface it as a next-step instruction.
 */
export function hasPackageJsonScript(projectDir, scriptName) {
    if (!scriptName || scriptName.length === 0) {
        return false;
    }
    const pkgPath = path.join(projectDir, 'package.json');
    if (!existsSync(pkgPath)) {
        return false;
    }
    try {
        const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
        const script = pkg.scripts?.[scriptName];
        return typeof script === 'string' && script.trim().length > 0;
    }
    catch {
        return false;
    }
}
function detectPackageManagerFromPackageJson(projectDir) {
    const pkgPath = path.join(projectDir, 'package.json');
    if (!existsSync(pkgPath)) {
        return null;
    }
    try {
        const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
        return normalizePackageManager(pkg.packageManager);
    }
    catch {
        return null;
    }
}
function detectPackageManagerFromLockfiles(projectDir) {
    if (existsSync(path.join(projectDir, 'pnpm-lock.yaml'))) {
        return 'pnpm';
    }
    if (existsSync(path.join(projectDir, 'package-lock.json'))) {
        return 'npm';
    }
    if (existsSync(path.join(projectDir, 'yarn.lock'))) {
        return 'yarn';
    }
    if (existsSync(path.join(projectDir, 'bun.lock')) ||
        existsSync(path.join(projectDir, 'bun.lockb'))) {
        return 'bun';
    }
    return null;
}
function resolvePackageManagerForBuildScript(projectDir) {
    return (readWorkspacePackageManager(projectDir) ??
        detectPackageManagerFromPackageJson(projectDir) ??
        detectPackageManagerFromLockfiles(projectDir) ??
        'pnpm');
}
function formatPackageManagerBuildCommand(packageManager) {
    return PACKAGE_MANAGER_BUILD_COMMANDS[packageManager];
}
function createGenericBuildCommand() {
    return {
        instruction: "Rebuild with your project's build tool or package script. If your stack does not have a " +
            'rebuild step, run the equivalent verification command for your language/framework. ' +
            'Configure software_delivery.build_command in workspace.yaml to silence this generic hint.',
        source: 'generic',
        fallbackToGeneric: true,
    };
}
function createLanguageDetectedBuildCommand(instruction) {
    return {
        instruction,
        source: 'language-detected',
        fallbackToGeneric: false,
    };
}
function projectContainsMatchingFile(projectDir, matcher) {
    const queue = [projectDir];
    while (queue.length > 0) {
        const currentDir = queue.pop();
        if (!currentDir) {
            continue;
        }
        try {
            const entries = readdirSync(currentDir, { encoding: 'utf8', withFileTypes: true });
            for (const entry of entries) {
                if (entry.isDirectory()) {
                    if (DETECTION_IGNORED_DIRECTORIES.has(entry.name)) {
                        continue;
                    }
                    queue.push(path.join(currentDir, entry.name));
                    continue;
                }
                if (!entry.isFile()) {
                    continue;
                }
                const fullPath = path.join(currentDir, entry.name);
                const relativePath = path.relative(projectDir, fullPath);
                if (matcher(relativePath, entry.name)) {
                    return true;
                }
            }
        }
        catch {
            continue;
        }
    }
    return false;
}
function detectJavaBuildCommand(projectDir) {
    if (projectContainsMatchingFile(projectDir, (_relativePath, basename) => basename === 'gradlew')) {
        return './gradlew build';
    }
    if (projectContainsMatchingFile(projectDir, (_relativePath, basename) => ['build.gradle', 'build.gradle.kts', 'settings.gradle', 'settings.gradle.kts'].includes(basename))) {
        return 'gradle build';
    }
    if (projectContainsMatchingFile(projectDir, (_relativePath, basename) => basename === 'pom.xml')) {
        return 'mvn package';
    }
    return null;
}
function resolveBuildCommandFromExplicitPreset(projectDir) {
    const preset = readWorkspaceExecutionPreset(projectDir);
    if (!preset) {
        return null;
    }
    switch (preset) {
        case 'dotnet':
            return createLanguageDetectedBuildCommand('dotnet build');
        case 'go':
            return createLanguageDetectedBuildCommand('go build ./...');
        case 'rust':
            return createLanguageDetectedBuildCommand('cargo build');
        case 'java':
            return createLanguageDetectedBuildCommand(detectJavaBuildCommand(projectDir) ?? 'mvn package');
        case 'php':
        case 'python':
        case 'ruby':
            return createGenericBuildCommand();
        default:
            return null;
    }
}
function detectBuildCommandFromFilesystem(projectDir) {
    if (projectContainsMatchingFile(projectDir, (_relativePath, basename) => ['.csproj', '.fsproj', '.vbproj', '.sln'].some((suffix) => basename.endsWith(suffix)))) {
        return createLanguageDetectedBuildCommand('dotnet build');
    }
    const javaCommand = detectJavaBuildCommand(projectDir);
    if (javaCommand) {
        return createLanguageDetectedBuildCommand(javaCommand);
    }
    if (projectContainsMatchingFile(projectDir, (_relativePath, basename) => basename === 'go.mod')) {
        return createLanguageDetectedBuildCommand('go build ./...');
    }
    if (projectContainsMatchingFile(projectDir, (_relativePath, basename) => basename === 'Cargo.toml')) {
        return createLanguageDetectedBuildCommand('cargo build');
    }
    return null;
}
/**
 * WU-2568: Extract the script name from a `pnpm <script>` invocation.
 *
 * Handles the common invocation shapes:
 * - `pnpm build` -> `build`
 * - `pnpm run build` -> `build` (explicit `run`)
 * - `pnpm -w build`, `pnpm --workspace-root build` -> `build` (workspace flag)
 * - `pnpm build --watch` -> `build` (trailing args ignored)
 *
 * Returns `null` for:
 * - non-pnpm commands (`dotnet build`, `cargo build --release`, `npm run build`, etc.)
 * - bare `pnpm` with no script argument
 * - empty / whitespace-only input
 *
 * Non-pnpm commands returning null is a feature, not a bug: the resolver
 * trusts non-pnpm commands as-is because there is no package.json analogue
 * to validate them against. The user set them intentionally.
 */
export function extractPnpmScriptName(command) {
    if (typeof command !== 'string') {
        return null;
    }
    const trimmed = command.trim();
    if (trimmed.length === 0) {
        return null;
    }
    const tokens = trimmed.split(/\s+/);
    if (tokens[0] !== 'pnpm' || tokens.length < 2) {
        return null;
    }
    // Skip leading pnpm-level flags and the explicit `run` verb.
    let index = 1;
    while (index < tokens.length) {
        const token = tokens[index] ?? '';
        if (token === 'run' || token === '-w' || token === '--workspace-root') {
            index += 1;
            continue;
        }
        break;
    }
    const scriptName = tokens[index];
    if (!scriptName || scriptName.length === 0 || scriptName.startsWith('-')) {
        return null;
    }
    return scriptName;
}
function extractNpmScriptName(command) {
    if (typeof command !== 'string') {
        return null;
    }
    const tokens = command.trim().split(/\s+/);
    if (tokens[0] !== 'npm' || (tokens[1] !== 'run' && tokens[1] !== 'run-script')) {
        return null;
    }
    const scriptName = tokens[2];
    if (!scriptName || scriptName.startsWith('-')) {
        return null;
    }
    return scriptName;
}
function extractYarnScriptName(command) {
    if (typeof command !== 'string') {
        return null;
    }
    const tokens = command.trim().split(/\s+/);
    if (tokens[0] !== 'yarn' || tokens.length < 2) {
        return null;
    }
    const scriptIndex = tokens[1] === 'run' ? 2 : 1;
    const scriptName = tokens[scriptIndex];
    if (!scriptName || scriptName.startsWith('-')) {
        return null;
    }
    return scriptName;
}
function extractBunScriptName(command) {
    if (typeof command !== 'string') {
        return null;
    }
    const tokens = command.trim().split(/\s+/);
    if (tokens[0] !== 'bun' || tokens[1] !== 'run') {
        return null;
    }
    const scriptName = tokens[2];
    if (!scriptName || scriptName.startsWith('-')) {
        return null;
    }
    return scriptName;
}
function extractPackageManagerScriptReference(command) {
    const pnpmScriptName = extractPnpmScriptName(command);
    if (pnpmScriptName) {
        return { packageManager: 'pnpm', scriptName: pnpmScriptName };
    }
    const npmScriptName = extractNpmScriptName(command);
    if (npmScriptName) {
        return { packageManager: 'npm', scriptName: npmScriptName };
    }
    const yarnScriptName = extractYarnScriptName(command);
    if (yarnScriptName) {
        return { packageManager: 'yarn', scriptName: yarnScriptName };
    }
    const bunScriptName = extractBunScriptName(command);
    if (bunScriptName) {
        return { packageManager: 'bun', scriptName: bunScriptName };
    }
    return null;
}
export function resolvePostUpgradeBuildCommand(projectDir) {
    const workspaceCmd = readWorkspaceBuildCommand(projectDir);
    if (workspaceCmd) {
        // WU-2568: A configured package-manager script invocation is only valid
        // if the consumer's package.json actually has that script. A stale default
        // (`build_command: pnpm build`, `npm run build`, `yarn build`, etc.)
        // in a non-JS repo is worthless - falling through to the package-json or
        // generic hint is strictly better than printing a command that will fail.
        //
        // Non-script commands (dotnet build, cargo build, mvn package, composer
        // install, etc.) are trusted as-is: there is no package.json analogue to
        // validate them against, and the user clearly set them intentionally.
        const packageScriptRef = extractPackageManagerScriptReference(workspaceCmd);
        const isValid = packageScriptRef === null || hasPackageJsonScript(projectDir, packageScriptRef.scriptName);
        if (isValid) {
            return {
                instruction: workspaceCmd,
                source: 'workspace',
                fallbackToGeneric: false,
            };
        }
        // Fall through to package-json / generic detection.
    }
    const presetResult = resolveBuildCommandFromExplicitPreset(projectDir);
    if (presetResult) {
        return presetResult;
    }
    if (hasPackageJsonBuildScript(projectDir)) {
        return {
            instruction: formatPackageManagerBuildCommand(resolvePackageManagerForBuildScript(projectDir)),
            source: 'package-json',
            fallbackToGeneric: false,
        };
    }
    return detectBuildCommandFromFilesystem(projectDir) ?? createGenericBuildCommand();
}
/**
 * WU-2366: Check whether core docs are stale after an upgrade.
 *
 * Processes each core doc template with the DATE token, hashes the result,
 * and compares against the on-disk file hash. Returns which files differ.
 *
 * @param targetDir - Project root directory to check
 * @returns Object with stale flag and list of stale file paths
 */
/**
 * WU-2371: Date pattern used to normalize date tokens for staleness comparison.
 * Matches YYYY-MM-DD date strings so that date differences don't cause false positives.
 */
const DATE_PATTERN = /\d{4}-\d{2}-\d{2}/g;
const DATE_PLACEHOLDER = 'XXXX-XX-XX';
export function checkDocsStaleness(targetDir) {
    const staleFiles = [];
    // WU-2371: Use the full token set (not just DATE) so the rendered template
    // matches the shape of docs produced by syncCoreDocs / init.
    const tokens = buildCoreDocTokens(targetDir);
    for (const [outputFile, templatePath] of Object.entries(CORE_DOC_TEMPLATE_PATHS)) {
        const filePath = path.join(targetDir, outputFile);
        if (!existsSync(filePath)) {
            // File doesn't exist on disk — it's stale by definition
            staleFiles.push(outputFile);
            continue;
        }
        try {
            const templateContent = loadTemplate(templatePath);
            const processedContent = processTemplate(templateContent, tokens);
            // WU-2371: Normalize dates before hashing so that a file synced yesterday
            // isn't reported stale just because the date differs.
            const normalizedTemplate = processedContent.replace(DATE_PATTERN, DATE_PLACEHOLDER);
            const templateHash = createHash('sha256').update(normalizedTemplate).digest('hex');
            const onDiskContent = readFileSync(filePath, 'utf-8');
            const normalizedOnDisk = onDiskContent.replace(DATE_PATTERN, DATE_PLACEHOLDER);
            const onDiskHash = createHash('sha256').update(normalizedOnDisk).digest('hex');
            if (templateHash !== onDiskHash) {
                staleFiles.push(outputFile);
            }
        }
        catch {
            // Template loading failed — skip gracefully (template may not exist in older versions)
        }
    }
    return { stale: staleFiles.length > 0, staleFiles };
}
/**
 * Print help message for lumenflow-upgrade
 */
/* istanbul ignore next -- CLI entry point */
function printHelp() {
    console.log(`
Usage: lumenflow-upgrade [options]

Options:
  -v, --version <ver>        Upgrade to specific version (e.g., 1.5.0)
  -l, --latest               Upgrade to latest version
  -n, --dry-run              Show commands without executing
  -f, --force                Run the full sync flow even when already on the
                             target version (bypasses the no-op short-circuit)
  --no-bootstrap             Skip self-bootstrap (use installed version as-is)
  -h, --help                 Show this help message

Packages upgraded (all 7):
${LUMENFLOW_PACKAGES.map((p) => `  - ${p}`).join('\n')}

Examples:
  lumenflow:upgrade --version 1.5.0    # Upgrade to specific version
  lumenflow:upgrade --latest           # Upgrade to latest
  lumenflow:upgrade --latest --dry-run # Preview upgrade commands

Micro-Worktree Pattern (WU-1127):
  This command uses micro-worktree isolation to atomically update
  package.json and pnpm-lock.yaml without requiring you to claim a WU.

  Run from your main checkout (NOT from inside a worktree):
    cd /path/to/main
    pnpm lumenflow:upgrade --latest
`);
}
/**
 * Main entry point for lumenflow-upgrade command
 */
/* istanbul ignore next -- CLI entry point */
export async function main() {
    const args = parseUpgradeArgs(process.argv);
    if (args.help) {
        printHelp();
        process.exit(EXIT_CODES.SUCCESS);
    }
    // Require either --version or --latest
    if (!args.version && !args.latest) {
        console.error(`${LOG_PREFIX} Error: Must specify --version <ver> or --latest`);
        printHelp();
        process.exit(EXIT_CODES.ERROR);
    }
    // WU-2087: Self-bootstrap to target version when installed version differs.
    // This solves the chicken-and-egg problem: if the installed version has bugs
    // (e.g., unconditional -w flag in v3.1.2), we delegate to the target version
    // which has the fix. Degrades gracefully if version resolution fails.
    //
    // WU-2600: Also short-circuit when installed === target (no-op upgrade).
    // We resolve both versions once and reuse the result for the no-op check.
    const targetVersion = await resolveTargetVersion(args);
    const installedVersion = getInstalledCliVersion();
    if (!args.noBootstrap) {
        if (targetVersion && installedVersion && targetVersion !== installedVersion) {
            console.log(`${LOG_PREFIX} Installed: v${installedVersion}, target: v${targetVersion} — self-bootstrapping`);
            try {
                selfBootstrap(targetVersion, process.argv);
                process.exit(EXIT_CODES.SUCCESS);
            }
            catch (error) {
                console.warn(`${LOG_PREFIX} Self-bootstrap failed, proceeding with installed version: ${error instanceof Error ? error.message : String(error)}`);
                // Fall through to use installed version
            }
        }
    }
    // WU-2600: Short-circuit when already on the target version. Must happen
    // BEFORE validateMainCheckout / executeUpgradeInMicroWorktree so no
    // transaction, marker, docs-sync, or commit is produced on the happy
    // no-op path. Overridden by --force.
    const shortCircuit = shouldShortCircuitUpgrade(installedVersion, targetVersion, args);
    if (shortCircuit.shortCircuit) {
        console.log(`${LOG_PREFIX} Already at ${shortCircuit.version}, nothing to do`);
        console.log(`${LOG_PREFIX} Pass --force to re-run sync-templates and docs-sync on this version.`);
        process.exit(EXIT_CODES.SUCCESS);
    }
    // WU-1127: Validate we're on main checkout (not in a worktree)
    const validation = await validateMainCheckout();
    if (!validation.valid) {
        console.error(`${LOG_PREFIX} ${validation.error}`);
        if (validation.fixCommand) {
            console.error(`\nTo fix:\n${validation.fixCommand}`);
        }
        process.exit(EXIT_CODES.ERROR);
    }
    // Build upgrade commands for dry-run display
    const { addCommand, versionSpec } = buildUpgradeCommands(args);
    if (args.dryRun) {
        console.log(`${LOG_PREFIX} DRY RUN - Commands that would be executed:`);
        console.log(`  ${addCommand}`);
        console.log(`\n${LOG_PREFIX} Packages: ${LUMENFLOW_PACKAGES.length}`);
        console.log(`${LOG_PREFIX} Version: ${versionSpec}`);
        console.log(`\n${LOG_PREFIX} No changes made.`);
        process.exit(EXIT_CODES.SUCCESS);
    }
    // Execute upgrade using micro-worktree
    try {
        await executeUpgradeInMicroWorktree(args);
    }
    catch (error) {
        console.error(`\n${LOG_PREFIX} Upgrade failed`);
        console.error(`${LOG_PREFIX} ${error instanceof Error ? error.message : String(error)}`);
        process.exit(EXIT_CODES.ERROR);
    }
    // WU-2373: Core docs are now synced inside the upgrade transaction.
    // No need to advise running docs:sync separately.
}
// Run main if executed directly
if (import.meta.main) {
    void runCLI(main);
}
//# sourceMappingURL=lumenflow-upgrade.js.map