var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
    var useValue = arguments.length > 2;
    for (var i = 0; i < initializers.length; i++) {
        value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
    }
    return useValue ? value : void 0;
};
var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
    function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
    var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
    var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
    var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
    var _, done = false;
    for (var i = decorators.length - 1; i >= 0; i--) {
        var context = {};
        for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
        for (var p in contextIn.access) context.access[p] = contextIn.access[p];
        context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
        var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
        if (kind === "accessor") {
            if (result === void 0) continue;
            if (result === null || typeof result !== "object") throw new TypeError("Object expected");
            if (_ = accept(result.get)) descriptor.get = _;
            if (_ = accept(result.set)) descriptor.set = _;
            if (_ = accept(result.init)) initializers.unshift(_);
        }
        else if (_ = accept(result)) {
            if (kind === "field") initializers.unshift(_);
            else descriptor[key] = _;
        }
    }
    if (target) Object.defineProperty(target, contextIn.name, descriptor);
    done = true;
};
import { exec } from "node:child_process";
import { readFile as fsReadFile, mkdir, writeFile, access, rm, stat, readdir, } from "node:fs/promises";
import * as path from "node:path";
import { promisify } from "node:util";
import { glob } from "glob";
import { detect, resolveCommand } from "package-manager-detector";
import { normalizePath } from "vite";
import { getErrorMeta, getLogger } from "../../util/logger.js";
import llmobs from "../llmobs/index.js";
import { NpmInstallError } from "../types.js";
import { CLARK_APP_BLACKLIST_PATTERNS, APPLICATION_FILE_PATTERNS, STANDARD_IGNORE_PATTERNS, } from "./constants.js";
import { DraftManager, validateFileOperation, } from "./filesystem/index.js";
import { Linter } from "./linter.js";
const execPromise = promisify(exec);
/**
 * AppShell provides the primary interface for the AI agent to interact
 * with the application environment.
 *
 * It encapsulates:
 * - File system operations (read, write, delete, list, rename)
 * - Virtual file abstraction (APIs appear as single .ts files)
 * - Draft management for undo/redo capability
 * - Code quality tools (linting, type checking)
 * - Package management
 */
let AppShell = (() => {
    let _instanceExtraInitializers = [];
    let _readFiles_decorators;
    let _runTypecheck_decorators;
    let _runLinter_decorators;
    let _warmLinterCache_decorators;
    let _runNpmInstall_decorators;
    let _runPackageManagerInstall_decorators;
    let _installSpecificPackages_decorators;
    let _uninstallPackages_decorators;
    let _saveGeneratedArtifact_decorators;
    let _readFile_decorators;
    let _writeFile_decorators;
    let _deleteFile_decorators;
    let _listFiles_decorators;
    let _listFilesInternal_decorators;
    let _renameFile_decorators;
    return class AppShell {
        static {
            const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0;
            _readFiles_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.readFiles" })];
            _runTypecheck_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.runTypecheck" })];
            _runLinter_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.runLinter" })];
            _warmLinterCache_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.warmLinterCache" })];
            _runNpmInstall_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.runNpmInstall" })];
            _runPackageManagerInstall_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.runPackageManagerInstall" })];
            _installSpecificPackages_decorators = [llmobs.decorate({
                    kind: "task",
                    name: "AppShell.installSpecificPackages",
                })];
            _uninstallPackages_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.uninstallPackages" })];
            _saveGeneratedArtifact_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.saveGeneratedArtifact" })];
            _readFile_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.readFile" })];
            _writeFile_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.writeFile" })];
            _deleteFile_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.deleteFile" })];
            _listFiles_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.listFiles" })];
            _listFilesInternal_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.listFilesInternal" })];
            _renameFile_decorators = [llmobs.decorate({ kind: "task", name: "AppShell.renameFile" })];
            __esDecorate(this, null, _readFiles_decorators, { kind: "method", name: "readFiles", static: false, private: false, access: { has: obj => "readFiles" in obj, get: obj => obj.readFiles }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _runTypecheck_decorators, { kind: "method", name: "runTypecheck", static: false, private: false, access: { has: obj => "runTypecheck" in obj, get: obj => obj.runTypecheck }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _runLinter_decorators, { kind: "method", name: "runLinter", static: false, private: false, access: { has: obj => "runLinter" in obj, get: obj => obj.runLinter }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _warmLinterCache_decorators, { kind: "method", name: "warmLinterCache", static: false, private: false, access: { has: obj => "warmLinterCache" in obj, get: obj => obj.warmLinterCache }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _runNpmInstall_decorators, { kind: "method", name: "runNpmInstall", static: false, private: false, access: { has: obj => "runNpmInstall" in obj, get: obj => obj.runNpmInstall }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _runPackageManagerInstall_decorators, { kind: "method", name: "runPackageManagerInstall", static: false, private: false, access: { has: obj => "runPackageManagerInstall" in obj, get: obj => obj.runPackageManagerInstall }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _installSpecificPackages_decorators, { kind: "method", name: "installSpecificPackages", static: false, private: false, access: { has: obj => "installSpecificPackages" in obj, get: obj => obj.installSpecificPackages }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _uninstallPackages_decorators, { kind: "method", name: "uninstallPackages", static: false, private: false, access: { has: obj => "uninstallPackages" in obj, get: obj => obj.uninstallPackages }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _saveGeneratedArtifact_decorators, { kind: "method", name: "saveGeneratedArtifact", static: false, private: false, access: { has: obj => "saveGeneratedArtifact" in obj, get: obj => obj.saveGeneratedArtifact }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _readFile_decorators, { kind: "method", name: "readFile", static: false, private: false, access: { has: obj => "readFile" in obj, get: obj => obj.readFile }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _writeFile_decorators, { kind: "method", name: "writeFile", static: false, private: false, access: { has: obj => "writeFile" in obj, get: obj => obj.writeFile }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _deleteFile_decorators, { kind: "method", name: "deleteFile", static: false, private: false, access: { has: obj => "deleteFile" in obj, get: obj => obj.deleteFile }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _listFiles_decorators, { kind: "method", name: "listFiles", static: false, private: false, access: { has: obj => "listFiles" in obj, get: obj => obj.listFiles }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _listFilesInternal_decorators, { kind: "method", name: "listFilesInternal", static: false, private: false, access: { has: obj => "listFilesInternal" in obj, get: obj => obj.listFilesInternal }, metadata: _metadata }, null, _instanceExtraInitializers);
            __esDecorate(this, null, _renameFile_decorators, { kind: "method", name: "renameFile", static: false, private: false, access: { has: obj => "renameFile" in obj, get: obj => obj.renameFile }, metadata: _metadata }, null, _instanceExtraInitializers);
            if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
        }
        appRootDirPath = __runInitializers(this, _instanceExtraInitializers);
        fsOperationQueue;
        linter;
        messageSender;
        draftManager;
        templateRenderer;
        virtualFs;
        pathValidator;
        enableDeletingLockfiles;
        /**
         * Tracks files that must be read before they can be written.
         * Used to enforce read-before-write pattern for AI safety.
         */
        requiredReads = new Set();
        constructor({ appRootDirPath, fsOperationQueue, messageSender, draftInterface, templateRenderer, virtualFileSystem, pathValidator, enableDeletingLockfiles, }) {
            // Normalize path to handle Windows drive letter case
            this.appRootDirPath = normalizePath(appRootDirPath);
            this.fsOperationQueue = fsOperationQueue;
            this.linter = new Linter(this.appRootDirPath);
            this.messageSender = messageSender;
            this.draftManager = draftInterface
                ? new DraftManager(draftInterface)
                : undefined;
            this.templateRenderer = templateRenderer;
            this.virtualFs = virtualFileSystem;
            this.pathValidator = pathValidator;
            this.enableDeletingLockfiles = enableDeletingLockfiles ?? false;
        }
        /**
         * Toggle the API virtual-file projection. When disabled, reads to `apis/*`
         * bypass the YAML→TS transformer and fall through to raw disk, which is what
         * v2→v3 migration needs so Clark sees the original YAML sources.
         */
        setApiHandlerEnabled(enabled) {
            this.virtualFs.setHandlerEnabled("api", enabled);
        }
        /**
         * Enqueue a filesystem operation and annotate queue timing in llmobs.
         * This helps separate queue contention from operation execution time.
         */
        async enqueueFsOperation(operationName, operation, metadata) {
            const enqueuedAt = performance.now();
            return await this.fsOperationQueue.enqueue(async () => {
                const startedAt = performance.now();
                const queueWaitMs = Math.round(startedAt - enqueuedAt);
                try {
                    return await operation();
                }
                finally {
                    const executeMs = Math.round(performance.now() - startedAt);
                    llmobs.annotate({
                        metadata: {
                            appShellQueueExecuteMs: executeMs,
                            appShellQueueOperation: operationName,
                            appShellQueueWaitMs: queueWaitMs,
                            ...(metadata ?? {}),
                        },
                    });
                }
            });
        }
        async readFiles(patterns, globOptions, enableAppBlacklist = true) {
            return await this.enqueueFsOperation("readFiles", async () => {
                // Get file list with virtual path conversion (physical API paths → virtual paths)
                const files = await this.listFilesInternal(patterns, globOptions);
                // Compute blacklisted files (regular component files like Button, Input)
                const blacklistedFiles = enableAppBlacklist
                    ? new Set(await glob(Array.from(CLARK_APP_BLACKLIST_PATTERNS), {
                        nodir: true,
                        cwd: this.appRootDirPath,
                        ignore: [...STANDARD_IGNORE_PATTERNS],
                    }))
                    : new Set();
                return await Promise.all(files.map(async (file) => {
                    const isBlacklisted = blacklistedFiles.has(file);
                    const content = isBlacklisted
                        ? "<… file contents omitted …>" // TODO(mark): could we just return the exported lines?
                        : await this.readFileInternal(file);
                    return {
                        type: "file",
                        filePath: file,
                        content,
                    };
                }));
            });
        }
        async runTypecheck(abortSignal) {
            const output = await this.runTypecheckRawOutput(abortSignal);
            if (!output) {
                return null;
            }
            const parsed = output.split("\n").reduce((acc, line) => {
                const [filePath, _rest] = line.split("(");
                if (filePath && filePath.trim() !== "") {
                    acc[filePath] = [...(acc[filePath] || []), line];
                    return acc;
                }
                return acc;
            }, {});
            return Object.keys(parsed).length > 0 ? parsed : null;
        }
        async runTypecheckRawOutput(abortSignal) {
            const command = process.env.SUPERBLOCKS_SERVER_ENV === "local"
                ? "pnpm exec tsc"
                : "npx tsc";
            const checkForErrors = new Promise((resolve) => {
                exec([
                    command,
                    "--noEmit",
                    // speed up marginal typecheck
                    "--incremental",
                    // expected format
                    "--pretty false",
                    // allow any
                    "--noImplicitAny false",
                    // cache in separate file to avoid conflicts with vite or user ide
                    "--tsBuildInfoFile .superblocks/app.tsbuildinfo",
                ].join(" "), {
                    cwd: this.appRootDirPath,
                    encoding: "utf-8",
                    signal: abortSignal,
                }, (_error, stdout, stderr) => {
                    resolve(stdout || stderr);
                });
            });
            const output = await checkForErrors;
            if (!output || typeof output !== "string") {
                return null;
            }
            return output;
        }
        async runLinter(filePaths, abortSignal) {
            const absoluteFilePaths = filePaths.map((filePath) => path.join(this.appRootDirPath, filePath));
            return await this.enqueueFsOperation("runLinter", async () => {
                const { errors, fixedFiles } = await this.linter.lint(absoluteFilePaths, abortSignal);
                // Mark auto-fixed files as requiring read before further writes
                for (const fixedFile of fixedFiles) {
                    this.requireRead(fixedFile);
                }
                if (Object.keys(errors).length > 0) {
                    return errors;
                }
                return null;
            }, {
                appShellQueueFileCount: filePaths.length,
            });
        }
        warmLinterCache() {
            void this.linter.warmCache(["**/*.{tsx}"]);
        }
        async runNpmInstall(abortSignal) {
            return await new Promise((resolve, reject) => {
                const command = 
                // if we're running in csb mock server, we need to avoid conflicts with the workspace
                process.env.SUPERBLOCKS_SERVER_ENV === "local"
                    ? "pnpm install --config.confirmModulesPurge=false"
                    : "npm install";
                exec(command, {
                    cwd: this.appRootDirPath,
                    timeout: 30_000,
                    signal: abortSignal,
                }, (error, _stdout, stderr) => {
                    if (error) {
                        return reject(new NpmInstallError(stderr));
                    }
                    return resolve();
                });
            });
        }
        async runPackageManagerInstall(abortSignal) {
            // In cloud edit mode, always use npm because pnpm is not available in the container.
            // The user's project may have pnpm signals (pnpm-lock.yaml, packageManager field)
            // from local editing, but we must use npm in cloud mode.
            const isCloudMode = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
            let packageManagerAgent;
            if (isCloudMode) {
                // Force npm in cloud mode
                packageManagerAgent = "npm";
            }
            else {
                // NOTE: this largely duplicates the logic in the CLI
                const packageManager = await detect({
                    strategies: [
                        "packageManager-field",
                        "lockfile",
                        "install-metadata",
                        "devEngines-field",
                    ],
                    cwd: this.appRootDirPath,
                });
                if (!packageManager) {
                    throw new Error("Could not detect package manager");
                }
                packageManagerAgent = packageManager.agent;
            }
            const installCommand = resolveCommand(packageManagerAgent, "install", packageManagerAgent === "npm" ? ["--fund=false", "--audit=false"] : []);
            if (!installCommand) {
                throw new Error(`Could not determine install command for ${packageManagerAgent}, skipping package installation`);
            }
            const { command, args } = installCommand;
            try {
                const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
                    cwd: this.appRootDirPath,
                    signal: abortSignal,
                });
                return stdout;
            }
            catch (error) {
                throw new NpmInstallError(error.stderr || error.message);
            }
        }
        async installSpecificPackages(packages, abortSignal) {
            if (!packages || packages.length === 0) {
                throw new Error("No packages specified for installation");
            }
            // In cloud edit mode, always use npm because pnpm is not available in the container.
            // The user's project may have pnpm signals (pnpm-lock.yaml, packageManager field)
            // from local editing, but we must use npm in cloud mode.
            const isCloudMode = process.env.SUPERBLOCKS_SERVER_ENV !== "local";
            let packageManagerAgent;
            if (isCloudMode) {
                // Force npm in cloud mode
                packageManagerAgent = "npm";
            }
            else {
                const packageManager = await detect({
                    strategies: [
                        "packageManager-field",
                        "lockfile",
                        "install-metadata",
                        "devEngines-field",
                    ],
                    cwd: this.appRootDirPath,
                });
                if (!packageManager) {
                    throw new Error("Could not detect package manager");
                }
                packageManagerAgent = packageManager.agent;
            }
            // Separate regular and dev dependencies
            const regularPackages = packages.filter((pkg) => !pkg.dev);
            const devPackages = packages.filter((pkg) => pkg.dev);
            const outputs = [];
            // Install regular dependencies
            if (regularPackages.length > 0) {
                const packageSpecs = regularPackages.map((pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name);
                const addCommand = resolveCommand(packageManagerAgent, "add", packageManagerAgent === "npm"
                    ? ["--fund=false", "--audit=false", ...packageSpecs]
                    : packageSpecs);
                if (!addCommand) {
                    throw new Error(`Could not determine add command for ${packageManagerAgent}`);
                }
                const { command, args } = addCommand;
                try {
                    const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
                        cwd: this.appRootDirPath,
                        signal: abortSignal,
                    });
                    outputs.push(stdout);
                }
                catch (error) {
                    throw new NpmInstallError(error.stderr || error.message);
                }
            }
            // Install dev dependencies
            if (devPackages.length > 0) {
                const packageSpecs = devPackages.map((pkg) => pkg.version ? `${pkg.name}@${pkg.version}` : pkg.name);
                const addCommand = resolveCommand(packageManagerAgent, "add", packageManagerAgent === "npm"
                    ? ["--save-dev", "--fund=false", "--audit=false", ...packageSpecs]
                    : ["-D", ...packageSpecs]);
                if (!addCommand) {
                    throw new Error(`Could not determine add command for ${packageManagerAgent}`);
                }
                const { command, args } = addCommand;
                try {
                    const { stdout } = await execPromise(`${command} ${args.join(" ")}`, {
                        cwd: this.appRootDirPath,
                        signal: abortSignal,
                    });
                    outputs.push(stdout);
                }
                catch (error) {
                    throw new NpmInstallError(error.stderr || error.message);
                }
            }
            return outputs.join("\n");
        }
        async uninstallPackages(packages, options) {
            return await new Promise((resolve, reject) => {
                // Use pnpm for local env, npm otherwise (same pattern as runNpmInstall)
                const packageManager = process.env.SUPERBLOCKS_SERVER_ENV === "local" ? "pnpm" : "npm";
                const uninstallCmd = packageManager === "pnpm" ? "remove" : "uninstall";
                const command = `${packageManager} ${uninstallCmd} ${packages.join(" ")}`;
                exec(command, {
                    cwd: this.appRootDirPath,
                    timeout: 30_000,
                    signal: options?.abortSignal,
                }, (error, stdout, stderr) => {
                    if (error) {
                        return reject(new NpmInstallError(stderr || error.message));
                    }
                    return resolve({ stdout, stderr });
                });
            });
        }
        static MAX_GENERATION_RUNS = 10;
        async saveGeneratedArtifact(artifact, stepId, runTimestamp) {
            const stepPath = path.join(this.appRootDirPath, ".superblocks", "generations", runTimestamp, stepId, artifact.filePath);
            await mkdir(path.dirname(stepPath), { recursive: true });
            const relativeStepPath = path.relative(this.appRootDirPath, stepPath);
            getLogger().debug(`[ai-service] Saving step file: ${relativeStepPath}`);
            await writeFile(stepPath, artifact.content);
            void this.pruneGenerations().catch((error) => {
                getLogger().warn("[ai-service] Failed to prune generations", getErrorMeta(error));
            });
        }
        async pruneGenerations(keepLast = AppShell.MAX_GENERATION_RUNS) {
            const generationsDir = path.join(this.appRootDirPath, ".superblocks", "generations");
            let entries;
            try {
                entries = await readdir(generationsDir);
            }
            catch (err) {
                if (err?.code === "ENOENT")
                    return;
                throw err;
            }
            if (entries.length <= keepLast) {
                return;
            }
            entries.sort();
            const toDelete = entries.slice(0, entries.length - keepLast);
            await Promise.all(toDelete.map((entry) => rm(path.join(generationsDir, entry), {
                recursive: true,
                force: true,
            })));
        }
        async sendMessage(message) {
            if (this.messageSender) {
                await this.messageSender(message);
            }
            else {
                getLogger().warn(`[ai-service] No message sender configured, cannot send message: ${message.type}`);
            }
        }
        /** Check if a given path exists in the app dir. */
        async pathExists(relativePath) {
            const fullPath = path.join(this.appRootDirPath, relativePath);
            try {
                await access(fullPath); // F_OK is the default: "is it there?"
                return true;
            }
            catch (err) {
                if (err.code === "ENOENT")
                    return false; // path missing
                throw err; // rethrow other errors (e.g., permissions)
            }
        }
        /**
         * Clears the scratch directory used for temporary API source files.
         * This is typically called when a draft is rejected to clean up temporary artifacts.
         */
        async clearScratchDirectory() {
            const scratchDir = path.join(this.appRootDirPath, ".superblocks", "scratch");
            await this.enqueueFsOperation("clearScratchDirectory", async () => {
                try {
                    await rm(scratchDir, { recursive: true });
                }
                catch {
                    // Ignore errors if directory doesn't exist or can't be deleted
                }
            });
        }
        // ============================================================================
        // Filesystem Operations
        // ============================================================================
        /**
         * Reads a single file's content from the application.
         *
         * Handles virtual path translation for files managed by the VirtualFileSystem:
         * - Virtual paths are read via the appropriate handler
         * - Regular files are read directly from disk
         *
         * @param relativePath - The virtual path relative to app root
         * @param options - Optional read options
         * @param options.offset - Line number to start reading from (1-indexed)
         * @param options.limit - Number of lines to read
         * @returns The file content as a string
         * @throws Error if file does not exist or cannot be read
         */
        async readFile(relativePath, options) {
            return await this.enqueueFsOperation("readFile", async () => {
                const content = await this.readFileInternal(relativePath, options);
                // Auto-track successful reads to allow subsequent writes
                this.markRead(relativePath);
                return content;
            }, {
                appShellQueueHasLimit: Boolean(options?.limit),
                appShellQueueHasOffset: Boolean(options?.offset),
                appShellQueuePath: relativePath,
            });
        }
        /**
         * Internal read implementation (no queue).
         * Use this from other methods that are already enqueued to avoid deadlock.
         */
        async readFileInternal(relativePath, options) {
            const logger = getLogger();
            let content;
            const isVirtual = this.virtualFs.isVirtualPath(relativePath);
            logger.info(`[AppShell.readFileInternal] path="${relativePath}", isVirtual=${isVirtual}`);
            // Try virtual filesystem first
            if (isVirtual) {
                logger.info(`[AppShell.readFileInternal] Reading via VirtualFileSystem: ${relativePath}`);
                const virtualContent = await this.virtualFs.read(relativePath, {
                    appRootDirPath: this.appRootDirPath,
                    templateRenderer: this.templateRenderer,
                });
                if (virtualContent === undefined) {
                    const info = this.virtualFs.parse(relativePath);
                    const name = info?.params.apiName ?? relativePath;
                    logger.warn(`[AppShell.readFileInternal] Virtual file not found: ${relativePath}`);
                    throw new Error(`${name} not found. Use the appropriate tool to create it.`);
                }
                logger.info(`[AppShell.readFileInternal] Virtual file read success: ${relativePath} (${virtualContent.length} chars)`);
                content = virtualContent;
            }
            else if (this.virtualFs.isPhysicalPath(relativePath)) {
                // Block direct reads of physical API files
                throw new Error(this.virtualFs.getBlockedReadError(relativePath));
            }
            else {
                // Regular file read
                logger.info(`[AppShell.readFileInternal] Reading from disk: ${relativePath}`);
                const absolutePath = path.join(this.appRootDirPath, relativePath);
                content = await fsReadFile(absolutePath, "utf-8");
            }
            // Apply offset and limit if provided
            if (options?.offset || options?.limit) {
                const lines = content.split("\n");
                const startLine = options.offset ? options.offset - 1 : 0;
                const endLine = options.limit ? startLine + options.limit : lines.length;
                return lines.slice(startLine, endLine).join("\n");
            }
            return content;
        }
        /**
         * Writes content to a file as a draft.
         *
         * Handles virtual path translation for files managed by the VirtualFileSystem:
         * - Virtual paths are written via the appropriate handler
         * - Regular files are written via the draft interface
         *
         * @param relativePath - Virtual path relative to app root
         * @param content - File content to write
         * @param options - Optional write options
         * @param options.bypassRequireRead - Skip the read-before-write check (use for new files)
         * @throws Error if path is blacklisted, restricted, or draft manager not configured
         * @throws Error if file requires reading first (unless bypassed)
         */
        async writeFile(relativePath, content, options) {
            if (!this.draftManager) {
                throw new Error("DraftInterface is required to write files. Configure AppShell with a draftInterface.");
            }
            // Enforce read-before-write unless explicitly bypassed
            if (!options?.bypassRequireRead && this.isReadRequired(relativePath)) {
                throw new Error(`Cannot write to ${relativePath} without reading it first. Please read the file before attempting to write to it.`);
            }
            const executeStart = performance.now();
            await this.writeFileInternal(relativePath, content);
            llmobs.annotate({
                metadata: {
                    appShellQueueExecuteMs: Math.round(performance.now() - executeStart),
                    appShellQueueOperation: "writeFile",
                    appShellQueuePath: relativePath,
                },
            });
        }
        /**
         * Internal write implementation.
         * Called directly by writeFile (no queue wrapper needed since draftInterface queues).
         */
        async writeFileInternal(relativePath, content) {
            // Handle virtual paths
            if (this.virtualFs.isVirtualPath(relativePath)) {
                await this.virtualFs.write(relativePath, content, {
                    appRootDirPath: this.appRootDirPath,
                });
                // Also create a draft marker for the virtual path
                await this.draftManager.createDraftMarker(relativePath);
                return;
            }
            // Validate the operation for non-virtual files
            validateFileOperation("write", relativePath, this.appRootDirPath, undefined, this.pathValidator, this.getFileOperationBlacklistOptions());
            // Ensure parent directory exists
            const absolutePath = path.join(this.appRootDirPath, relativePath);
            await mkdir(path.dirname(absolutePath), { recursive: true });
            // Create draft (this queues internally via draftInterface)
            await this.draftManager.createDraft(relativePath, content);
        }
        /**
         * Deletes a file or directory as a draft operation.
         *
         * Handles virtual path translation for files managed by the VirtualFileSystem:
         * - Virtual paths delete via the appropriate handler
         * - Regular files/directories are deleted via the draft interface
         *
         * @param relativePath - Virtual path relative to app root
         * @throws Error if path is protected or draft manager not configured
         */
        async deleteFile(relativePath) {
            if (!this.draftManager) {
                throw new Error("DraftInterface is required to delete files. Configure AppShell with a draftInterface.");
            }
            await this.deleteFileInternal(relativePath);
        }
        /**
         * Internal delete implementation.
         * Called directly by deleteFile (no queue wrapper needed since draftInterface queues).
         */
        async deleteFileInternal(relativePath) {
            // Handle virtual paths
            if (this.virtualFs.isVirtualPath(relativePath)) {
                await this.virtualFs.delete(relativePath, {
                    appRootDirPath: this.appRootDirPath,
                    createDraftDelete: this.draftManager.createDraftDelete.bind(this.draftManager),
                });
                return;
            }
            // Validate the operation for non-virtual files
            validateFileOperation("delete", relativePath, this.appRootDirPath, undefined, this.pathValidator, this.getFileOperationBlacklistOptions());
            const absolutePath = path.join(this.appRootDirPath, relativePath);
            // Check if path exists and what type it is
            let stats;
            try {
                stats = await stat(absolutePath);
            }
            catch {
                throw new Error(`File or directory ${relativePath} not found`);
            }
            if (stats.isDirectory()) {
                // Recursively delete directory contents
                await this.deleteDirectoryRecursive(absolutePath);
                // Remove the directory itself
                await rm(absolutePath, { recursive: true, force: true });
            }
            else {
                // Single file deletion
                await this.draftManager.createDraftDelete(relativePath);
            }
        }
        /**
         * Implementation of listFiles supporting both signatures.
         */
        async listFiles(patterns, options) {
            return await this.enqueueFsOperation("listFiles", async () => {
                const { includeMetadata, ...restOptions } = options ?? {};
                // Get virtualized file paths
                const allPaths = await this.listFilesInternal(patterns, restOptions);
                // Return paths only if metadata not requested
                if (!includeMetadata) {
                    return allPaths;
                }
                // Gather metadata for each path
                return await this.getFileMetadataList(allPaths);
            }, {
                appShellQueueIncludeMetadata: Boolean(options?.includeMetadata),
                appShellQueuePatternCount: patterns?.length ?? 0,
            });
        }
        /**
         * Internal list implementation (no queue).
         * Use this from other methods that are already enqueued to avoid deadlock.
         *
         * @param patterns - Glob patterns to match
         * @param options - Glob options
         * @returns Array of virtualized file paths (physical API paths converted to virtual)
         */
        async listFilesInternal(patterns, options) {
            const patternsToUse = patterns ?? [...APPLICATION_FILE_PATTERNS];
            const { includeDirs, ...globOptions } = options ?? {};
            const files = await glob(patternsToUse, {
                cwd: this.appRootDirPath,
                absolute: false,
                nodir: !includeDirs,
                ignore: [...STANDARD_IGNORE_PATTERNS],
                ...globOptions,
            });
            // Convert physical paths to virtual paths using VFS
            const virtualPaths = this.virtualFs.toVirtualPaths(files);
            // Discover virtual files from scratch (newly created, not yet persisted)
            const scratchApiFiles = await glob(".superblocks/scratch/apis%2F*.ts", {
                cwd: this.appRootDirPath,
                absolute: false,
                nodir: true,
            });
            const scratchVirtualPaths = scratchApiFiles
                .map((f) => {
                const match = f.match(/apis%2F(.+)\.ts$/);
                return match ? `apis/${match[1]}.ts` : null;
            })
                .filter((f) => f !== null);
            // Combine and deduplicate
            return Array.from(new Set([...virtualPaths, ...scratchVirtualPaths]));
        }
        /**
         * Gets metadata for a list of file paths.
         *
         * @param paths - Array of virtual paths to get metadata for
         * @returns Array of FileMetadata objects (excludes paths that don't exist)
         */
        async getFileMetadataList(paths) {
            const results = [];
            for (const virtualPath of paths) {
                const metadata = await this.getFileMetadata(virtualPath);
                if (metadata) {
                    results.push(metadata);
                }
            }
            return results;
        }
        /**
         * Gets metadata for a single file path.
         *
         * @param virtualPath - The virtual path to get metadata for
         * @returns FileMetadata or undefined if file doesn't exist
         */
        async getFileMetadata(virtualPath) {
            // Check if virtual file - use handler's getMetadata if available
            if (this.virtualFs.isVirtualPath(virtualPath)) {
                const handler = this.virtualFs.getHandler(virtualPath);
                if (handler?.getMetadata) {
                    return handler.getMetadata(virtualPath, {
                        appRootDirPath: this.appRootDirPath,
                    });
                }
                // Fallback: derive metadata from scratch file for API files
                return this.getApiFileMetadata(virtualPath);
            }
            // Regular file - use fs.stat
            const absolutePath = path.join(this.appRootDirPath, virtualPath);
            try {
                const stats = await stat(absolutePath);
                return {
                    path: virtualPath,
                    size: stats.size,
                    mtime: stats.mtime,
                    isDirectory: stats.isDirectory(),
                    isFile: stats.isFile(),
                };
            }
            catch {
                return undefined;
            }
        }
        /**
         * Gets metadata for a virtual API file.
         * Checks scratch file first (for pending changes), then falls back to YAML.
         *
         * @param virtualPath - The virtual API path (e.g., "apis/MyAPI.ts")
         * @returns FileMetadata or undefined if not found
         */
        async getApiFileMetadata(virtualPath) {
            // Check scratch file first (for pending changes)
            const scratchPath = `.superblocks/scratch/${encodeURIComponent(virtualPath)}`;
            const absoluteScratchPath = path.join(this.appRootDirPath, scratchPath);
            try {
                const stats = await stat(absoluteScratchPath);
                return {
                    path: virtualPath,
                    size: stats.size,
                    mtime: stats.mtime,
                    isDirectory: false,
                    isFile: true,
                };
            }
            catch {
                // Fall back to YAML directory
            }
            // Try to find the API YAML file
            const info = this.virtualFs.parse(virtualPath);
            if (info?.params.apiName) {
                const yamlPath = `apis/${info.params.apiName}/api.yaml`;
                const absoluteYamlPath = path.join(this.appRootDirPath, yamlPath);
                try {
                    const stats = await stat(absoluteYamlPath);
                    return {
                        path: virtualPath,
                        size: stats.size,
                        mtime: stats.mtime,
                        isDirectory: false,
                        isFile: true,
                    };
                }
                catch {
                    return undefined;
                }
            }
            return undefined;
        }
        /**
         * Renames or moves a file.
         *
         * Implemented as read + write + delete to work with the draft system.
         *
         * @param oldPath - Current virtual path
         * @param newPath - New virtual path
         * @throws Error if old file doesn't exist or new path is invalid
         * @throws Error if old file requires reading first
         */
        async renameFile(oldPath, newPath) {
            if (!this.draftManager) {
                throw new Error("DraftInterface is required to rename files. Configure AppShell with a draftInterface.");
            }
            // Validate both paths
            validateFileOperation("rename", oldPath, this.appRootDirPath, undefined, this.pathValidator, this.getFileOperationBlacklistOptions());
            validateFileOperation("write", newPath, this.appRootDirPath, undefined, this.pathValidator, this.getFileOperationBlacklistOptions());
            // Enforce read-before-write on the source file
            if (this.isReadRequired(oldPath)) {
                throw new Error(`Cannot rename ${oldPath} without reading it first. Please read the file before attempting to rename it.`);
            }
            // Read content first (this is just fs read, no queue needed)
            const content = await this.readFileInternal(oldPath);
            await this.writeFileInternal(newPath, content);
            await this.deleteFileInternal(oldPath);
        }
        // ============================================================================
        // Draft Management
        // ============================================================================
        /**
         * Checks if there are any pending draft changes.
         *
         * @returns true if there are uncommitted draft changes
         */
        async hasDraftChanges() {
            if (!this.draftManager) {
                return false;
            }
            return await this.draftManager.hasDraftChanges();
        }
        /**
         * Gets a unified diff of pending draft changes.
         *
         * @param paths - Optional specific paths to diff. If empty, diffs all changes.
         * @returns Unified diff string
         */
        async getDraftDiff(paths) {
            if (!this.draftManager) {
                return "";
            }
            return await this.draftManager.getDraftDiff(paths);
        }
        /**
         * Gets categorized draft changes.
         *
         * @returns Object containing arrays of edited, added, and deleted paths
         */
        async getDraftChanges() {
            if (!this.draftManager) {
                return { edits: [], additions: [], deletions: [] };
            }
            return await this.draftManager.getDraftChanges();
        }
        /**
         * Gets the underlying draft interface for direct access.
         *
         * This should only be used for advanced operations that require
         * direct access to the draft interface. Prefer using the AppShell
         * methods when possible.
         *
         * @internal
         */
        getDraftInterface() {
            return this.draftManager?.getInterface();
        }
        // ============================================================================
        // Read Tracking (Read-Before-Write Enforcement)
        // ============================================================================
        /**
         * Check if a file must be read before it can be written.
         *
         * @param relativePath - The path to check
         * @returns true if the file must be read before writing
         */
        isReadRequired(relativePath) {
            return this.requiredReads.has(relativePath);
        }
        /**
         * Mark a file as requiring a read before write.
         *
         * Call this when a file has been modified externally (e.g., by lint/format)
         * to ensure the AI reads the updated content before making further changes.
         *
         * @param relativePath - The path to mark as requiring read
         */
        requireRead(relativePath) {
            this.requiredReads.add(relativePath);
        }
        /**
         * Mark a file as having been read, allowing subsequent writes.
         * Called internally after successful read operations.
         */
        markRead(relativePath) {
            this.requiredReads.delete(relativePath);
        }
        getFileOperationBlacklistOptions() {
            return {
                useLessProtectedPatterns: this.enableDeletingLockfiles,
            };
        }
        // ============================================================================
        // Private Helpers
        // ============================================================================
        /**
         * Recursively deletes directory contents by creating draft deletes.
         */
        async deleteDirectoryRecursive(dirPath) {
            if (!this.draftManager) {
                throw new Error("DraftInterface is required for directory deletion");
            }
            const entries = await readdir(dirPath, { withFileTypes: true });
            for (const entry of entries) {
                const fullPath = path.join(dirPath, entry.name);
                const relativePath = path.relative(this.appRootDirPath, fullPath);
                if (entry.isDirectory()) {
                    await this.deleteDirectoryRecursive(fullPath);
                }
                else {
                    await this.draftManager.createDraftDelete(relativePath);
                }
            }
        }
    };
})();
export { AppShell };
//# sourceMappingURL=shell.js.map