"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectDefaultBranch = detectDefaultBranch;
exports.installHooks = installHooks;
exports.installDevcontainer = installDevcontainer;
exports.installCiGuardrails = installCiGuardrails;
exports.installCiBaselineRefresh = installCiBaselineRefresh;
exports.installPrReview = installPrReview;
exports.installIgnoreFiles = installIgnoreFiles;
exports.installHooksPostinstall = installHooksPostinstall;
/**
 * Phase Ship installers — additive copy of dxkit's guardrail templates
 * into a consumer repo. Wired into `vyuh-dxkit init` via the
 * `--with-hooks` / `--with-devcontainer` / `--with-ci` /
 * `--with-baseline-refresh` flags (or `--full` which implies all).
 *
 * Each installer is additive by default: existing consumer files are
 * never overwritten. When a conflict is detected, the installer
 * writes a sidecar (`.dxkit`-suffixed) reference file and emits a
 * note with merge instructions. `force: true` bypasses the sidecar
 * fallback and overwrites in place.
 */
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const child_process_1 = require("child_process");
const files_1 = require("./files");
const detect_1 = require("./detect");
const languages_1 = require("./languages");
/**
 * Detect the consumer repo's default branch so workflow templates
 * that fire on pushes to "the main branch" point at the right name.
 * The resolution order is intentionally lenient — we'd rather
 * substitute *some* sensible branch and let the consumer edit the
 * workflow than refuse to install when git state is incomplete.
 *
 *   1. `git symbolic-ref refs/remotes/origin/HEAD` (set whenever the
 *      repo was cloned from a remote with a default-branch HEAD)
 *   2. `git rev-parse --verify <name>` against `main` / `master` /
 *      `trunk` / `develop` — the four conventions that cover ~all
 *      real repos
 *   3. The current branch (`git branch --show-current`) — the best
 *      guess in a freshly-`git init`'d repo that hasn't been pushed
 *   4. Fallback to `'main'` — the GitHub default-branch default
 */
function detectDefaultBranch(cwd) {
    try {
        const ref = (0, child_process_1.execFileSync)('git', ['symbolic-ref', 'refs/remotes/origin/HEAD'], {
            cwd,
            encoding: 'utf8',
            stdio: ['ignore', 'pipe', 'ignore'],
        }).trim();
        const match = ref.match(/^refs\/remotes\/origin\/(.+)$/);
        if (match && match[1])
            return match[1];
    }
    catch {
        /* fall through */
    }
    for (const candidate of ['main', 'master', 'trunk', 'develop']) {
        try {
            (0, child_process_1.execFileSync)('git', ['rev-parse', '--verify', '--quiet', candidate], {
                cwd,
                stdio: 'ignore',
            });
            return candidate;
        }
        catch {
            /* try next */
        }
    }
    try {
        const current = (0, child_process_1.execFileSync)('git', ['branch', '--show-current'], {
            cwd,
            encoding: 'utf8',
            stdio: ['ignore', 'pipe', 'ignore'],
        }).trim();
        if (current)
            return current;
    }
    catch {
        /* fall through */
    }
    return 'main';
}
function emptyResult() {
    return { installed: [], skipped: [], sidecars: [], notes: [] };
}
function templatesDir() {
    return path.join(__dirname, '..', 'templates');
}
/**
 * Copy a single source file to a destination, handling the
 * additive-vs-force decision. Returns the (relative) path that was
 * written + which bucket it lands in.
 */
function copyAdditive(srcAbs, destAbs, cwd, opts, result, options = {}) {
    const relDest = path.relative(cwd, destAbs);
    const exists = fs.existsSync(destAbs);
    if (exists && !opts.force) {
        const suffix = options.sidecarSuffix ?? '.dxkit';
        const sidecarAbs = destAbs + suffix;
        const relSidecar = path.relative(cwd, sidecarAbs);
        fs.mkdirSync(path.dirname(sidecarAbs), { recursive: true });
        fs.copyFileSync(srcAbs, sidecarAbs);
        if (options.executable)
            (0, files_1.makeExecutable)(sidecarAbs);
        result.sidecars.push(relSidecar);
        return;
    }
    fs.mkdirSync(path.dirname(destAbs), { recursive: true });
    fs.copyFileSync(srcAbs, destAbs);
    if (options.executable)
        (0, files_1.makeExecutable)(destAbs);
    if (exists) {
        result.installed.push(relDest);
    }
    else {
        result.installed.push(relDest);
    }
}
/**
 * Hooks installer. Writes `.githooks/pre-push` by default;
 * additionally writes `.githooks/pre-commit` when
 * `opts.withPrecommit === true`. When the consumer already has a
 * matching hook (via .githooks/ or .husky/), emits a `.dxkit`
 * sidecar instead and prints merge instructions.
 *
 * Customer still needs to run `git config core.hooksPath .githooks`
 * to activate. We don't run it for them — that's a global git config
 * mutation outside dxkit's purview.
 *
 * Why pre-commit is opt-in: see `InstallerOpts.withPrecommit`.
 */
function installHooks(cwd, opts = {}) {
    const result = emptyResult();
    const tmplDir = path.join(templatesDir(), '.githooks');
    const destDir = path.join(cwd, '.githooks');
    // Husky users have hooks at `.husky/<name>`. We treat either an
    // existing `.githooks/<name>` *or* `.husky/<name>` as a conflict —
    // both routes mean the consumer already has an active hook, even
    // though only the .githooks/ path is the destination we write to.
    //
    // pre-commit is opt-in (slow on large repos until incremental
    // scanning lands); pre-push always installs (acceptable cost).
    const hookNames = opts.withPrecommit
        ? [{ name: 'pre-commit' }, { name: 'pre-push' }]
        : [{ name: 'pre-push' }];
    let anyConflict = false;
    for (const { name } of hookNames) {
        const destAbs = path.join(destDir, name);
        const huskyAbs = path.join(cwd, '.husky', name);
        const conflict = fs.existsSync(destAbs) || fs.existsSync(huskyAbs);
        if (conflict)
            anyConflict = true;
        const srcAbs = path.join(tmplDir, name);
        if (conflict && !opts.force) {
            // Always land the dxkit hook at .githooks/<name>.dxkit when
            // there's a conflict, even when the conflict is at a different
            // path (husky). The sidecar is the same regardless of where the
            // existing hook lives, and the merge note tells the consumer
            // how to chain them.
            const sidecarAbs = destAbs + '.dxkit';
            fs.mkdirSync(path.dirname(sidecarAbs), { recursive: true });
            fs.copyFileSync(srcAbs, sidecarAbs);
            (0, files_1.makeExecutable)(sidecarAbs);
            result.sidecars.push(path.relative(cwd, sidecarAbs));
        }
        else {
            fs.mkdirSync(path.dirname(destAbs), { recursive: true });
            fs.copyFileSync(srcAbs, destAbs);
            (0, files_1.makeExecutable)(destAbs);
            result.installed.push(path.relative(cwd, destAbs));
        }
    }
    if (anyConflict) {
        result.notes.push('Detected an existing pre-commit/pre-push hook. The dxkit hooks were ' +
            'written as `.dxkit` sidecars next to your existing hooks; add a ' +
            '`sh .githooks/<name>.dxkit` line to each existing hook to chain them.');
    }
    result.notes.push('Activate the hooks: `git config core.hooksPath .githooks` (one-time, per clone).');
    if (!opts.withPrecommit) {
        result.notes.push('pre-commit hook NOT installed (default). The full guardrail re-runs every analyzer ' +
            'and is slow on large repos. Pre-push catches the same regressions before code leaves ' +
            'the machine. Re-run init with `--with-precommit-hook` to enable commit-time gating.');
    }
    return result;
}
/**
 * Render the per-stack features block as a properly-indented JSON
 * fragment slotted into the template's `"features": __PLACEHOLDER__`
 * line. The first line stays flush; continuation lines get the
 * template's two-space outer indent so the resulting object reads
 * naturally inside the surrounding JSONC.
 */
function renderFeaturesBlock(features) {
    const raw = JSON.stringify(features, null, 2);
    return raw
        .split('\n')
        .map((line, i) => (i === 0 ? line : '  ' + line))
        .join('\n');
}
/**
 * Render the extensions array as a JSON array, indented to line up
 * naturally inside the surrounding JSONC. Mirrors renderFeaturesBlock's
 * shape so the two substitutions produce visually consistent output.
 */
function renderExtensionsBlock(extensions) {
    const raw = JSON.stringify(extensions, null, 2);
    return raw
        .split('\n')
        .map((line, i) => (i === 0 ? line : '      ' + line))
        .join('\n');
}
/**
 * Read the devcontainer.json template, substitute the
 * `__DXKIT_DEVCONTAINER_FEATURES__` and `__DXKIT_DEVCONTAINER_EXTENSIONS__`
 * placeholders with the detected stack's per-pack contributions, and
 * return the rendered text. Pure-ish: one filesystem read, no writes.
 */
function renderDevcontainerJson(tmplDir, cwd) {
    const srcAbs = path.join(tmplDir, 'devcontainer.json');
    const template = fs.readFileSync(srcAbs, 'utf8');
    const stack = (0, detect_1.detect)(cwd);
    const features = (0, languages_1.buildDevcontainerFeatures)(stack.languages);
    const extensions = (0, languages_1.buildDevcontainerExtensions)(stack.languages);
    return template
        .replace('__DXKIT_DEVCONTAINER_FEATURES__', renderFeaturesBlock(features))
        .replace('__DXKIT_DEVCONTAINER_EXTENSIONS__', renderExtensionsBlock(extensions));
}
/**
 * Devcontainer installer. Writes the dxkit lightweight devcontainer
 * (devcontainer.json + post-create.sh + install-agent-clis.sh) into
 * `.devcontainer/`. If the consumer already has a devcontainer.json,
 * writes the entire dxkit set into `.devcontainer/.dxkit-reference/`
 * for manual merge.
 *
 * devcontainer.json is rendered per-detected-stack: only active
 * packs' features land in the output. Always-on entries (Node +
 * GitHub CLI) ship regardless so the post-create script can run on
 * a non-Node project.
 */
function installDevcontainer(cwd, opts = {}) {
    const result = emptyResult();
    const tmplDir = path.join(templatesDir(), '.devcontainer');
    const destDir = path.join(cwd, '.devcontainer');
    const filesToCopyVerbatim = [
        { name: 'post-create.sh', executable: true },
        { name: 'install-agent-clis.sh', executable: true },
    ];
    const hasExistingDevcontainer = fs.existsSync(path.join(destDir, 'devcontainer.json'));
    if (hasExistingDevcontainer && !opts.force) {
        // Stash the whole dxkit set under a reference dir so the
        // consumer can read it without it interfering with their own.
        // The reference devcontainer.json is rendered with the
        // per-stack features so the customer sees the same shape as
        // a fresh install — they can lift the features block directly.
        const refDir = path.join(destDir, '.dxkit-reference');
        fs.mkdirSync(refDir, { recursive: true });
        const renderedJson = renderDevcontainerJson(tmplDir, cwd);
        fs.writeFileSync(path.join(refDir, 'devcontainer.json'), renderedJson, 'utf8');
        result.sidecars.push(path.relative(cwd, path.join(refDir, 'devcontainer.json')));
        for (const f of filesToCopyVerbatim) {
            const srcAbs = path.join(tmplDir, f.name);
            const destAbs = path.join(refDir, f.name);
            fs.copyFileSync(srcAbs, destAbs);
            if (f.executable)
                (0, files_1.makeExecutable)(destAbs);
            result.sidecars.push(path.relative(cwd, destAbs));
        }
        result.notes.push('Existing devcontainer.json detected — dxkit pieces stashed in ' +
            '`.devcontainer/.dxkit-reference/`. To enable dxkit guardrails in ' +
            'your container: (1) merge the `features` block from the reference ' +
            'devcontainer.json into yours, (2) add `bash .devcontainer/post-create.sh` ' +
            'to your `postCreateCommand`, (3) copy `post-create.sh` and ' +
            '`install-agent-clis.sh` into `.devcontainer/`.');
        return result;
    }
    // devcontainer.json gets rendered (per-stack features substitution),
    // the rest are flat copies.
    fs.mkdirSync(destDir, { recursive: true });
    const renderedJson = renderDevcontainerJson(tmplDir, cwd);
    const destDevcontainerJson = path.join(destDir, 'devcontainer.json');
    fs.writeFileSync(destDevcontainerJson, renderedJson, 'utf8');
    result.installed.push(path.relative(cwd, destDevcontainerJson));
    for (const f of filesToCopyVerbatim) {
        copyAdditive(path.join(tmplDir, f.name), path.join(destDir, f.name), cwd, { force: true }, result, { executable: f.executable });
    }
    return result;
}
/**
 * Generic workflow installer used by both --with-ci (PR-gate) and
 * --with-baseline-refresh (post-merge auto-regen). Both targets are
 * uniquely-named workflow files; conflict only happens on a re-run
 * against an existing dxkit install, which we skip with a note.
 *
 * `substitutions` lets a workflow template carry placeholders the
 * installer fills in at write time. The baseline-refresh workflow
 * uses this for the consumer's default branch name; the PR-gate
 * workflow ships verbatim.
 */
function installWorkflow(cwd, fileName, opts, substitutions = {}) {
    const result = emptyResult();
    const srcAbs = path.join(templatesDir(), '.github', 'workflows', fileName);
    const destAbs = path.join(cwd, '.github', 'workflows', fileName);
    if (fs.existsSync(destAbs) && !opts.force) {
        result.skipped.push(path.relative(cwd, destAbs));
        return result;
    }
    fs.mkdirSync(path.dirname(destAbs), { recursive: true });
    if (Object.keys(substitutions).length === 0) {
        fs.copyFileSync(srcAbs, destAbs);
    }
    else {
        let content = fs.readFileSync(srcAbs, 'utf8');
        for (const [key, value] of Object.entries(substitutions)) {
            content = content.split(key).join(value);
        }
        fs.writeFileSync(destAbs, content, 'utf8');
    }
    result.installed.push(path.relative(cwd, destAbs));
    return result;
}
function installCiGuardrails(cwd, opts = {}) {
    return installWorkflow(cwd, 'dxkit-guardrails.yml', opts);
}
function installCiBaselineRefresh(cwd, opts = {}) {
    const defaultBranch = detectDefaultBranch(cwd);
    const result = installWorkflow(cwd, 'dxkit-baseline-refresh.yml', opts, {
        __DXKIT_DEFAULT_BRANCH__: defaultBranch,
    });
    if (result.installed.length > 0 && defaultBranch !== 'main') {
        result.notes.push(`baseline-refresh workflow targets the '${defaultBranch}' branch (detected from your repo). ` +
            `Edit the workflow's \`branches:\` trigger if you want a different one.`);
    }
    return result;
}
/**
 * AI PR-review workflow installer. Writes
 * `.github/workflows/pr-review.yml` — a workflow that runs Claude
 * Code over a PR's diff and posts a review comment.
 *
 * Opt-in (via `--with-pr-review`) because the workflow needs an
 * `ANTHROPIC_API_KEY` repo secret AND a flipped `ENABLE_AI_REVIEW`
 * repo variable to actually run anything; without both, the
 * workflow file just sits inert in the repo and clutters the
 * Actions tab. Customers who want this should opt in explicitly
 * with the flag (and configure the repo secret + variable
 * separately).
 */
function installPrReview(cwd, opts = {}) {
    const result = installWorkflow(cwd, 'pr-review.yml', opts);
    if (result.installed.length > 0) {
        result.notes.push('pr-review.yml is dormant until you configure both: ' +
            '(1) `ANTHROPIC_API_KEY` repo secret, and ' +
            '(2) `ENABLE_AI_REVIEW=true` repo variable.');
    }
    return result;
}
const GITIGNORE_HEADER = '# dxkit — runtime outputs (analyzer reports + dashboard)';
const GITIGNORE_ENTRIES = [
    '.dxkit/reports/',
    '.dxkit/dashboard.html',
    '.dxkit/cache/',
    'graphify-out/',
];
/**
 * Seed `.gitignore` with dxkit's runtime-output paths and write a
 * starter `.dxkit-ignore` template explaining the optional dxkit-
 * specific scan-exclusion file.
 *
 * Concerns the seeded files address:
 *   - `.gitignore`: stops customers from accidentally committing
 *     `.dxkit/reports/*.md` and `.dxkit/dashboard.html` (which
 *     churn on every analyzer run). Selectively keeps
 *     `.dxkit/baselines/` tracked — it IS the guardrail anchor.
 *   - `.dxkit-ignore`: dxkit's own scan-exclusion file. Loaded by
 *     `loadExclusions()` if present; never created by dxkit before
 *     this commit. Seeding a documented template makes the feature
 *     discoverable.
 *
 * Both are additive: existing `.gitignore` entries are preserved
 * (dedup against current contents); existing `.dxkit-ignore` is
 * never overwritten.
 */
function installIgnoreFiles(cwd, opts = {}) {
    const result = emptyResult();
    // .gitignore: append runtime-output entries
    const gitignorePath = path.join(cwd, '.gitignore');
    let existing = '';
    if (fs.existsSync(gitignorePath)) {
        existing = fs.readFileSync(gitignorePath, 'utf-8');
        if (existing.includes(GITIGNORE_HEADER)) {
            result.skipped.push('.gitignore');
        }
        else {
            const existingLines = new Set(existing.split('\n').map((l) => l.trim()));
            const newEntries = GITIGNORE_ENTRIES.filter((e) => !existingLines.has(e));
            if (newEntries.length > 0) {
                const block = '\n' + GITIGNORE_HEADER + '\n' + newEntries.join('\n') + '\n';
                fs.appendFileSync(gitignorePath, block, 'utf-8');
                result.installed.push('.gitignore');
            }
            else {
                result.skipped.push('.gitignore');
            }
        }
    }
    else {
        const content = GITIGNORE_HEADER + '\n' + GITIGNORE_ENTRIES.join('\n') + '\n';
        fs.writeFileSync(gitignorePath, content, 'utf-8');
        result.installed.push('.gitignore');
    }
    // .dxkit-ignore: write starter template (never overwrite)
    const dxkitIgnorePath = path.join(cwd, '.dxkit-ignore');
    if (fs.existsSync(dxkitIgnorePath) && !opts.force) {
        result.skipped.push('.dxkit-ignore');
    }
    else {
        fs.writeFileSync(dxkitIgnorePath, DXKIT_IGNORE_TEMPLATE, 'utf-8');
        result.installed.push('.dxkit-ignore');
    }
    return result;
}
/**
 * Wire `vyuh-dxkit hooks activate` into the consumer's `package.json`
 * postinstall script so cloning + `npm install` auto-activates the
 * dxkit hook directory. Closes the per-clone "run this one git config
 * command" friction step that today's installHooks emits as a note.
 *
 * Conflict policy (mirrors the rest of the ship surface):
 *   - No package.json → skip cleanly (Python-only / Go-only / etc.
 *     repos use the existing manual-activation path).
 *   - scripts.postinstall absent → write ours.
 *   - scripts.postinstall already mentions `vyuh-dxkit hooks activate`
 *     → skip (idempotent re-runs).
 *   - scripts.postinstall is set to something else → leave it alone
 *     and emit a note asking the consumer to chain. Auto-chaining
 *     risks breaking their existing script's exit-code semantics.
 */
const POSTINSTALL_CMD = 'vyuh-dxkit hooks activate';
function installHooksPostinstall(cwd, _opts = {}) {
    const result = emptyResult();
    const pkgPath = path.join(cwd, 'package.json');
    if (!fs.existsSync(pkgPath)) {
        // Non-Node repos (Python-only, Go-only, .NET-only, etc.) — no
        // postinstall surface to wire into. Fall back to the existing
        // manual note emitted by `installHooks`.
        return result;
    }
    let raw;
    try {
        raw = fs.readFileSync(pkgPath, 'utf-8');
    }
    catch {
        return result;
    }
    let pkg;
    try {
        pkg = JSON.parse(raw);
    }
    catch {
        // Malformed package.json — bail rather than risk corrupting it.
        result.notes.push(`Skipped postinstall hooks-activation wire-up: ${path.relative(cwd, pkgPath)} is not valid JSON.`);
        return result;
    }
    const existing = pkg.scripts?.postinstall;
    if (existing && existing.includes(POSTINSTALL_CMD)) {
        result.skipped.push('package.json (postinstall)');
        return result;
    }
    if (existing && existing.trim().length > 0) {
        // Don't auto-chain — too risky. Surface a clear note instead.
        result.notes.push(`Existing package.json scripts.postinstall preserved. To auto-activate dxkit hooks on every clone, ` +
            `chain by hand: \`"postinstall": "${existing} && ${POSTINSTALL_CMD}"\`.`);
        return result;
    }
    pkg.scripts = { ...(pkg.scripts ?? {}), postinstall: POSTINSTALL_CMD };
    // Preserve trailing newline if the original had one — many editors
    // and linters expect it.
    const trailingNewline = raw.endsWith('\n') ? '\n' : '';
    fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + trailingNewline, 'utf-8');
    result.installed.push('package.json (postinstall)');
    result.notes.push('Wired `vyuh-dxkit hooks activate` into package.json postinstall — every clone + `npm install` ' +
        'will activate `core.hooksPath = .githooks` automatically. (Manual one-time activation no longer needed.)');
    return result;
}
const DXKIT_IGNORE_TEMPLATE = `# .dxkit-ignore — extra paths dxkit's analyzers should skip.
#
# Format: same as .gitignore (directory/, file-glob, multi-segment).
# Union'd on top of:
#   - dxkit's bundled defaults (node_modules/, dist/, .git/, build/, ...)
#   - this repo's .gitignore
#
# Use this file for dxkit-specific exclusions that you don't want in
# .gitignore (e.g. vendored code you DO commit but DON'T want dxkit
# to analyze for quality / coverage / security findings).
#
# Common examples (uncomment + adjust to your project):
#
# vendor/                    # vendored third-party code committed to git
# third_party/
# generated/                 # generated code (protobuf, GraphQL types, ORM)
# *.generated.ts
# *.designer.cs
# legacy/                    # pre-existing code you don't want to track findings against
# fixtures/large/            # test fixtures that inflate metrics
#
# After editing, the next \`vyuh-dxkit baseline create\` will pick up the
# changes. Note: changes to this file invalidate cached baselines
# (the file's content hash lands in the baseline envelope).
`;
//# sourceMappingURL=ship-installers.js.map