"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;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.isSafeToCreateProjectIn = isSafeToCreateProjectIn;
exports.scaffoldAlreadyExists = scaffoldAlreadyExists;
exports.checkNodeVersion = checkNodeVersion;
exports.bunIsAvailable = bunIsAvailable;
exports.checkThatNpmCanReadCwd = checkThatNpmCanReadCwd;
exports.installPackages = installPackages;
exports.getDependencies = getDependencies;
exports.checkDbConnection = checkDbConnection;
exports.isDockerAvailable = isDockerAvailable;
exports.startPostgresDatabase = startPostgresDatabase;
exports.isServerPortInUse = isServerPortInUse;
exports.checkCancel = checkCancel;
exports.cleanUpDockerResources = cleanUpDockerResources;
exports.resolvePackageRootDir = resolvePackageRootDir;
exports.downloadAndExtractStorefront = downloadAndExtractStorefront;
exports.findAvailablePort = findAvailablePort;
const prompts_1 = require("@clack/prompts");
const cross_spawn_1 = __importDefault(require("cross-spawn"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const node_child_process_1 = require("node:child_process");
const node_fs_1 = require("node:fs");
const node_os_1 = require("node:os");
const node_stream_1 = require("node:stream");
const promises_1 = require("node:stream/promises");
const node_util_1 = require("node:util");
const path_1 = __importDefault(require("path"));
const picocolors_1 = __importDefault(require("picocolors"));
const semver_1 = __importDefault(require("semver"));
const tar = __importStar(require("tar"));
const constants_1 = require("./constants");
const logger_1 = require("./logger");
/**
 * If project only contains files generated by GH, it’s safe.
 * Also, if project contains remnant error logs from a previous
 * installation, lets remove them now.
 * We also special case IJ-based products .idea because it integrates with CRA:
 * https://github.com/facebook/create-react-app/pull/368#issuecomment-243446094
 */
function isSafeToCreateProjectIn(root, name) {
    // These files should be allowed to remain on a failed install,
    // but then silently removed during the next create.
    const errorLogFilePatterns = ['npm-debug.log', 'yarn-error.log', 'yarn-debug.log'];
    const validFiles = [
        '.DS_Store',
        'Thumbs.db',
        '.git',
        '.gitignore',
        '.idea',
        'README.md',
        'LICENSE',
        '.hg',
        '.hgignore',
        '.hgcheck',
        '.npmignore',
        'mkdocs.yml',
        'docs',
        '.travis.yml',
        '.gitlab-ci.yml',
        '.gitattributes',
        'migration.ts',
        'node_modules',
        'package.json',
        'package-lock.json',
        'src',
        'static',
        'tsconfig.json',
        'yarn.lock',
    ];
    const conflicts = fs_extra_1.default
        .readdirSync(root)
        .filter(file => !validFiles.includes(file))
        // IntelliJ IDEA creates module files before CRA is launched
        .filter(file => !/\.iml$/.test(file))
        // Don't treat log files from previous installation as conflicts
        .filter(file => !errorLogFilePatterns.some(pattern => file.indexOf(pattern) === 0));
    if (conflicts.length > 0) {
        (0, logger_1.log)(`The directory ${picocolors_1.default.green(name)} contains files that could conflict:`, { newline: 'after' });
        for (const file of conflicts) {
            (0, logger_1.log)(`  ${file}`);
        }
        (0, logger_1.log)('Either try using a new directory name, or remove the files listed above.', {
            newline: 'before',
        });
        return false;
    }
    // Remove any remnant files from a previous installation
    const currentFiles = fs_extra_1.default.readdirSync(path_1.default.join(root));
    currentFiles.forEach(file => {
        errorLogFilePatterns.forEach(errorLogFilePattern => {
            // This will catch `(npm-debug|yarn-error|yarn-debug).log*` files
            if (file.indexOf(errorLogFilePattern) === 0) {
                fs_extra_1.default.removeSync(path_1.default.join(root, file));
            }
        });
    });
    return true;
}
function scaffoldAlreadyExists(root, name) {
    const scaffoldFiles = ['migration.ts', 'package.json', 'tsconfig.json', 'README.md'];
    const files = fs_extra_1.default.readdirSync(root);
    return scaffoldFiles.every(scaffoldFile => files.includes(scaffoldFile));
}
function checkNodeVersion(requiredVersion) {
    if (!semver_1.default.satisfies(process.version, requiredVersion)) {
        (0, logger_1.log)(picocolors_1.default.red(`You are running Node ${process.version}.` +
            `Vendure requires Node ${requiredVersion} or higher.` +
            'Please update your version of Node.'));
        process.exit(1);
    }
}
// Bun support should not be exposed yet, see
// https://github.com/oven-sh/bun/issues/4947
// https://github.com/lovell/sharp/issues/3511
function bunIsAvailable() {
    try {
        (0, node_child_process_1.execFileSync)('bun', ['--version'], { stdio: 'ignore' });
        return true;
    }
    catch (e) {
        return false;
    }
}
function checkThatNpmCanReadCwd() {
    const cwd = process.cwd();
    let childOutput = null;
    try {
        // Note: intentionally using spawn over exec since
        // the problem doesn't reproduce otherwise.
        // `npm config list` is the only reliable way I could find
        // to reproduce the wrong path. Just printing process.cwd()
        // in a Node process was not enough.
        childOutput = cross_spawn_1.default.sync('npm', ['config', 'list']).output.join('');
    }
    catch (err) {
        // Something went wrong spawning node.
        // Not great, but it means we can't do this check.
        // We might fail later on, but let's continue.
        return true;
    }
    if (typeof childOutput !== 'string') {
        return true;
    }
    const lines = childOutput.split('\n');
    // `npm config list` output includes the following line:
    // "; cwd = C:\path\to\current\dir" (unquoted)
    // I couldn't find an easier way to get it.
    const prefix = '; cwd = ';
    const line = lines.find(l => l.indexOf(prefix) === 0);
    if (typeof line !== 'string') {
        // Fail gracefully. They could remove it.
        return true;
    }
    const npmCWD = line.substring(prefix.length);
    if (npmCWD === cwd) {
        return true;
    }
    (0, logger_1.log)(picocolors_1.default.red('Could not start an npm process in the right directory.\n\n' +
        `The current directory is: ${picocolors_1.default.bold(cwd)}\n` +
        `However, a newly started npm process runs in: ${picocolors_1.default.bold(npmCWD)}\n\n` +
        'This is probably caused by a misconfigured system terminal shell.'));
    if (process.platform === 'win32') {
        (0, logger_1.log)(picocolors_1.default.red('On Windows, this can usually be fixed by running:\n\n') +
            `  ${picocolors_1.default.cyan('reg')} delete "HKCU\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n` +
            `  ${picocolors_1.default.cyan('reg')} delete "HKLM\\Software\\Microsoft\\Command Processor" /v AutoRun /f\n\n` +
            picocolors_1.default.red('Try to run the above two lines in the terminal.\n') +
            picocolors_1.default.red('To learn more about this problem, read: https://blogs.msdn.microsoft.com/oldnewthing/20071121-00/?p=24433/'));
    }
    return false;
}
/**
 * Install packages via npm.
 * Based on the install function from https://github.com/facebook/create-react-app
 */
function installPackages(options) {
    const { dependencies, isDevDependencies = false, logLevel, cwd } = options;
    return new Promise((resolve, reject) => {
        const command = 'npm';
        const args = ['install', '--save', '--save-exact', '--loglevel', 'error'].concat(dependencies);
        if (isDevDependencies) {
            args.push('--save-dev');
        }
        if (logLevel === 'verbose') {
            args.push('--verbose');
        }
        const child = (0, cross_spawn_1.default)(command, args, {
            stdio: logLevel === 'verbose' ? 'inherit' : 'ignore',
            cwd,
        });
        child.on('close', code => {
            if (code !== 0) {
                let message = 'An error occurred when installing dependencies.';
                if (logLevel === 'silent') {
                    message += ' Try running with `--log-level verbose` to diagnose.';
                }
                reject({
                    message,
                    command: `${command} ${args.join(' ')}`,
                });
                return;
            }
            resolve();
        });
    });
}
function getDependencies(dbType, vendurePkgVersion = '') {
    const dependencies = [
        `@vendure/core${vendurePkgVersion}`,
        `@vendure/email-plugin${vendurePkgVersion}`,
        `@vendure/asset-server-plugin${vendurePkgVersion}`,
        `@vendure/graphiql-plugin${vendurePkgVersion}`,
        `@vendure/dashboard${vendurePkgVersion}`,
        'dotenv',
        dbDriverPackage(dbType),
    ];
    const devDependencies = [
        `@vendure/cli${vendurePkgVersion}`,
        'concurrently',
        'ts-node',
        `typescript@${constants_1.TYPESCRIPT_VERSION}`,
    ];
    return { dependencies, devDependencies };
}
/**
 * Returns the name of the npm driver package for the
 * selected database.
 */
function dbDriverPackage(dbType) {
    switch (dbType) {
        case 'mysql':
        case 'mariadb':
            return 'mysql2';
        case 'postgres':
            return 'pg';
        case 'sqlite':
            return 'better-sqlite3';
        default:
            const n = dbType;
            (0, logger_1.log)(picocolors_1.default.red(`No driver package configured for type "${dbType}"`));
            return '';
    }
}
/**
 * Checks that the specified DB connection options are working (i.e. a connection can be
 * established) and that the named database exists.
 */
function checkDbConnection(options, root) {
    switch (options.type) {
        case 'mysql':
            return checkMysqlDbExists(options, root);
        case 'postgres':
            return checkPostgresDbExists(options, root);
        default:
            return Promise.resolve(true);
    }
}
async function checkMysqlDbExists(options, root) {
    // Use require.resolve to find the package, which handles npm workspace hoisting
    const mysqlPath = require.resolve('mysql2/promise', { paths: [root] });
    const mysql = await Promise.resolve(`${mysqlPath}`).then(s => __importStar(require(s)));
    const connectionOptions = {
        host: options.host,
        user: options.username,
        password: options.password,
        port: options.port,
        database: options.database,
    };
    let connection;
    try {
        // mysql2/promise's createConnection returns a Promise that resolves
        // to a connection once connected, so we must await it.
        connection = await mysql.createConnection(connectionOptions);
    }
    catch (err) {
        if (err.code === 'ER_BAD_DB_ERROR') {
            throwDatabaseDoesNotExist(options.database);
        }
        throwConnectionError(err);
    }
    await connection.end();
    return true;
}
async function checkPostgresDbExists(options, root) {
    // Use require.resolve to find the package, which handles npm workspace hoisting
    const pgPath = require.resolve('pg', { paths: [root] });
    const { Client } = await Promise.resolve(`${pgPath}`).then(s => __importStar(require(s)));
    const connectionOptions = {
        host: options.host,
        user: options.username,
        password: options.password,
        port: options.port,
        database: options.database,
        schema: options.schema,
        ssl: options.ssl,
    };
    const client = new Client(connectionOptions);
    try {
        await client.connect();
        const schema = await client.query(`SELECT schema_name
             FROM information_schema.schemata
             WHERE schema_name = '${options.schema}'`);
        if (schema.rows.length === 0) {
            throw new Error('NO_SCHEMA');
        }
    }
    catch (e) {
        if (e.code === '3D000') {
            throwDatabaseDoesNotExist(options.database);
        }
        else if (e.message === 'NO_SCHEMA') {
            throwDatabaseSchemaDoesNotExist(options.database, options.schema);
        }
        else if (e.code === '28000') {
            throwSSLConnectionError(e, options.ssl);
        }
        throwConnectionError(e);
        await client.end();
        throw e;
    }
    await client.end();
    return true;
}
/**
 * Check to see if Docker is installed and running.
 * If not, attempt to start it.
 * If that is not possible, return false.
 *
 * Refs:
 * - https://stackoverflow.com/a/48843074/772859
 */
async function isDockerAvailable() {
    const dockerSpinner = (0, prompts_1.spinner)();
    function isDaemonRunning() {
        try {
            (0, node_child_process_1.execFileSync)('docker', ['stats', '--no-stream'], { stdio: 'ignore' });
            return true;
        }
        catch (e) {
            return false;
        }
    }
    dockerSpinner.start('Checking for Docker');
    try {
        (0, node_child_process_1.execFileSync)('docker', ['-v'], { stdio: 'ignore' });
        dockerSpinner.message('Docker was found!');
    }
    catch (e) {
        dockerSpinner.stop('Docker was not found on this machine. We will use SQLite for the database.');
        return { result: 'not-found' };
    }
    // Now we need to check if the docker daemon is running
    const isRunning = isDaemonRunning();
    if (isRunning) {
        dockerSpinner.stop('Docker is running');
        return { result: 'running' };
    }
    dockerSpinner.message('Docker daemon is not running. Attempting to start');
    // detect the current OS
    const currentPlatform = (0, node_os_1.platform)();
    try {
        if (currentPlatform === 'win32') {
            // https://stackoverflow.com/a/44182489/772859
            (0, node_child_process_1.execSync)('"C:\\Program Files\\Docker\\Docker\\Docker Desktop.exe"', { stdio: 'ignore' });
        }
        else if (currentPlatform === 'darwin') {
            (0, node_child_process_1.execSync)('open -a Docker', { stdio: 'ignore' });
        }
        else {
            (0, node_child_process_1.execSync)('systemctl start docker', { stdio: 'ignore' });
        }
    }
    catch (e) {
        dockerSpinner.stop('Could not start Docker.');
        (0, logger_1.log)(e.message, { level: 'verbose' });
        return { result: 'not-running' };
    }
    // Verify that the daemon is now running
    let attempts = 1;
    do {
        (0, logger_1.log)(`Checking for Docker daemon... (attempt ${attempts})`, { level: 'verbose' });
        if (isDaemonRunning()) {
            (0, logger_1.log)(`Docker daemon is now running (after ${attempts} attempts).`, { level: 'verbose' });
            dockerSpinner.stop('Docker is running');
            return { result: 'running' };
        }
        await new Promise(resolve => setTimeout(resolve, 50));
        attempts++;
    } while (attempts < 100);
    dockerSpinner.stop('Docker daemon could not be started');
    return { result: 'not-running' };
}
async function startPostgresDatabase(root) {
    var _a;
    // Now we need to run the postgres database via Docker
    let containerName;
    const postgresContainerSpinner = (0, prompts_1.spinner)();
    postgresContainerSpinner.start('Starting PostgreSQL database');
    try {
        const result = await (0, node_util_1.promisify)(node_child_process_1.execFile)(`docker`, [
            `compose`,
            `-f`,
            path_1.default.join(root, 'docker-compose.yml'),
            `up`,
            `-d`,
            `postgres_db`,
        ]);
        containerName = (_a = result.stderr.match(/Container\s+(.+-postgres_db[^ ]*)/)) === null || _a === void 0 ? void 0 : _a[1];
        if (!containerName) {
            // guess the container name based on the directory name
            containerName = path_1.default.basename(root).replace(/[^a-z0-9]/gi, '') + '-postgres_db-1';
            postgresContainerSpinner.message('Could not find container name. Guessing it is: ' + containerName);
            (0, logger_1.log)(picocolors_1.default.red('Could not find container name. Guessing it is: ' + containerName), {
                newline: 'before',
                level: 'verbose',
            });
        }
        else {
            (0, logger_1.log)(picocolors_1.default.green(`Started PostgreSQL database in container "${containerName}"`), {
                newline: 'before',
                level: 'verbose',
            });
        }
    }
    catch (e) {
        (0, logger_1.log)(picocolors_1.default.red(`Failed to start PostgreSQL database: ${e.message}`));
        postgresContainerSpinner.stop('Failed to start PostgreSQL database');
        return false;
    }
    postgresContainerSpinner.message(`Waiting for PostgreSQL database to be ready...`);
    let attempts = 1;
    let isReady = false;
    do {
        // We now need to ensure that the database is ready to accept connections
        try {
            const result = (0, node_child_process_1.execFileSync)(`docker`, [`exec`, `-i`, containerName, `pg_isready`]);
            isReady = result === null || result === void 0 ? void 0 : result.toString().includes('accepting connections');
            if (!isReady) {
                (0, logger_1.log)(picocolors_1.default.yellow(`PostgreSQL database not yet ready. Attempt ${attempts}...`), {
                    level: 'verbose',
                });
            }
        }
        catch (e) {
            // ignore
            (0, logger_1.log)('is_ready error:' + e.message, { level: 'verbose', newline: 'before' });
        }
        await new Promise(resolve => setTimeout(resolve, 50));
        attempts++;
    } while (!isReady && attempts < 100);
    postgresContainerSpinner.stop('PostgreSQL database is ready');
    return true;
}
function throwConnectionError(err) {
    throw new Error('Could not connect to the database. ' +
        `Please check the connection settings in your Vendure config.\n[${(err.message || err.toString())}]`);
}
function throwSSLConnectionError(err, sslEnabled) {
    throw new Error('Could not connect to the database. ' +
        (sslEnabled === undefined
            ? 'Is your server requiring an SSL connection?'
            : 'Are you sure your server supports SSL?') +
        `Please check the connection settings in your Vendure config.\n[${(err.message || err.toString())}]`);
}
function throwDatabaseDoesNotExist(name) {
    throw new Error(`Database "${name}" does not exist. Please create the database and then try again.`);
}
function throwDatabaseSchemaDoesNotExist(dbName, schemaName) {
    throw new Error(`Schema "${dbName}.${schemaName}" does not exist. Please create the schema "${schemaName}" and then try again.`);
}
function isServerPortInUse(port) {
    // eslint-disable-next-line @typescript-eslint/no-var-requires
    const tcpPortUsed = require('tcp-port-used');
    try {
        return tcpPortUsed.check(port);
    }
    catch (e) {
        (0, logger_1.log)(picocolors_1.default.yellow(`Warning: could not determine whether port ${port} is available`));
        return Promise.resolve(false);
    }
}
/**
 * Checks if the response from a Clack prompt was a cancellation symbol, and if so,
 * ends the interactive process.
 */
function checkCancel(value) {
    if ((0, prompts_1.isCancel)(value)) {
        (0, prompts_1.cancel)('Setup cancelled.');
        process.exit(0);
    }
    return true;
}
function cleanUpDockerResources(name) {
    try {
        (0, node_child_process_1.execSync)(`docker stop $(docker ps -a -q --filter "label=io.vendure.create.name=${name}")`, {
            stdio: 'ignore',
        });
        (0, node_child_process_1.execSync)(`docker rm $(docker ps -a -q --filter "label=io.vendure.create.name=${name}")`, {
            stdio: 'ignore',
        });
        (0, node_child_process_1.execSync)(`docker volume rm $(docker volume ls --filter "label=io.vendure.create.name=${name}" -q)`, {
            stdio: 'ignore',
        });
    }
    catch (e) {
        (0, logger_1.log)(picocolors_1.default.yellow(`Could not clean up Docker resources`), { level: 'verbose' });
    }
}
function resolvePackageRootDir(packageName, rootDir) {
    let packageEntryPath;
    try {
        packageEntryPath = require.resolve(packageName, { paths: [rootDir] });
    }
    catch (_a) {
        (0, logger_1.log)(`Falling back to direct node_modules lookup for ${packageName}`);
        const fallbackPath = path_1.default.join(process.cwd(), 'node_modules', packageName);
        if (fs_extra_1.default.existsSync(fallbackPath)) {
            return fallbackPath;
        }
        throw new Error(`Cannot resolve package "${packageName}" (checked in ${fallbackPath}). Is it installed?`);
    }
    const target = packageName.split('/').pop();
    let dir = path_1.default.dirname(packageEntryPath);
    const root = path_1.default.parse(dir).root;
    while (path_1.default.basename(dir) !== target) {
        const next = path_1.default.dirname(dir);
        if (next === dir || dir === root) {
            throw new Error(`Could not locate package root for "${packageName}" from "${packageEntryPath}".`);
        }
        dir = next;
    }
    return dir;
}
/**
 * Downloads the Next.js storefront starter from GitHub and extracts it to the target directory.
 * Uses the GitHub API tarball endpoint to avoid requiring git.
 */
async function downloadAndExtractStorefront(targetDir) {
    const tarballUrl = `https://api.github.com/repos/${constants_1.STOREFRONT_REPO}/tarball/${constants_1.STOREFRONT_BRANCH}`;
    const tempTarPath = path_1.default.join(targetDir, '..', 'storefront-temp.tar.gz');
    try {
        // Fetch the tarball from GitHub
        const response = await fetch(tarballUrl, {
            headers: {
                Accept: 'application/vnd.github+json',
                'User-Agent': 'vendure-create',
            },
        });
        if (!response.ok) {
            throw new Error(`Failed to download storefront: ${response.status} ${response.statusText}`);
        }
        // Save the tarball to a temp file
        const fileStream = (0, node_fs_1.createWriteStream)(tempTarPath);
        // Convert the web ReadableStream to a Node.js Readable stream
        const nodeReadable = node_stream_1.Readable.fromWeb(response.body);
        await (0, promises_1.pipeline)(nodeReadable, fileStream);
        // Create target directory
        await fs_extra_1.default.ensureDir(targetDir);
        // Extract the tarball
        await tar.extract({
            file: tempTarPath,
            cwd: targetDir,
            strip: 1, // Remove the top-level directory from the archive
        });
        // Clean up temp file
        await fs_extra_1.default.remove(tempTarPath);
    }
    catch (error) {
        // Clean up on error
        await fs_extra_1.default.remove(tempTarPath).catch(() => {
            // eslint-disable-next-line
            console.error(error);
        });
        throw error;
    }
}
/**
 * Finds an available port starting from the given port.
 * Returns the first available port within the specified range.
 */
async function findAvailablePort(startPort, range = 20) {
    let port = startPort;
    while (await isServerPortInUse(port)) {
        port++;
        if (port > startPort + range) {
            throw new Error(`Could not find an available port between ${startPort} and ${startPort + range}`);
        }
    }
    return port;
}
//# sourceMappingURL=helpers.js.map