/* CasualOS is a set of web-based tools designed to facilitate the creation of real-time, multi-user, context-aware interactive experiences.
 *
 * Copyright (c) 2019-2025 Casual Simulation, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
import prompts from 'prompts';
import { Command } from 'commander';
import { Mime } from 'mime';
/* eslint-disable casualos/no-non-type-imports */
import { createRecordsClient } from '@casual-simulation/aux-records/RecordsClient';
import { askForInputs, onState } from './schema';
import repl from 'node:repl';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import Conf from 'conf';
import { calculateStringTagValue, constructInitializationUpdate, createBot, createInitializationUpdate, DATE_TAG_PREFIX, DNA_TAG_PREFIX, failure, getBotsStateFromStoredAux, getSessionKeyExpiration, hasValue, isExpired, isFailure, isFormula, isModule, isScript, LIBRARY_SCRIPT_PREFIX, merge, NUMBER_TAG_PREFIX, parseFormula, parseModule, parseScript, parseSessionKey, parseVersionNumber, ROTATION_TAG_PREFIX, STRING_TAG_PREFIX, success, tryParseJson, VECTOR_TAG_PREFIX, willExpire, wrap, } from '@casual-simulation/aux-common';
import { formatVersionSpecifier, getServerConfigSchema, STORED_AUX_SCHEMA, } from '@casual-simulation/aux-records';
import { PassThrough } from 'node:stream';
import { getSchemaMetadata } from '@casual-simulation/aux-common';
import path from 'path';
import { readFile } from 'fs/promises';
import { setupInfraCommands } from 'infra';
import { z } from 'zod';
import { existsSync, statSync } from 'node:fs';
import { mkdir, readdir, stat, writeFile } from 'node:fs/promises';
import { v4 as uuid } from 'uuid';
import fastJsonStableStringify from '../fast-json-stable-stringify';
import { minifyBots } from './minify';
import { resolveRecordFileInfo, uploadFile, } from '@casual-simulation/aux-vm/managers/RecordsManager';
import standardMimeTypes from 'mime/types/standard.js';
import otherMimeTypes from 'mime/types/other.js';
import { parseEntitlements } from 'cli-utils';
import { Transpiler } from '@casual-simulation/aux-runtime';
import { groupBy } from 'es-toolkit';
const mime = new Mime(standardMimeTypes, otherMimeTypes, {
    'application/json': ['aux', 'json'],
});
const REFRESH_LIFETIME_MS = 1000 * 60 * 60 * 24 * 7; // 1 week
const config = new Conf({
    projectName: 'casualos-cli',
});
const program = new Command();
program
    .name('casualos')
    .description('A CLI for CasualOS')
    .version(GIT_TAG)
    .option('-e, --endpoint <url>', 'The endpoint to use for queries. Can be used to override the current endpoint.')
    .option('--origin <origin>', 'The HTTP origin to use for requests. If not specified, the origin associated with the endpoint will be used. If none is available, then the endpoint is used.')
    .option('--session-key <sessionKey>', 'The session key to use for the session. If not specified, then the current session key will be used.');
program
    .command('login')
    .description('Login to the CasualOS API')
    .action(async () => {
    const opts = program.optsWithGlobals();
    const client = await getClient(opts);
    await login(client);
});
program
    .command('logout')
    .description('Logout of the CasualOS API')
    .action(async () => {
    const opts = program.optsWithGlobals();
    const endpoint = await getEndpoint(opts.endpoint);
    saveSessionKey(endpoint, null);
    console.log('Logged out!');
});
program
    .command('set-endpoint')
    .argument('[endpoint]', 'The endpoint to use for queries. If omitted, then you will be prompted to enter an endpoint.')
    .description('Set the endpoint that is currently in use.')
    .action(async (endpoint) => {
    if (endpoint) {
        saveCurrentEndpoint(endpoint);
    }
    else {
        endpoint = await updateEndpoint();
    }
    const origin = getOrigin(endpoint);
    if (origin !== endpoint) {
        const response = await prompts({
            type: 'confirm',
            name: 'confirm',
            message: `The origin for the endpoint (${origin}) is different from the endpoint (${endpoint}) itself. Do you want to keep it?`,
            initial: true,
            onState,
        });
        if (response.confirm) {
            return;
        }
    }
    const originResponse = await prompts({
        type: 'text',
        name: 'origin',
        message: 'Enter the origin to use for requests to this endpoint.',
        initial: endpoint,
        onState,
    });
    saveOrigin(endpoint, originResponse.origin);
});
program
    .command('status')
    .description('Get the status of the current session.')
    .action(async () => {
    const endpoint = getCurrentEndpoint();
    printStatus(endpoint);
});
program
    .command('query')
    .description('Query the CasualOS API')
    .argument('[procedure]', 'The procedure to execute. If omitted, then you will be prompted to select a procedure.')
    .argument('[input]', 'The input to the procedure. If specified, then it will be parsed as JSON. If omitted, then you will be prompted to enter the input.')
    .option('-k, --key <key>', 'The session key to use for the query. If not specified, then the current session key will be used.')
    .action(async (procedure, input, options) => {
    const opts = program.optsWithGlobals();
    const client = await getClient(opts);
    await query(client, procedure, input);
});
program
    .command('repl')
    .description('Start a REPL for the CasualOS API')
    .option('-k, --key <key>', 'The session key to use for the session. If omitted, then the current session key will be used.')
    .addHelpText('after', `\nThe CasualOS REPL allows you to interact with the CasualOS API using a Read-Eval-Print Loop (REPL).\nIt supports JavaScript and has a special function, query([procedure], [input]), that can be used to query the API.`)
    .action(async (options) => {
    const opts = program.optsWithGlobals();
    const endpoint = await getEndpoint(opts.endpoint);
    const client = await getClient(opts);
    let userId = null;
    if (client.sessionKey) {
        const parseResult = parseSessionKey(client.sessionKey);
        if (parseResult) {
            userId = parseResult[0];
        }
    }
    const replIn = new PassThrough();
    process.stdin.pipe(replIn);
    function pauseRepl(func) {
        return async (...args) => {
            process.stdin.unpipe(replIn);
            replIn.pause();
            try {
                return await func(...args);
            }
            finally {
                replIn.resume();
                process.stdin.pipe(replIn);
            }
        };
    }
    const replServer = repl.start({
        prompt: 'casualos > ',
        input: replIn,
        output: process.stdout,
    });
    replServer.on('exit', () => {
        process.stdin.unpipe(replIn);
    });
    Object.defineProperties(replServer.context, {
        query: {
            configurable: false,
            writable: false,
            enumerable: true,
            value: pauseRepl(async (procedure, input) => {
                return await query(client, procedure, input, false, true, replServer);
            }),
        },
        userId: {
            configurable: false,
            writable: false,
            enumerable: true,
            value: userId,
        },
    });
});
const auxActions = new Set(['convert']);
program
    .command('aux')
    .argument('[action]', 'The action to take related to aux files.')
    .option('-ls, --list', 'List possible aux actions')
    .description('Work with aux files and their contents.')
    .action(async (action, options) => {
    if (options.list) {
        console.log(`Possible aux actions: ${Array.from(auxActions).join(', ')}`);
        return;
    }
    if (!auxActions.has(action ?? '')) {
        console.warn(`Unrecognized action "${action ?? ''}" provided.\nUse -ls or --list to view possible actions.`);
    }
    switch (action) {
        case 'convert':
            return await auxConvert();
            break;
        default:
            break;
    }
});
program
    .command('convert-aux')
    .argument('<input>', 'The path of the file to convert. If the -r flag is provided, then this can be a directory and all aux files in the directory will be converted.')
    .argument('<output>', 'The path to write the converted file to. If the -r flag is provided, then this should be a directory and converted files will be written to this directory with the same name as the original file.')
    .option('-r, --recursive', 'Whether to recursively convert aux files in a directory. If this flag is provided, then the path argument can be a directory and all aux files in the directory will be converted.')
    .option('-a, --aux-version <version>', 'The version of aux files to convert to. If not provided, then the latest version (2) will be used.')
    .option('-o, --overwrite', 'Whether to overwrite existing files. If this flag is not provided and the output file already exists, then the conversion will fail.')
    .description('Convert AUX files between formats.')
    .action(async (input, output, options) => {
    const recursive = options.recursive ?? false;
    const overwrite = options.overwrite ?? false;
    const inputPath = path.resolve(input);
    const outputPath = path.resolve(output);
    const auxVersion = z.coerce
        .number()
        .int()
        .min(1)
        .max(2)
        .default(2)
        .parse(options.auxVersion);
    if (auxVersion !== 1 && auxVersion !== 2) {
        console.error('Invalid aux version specified. Must be 1 or 2.');
        process.exit(1);
    }
    console.log(`Converting aux file(s) to version ${auxVersion}...`);
    if (recursive) {
        const files = await readdir(inputPath, {
            withFileTypes: true,
        });
        await mkdir(outputPath, {
            recursive: true,
        });
        for (let file of files) {
            if (!file.isFile() && !file.isSymbolicLink()) {
                continue;
            }
            const fullInputPath = path.join(inputPath, file.name);
            const fullOutputPath = path.join(outputPath, file.name);
            const result = await convertAuxFile(fullInputPath, auxVersion);
            if (isFailure(result)) {
                console.error(`Failed to convert aux file at path ${fullInputPath}: ${result.error.errorMessage}`);
                continue;
            }
            const writeResult = await wrap(() => writeFile(fullOutputPath, JSON.stringify(result.value), {
                flag: overwrite ? 'w' : 'wx',
            }));
            if (isFailure(writeResult)) {
                console.error(`Failed to write converted aux file to path ${fullOutputPath}: ${writeResult.error.error}`);
                continue;
            }
            console.log(`Successfully converted aux file at path ${fullInputPath} to ${fullOutputPath}`);
        }
    }
    else {
        const result = await convertAuxFile(inputPath, auxVersion);
        if (isFailure(result)) {
            console.error(`Failed to convert aux file at path ${inputPath}: ${result.error.errorMessage}`);
            process.exit(1);
        }
        const writeResult = await wrap(() => writeFile(outputPath, JSON.stringify(result.value), {
            flag: overwrite ? 'w' : 'wx',
        }));
        if (isFailure(writeResult)) {
            console.error(`Failed to write converted aux file to path ${outputPath}: ${writeResult.error.error}`);
            process.exit(1);
        }
        console.log(`Successfully converted aux file at path ${inputPath} to ${outputPath}`);
    }
});
function constructAux(state, version) {
    if (version === 2) {
        const update = constructInitializationUpdate(createInitializationUpdate(Object.values(state)));
        const storedAux = {
            version: 2,
            updates: [update],
        };
        return storedAux;
    }
    else {
        return {
            version: 1,
            state,
        };
    }
}
async function convertAuxFile(inputPath, auxVersion) {
    const fileContent = await readAuxFileBots(inputPath);
    if (isFailure(fileContent)) {
        return fileContent;
    }
    const aux = fileContent.value;
    const storedAux = constructAux(aux, auxVersion);
    return success(storedAux);
}
program
    .command('minify-aux')
    .description('Minify an AUX file in place.')
    .argument('<input>', 'The AUX file to minify.')
    .option('-t, --target <...targets>', 'The targets to minify for.')
    .option('-a, --aux-version <version>', 'The version of aux files to produce. If not provided, then the minified file will use the same version as the input file.')
    .action(async (input, options) => {
    const defaultTargets = ['chrome100'];
    const targets = options.target
        ? Array.isArray(options.target)
            ? options.target
            : [options.target]
        : defaultTargets;
    const inputPath = path.resolve(input);
    const auxVersion = z.coerce
        .number()
        .int()
        .min(1)
        .max(2)
        .nullable()
        .optional()
        .parse(options.auxVersion);
    if (auxVersion !== 1 &&
        auxVersion !== 2 &&
        auxVersion !== null &&
        auxVersion !== undefined) {
        console.error('Invalid aux version specified. Must be 1 or 2.');
        process.exit(1);
    }
    const auxJson = await readAuxFile(inputPath);
    if (auxJson.success === false) {
        console.error(auxJson.error.errorMessage);
        process.exit(1);
    }
    else {
        const originalStat = await stat(inputPath);
        const bots = getBotsStateFromStoredAux(auxJson.value);
        const minified = await minifyBots(bots, targets);
        const storedAux = constructAux(minified, (auxVersion ?? auxJson.value.version));
        await writeFile(inputPath, JSON.stringify(storedAux));
        const newStat = await stat(inputPath);
        console.log(`Minified aux file: ${inputPath}`);
        console.log(`Original Size: ${originalStat.size} bytes`);
        console.log(`New Size: ${newStat.size} bytes`);
        console.log(`Targets: ${targets.join(', ')}`);
        console.log(`Version: ${storedAux.version}`);
    }
});
program
    .command('check-aux')
    .description('Check if an AUX file is valid and can be loaded.')
    .argument('<input>', 'The AUX file to check.')
    .option('--skip-scripts', 'Whether to skip checking script tags. By default, script tags are checked and any errors in them will be reported.')
    .action(async (input, options) => {
    // Implement the action for checking the AUX file here.
    const skipScripts = options.skipScripts ?? false;
    const inputPath = path.resolve(input);
    const auxJson = tryParseJson(await readFile(inputPath, 'utf-8'));
    if (auxJson.success === false) {
        console.error(`Could not parse aux file: ${auxJson.error}`);
        process.exit(1);
    }
    const aux = STORED_AUX_SCHEMA.safeParse(auxJson.value);
    if (aux.success === false) {
        console.error(`Aux file is not a valid stored aux: ${aux.error.toString()}`);
        process.exit(1);
    }
    const botsState = getBotsStateFromStoredAux(aux.data);
    const transpiler = new Transpiler();
    function checkCode(bot, tag, space, code) {
        try {
            transpiler.transpile(code);
        }
        catch (err) {
            return failure({
                bot,
                tag,
                space,
                errorCode: 'invalid_script',
                errorMessage: err.toString(),
            });
        }
        return success();
    }
    function checkValue(bot, tag, space, value) {
        if (isFormula(value)) {
            const formula = parseFormula(value);
            const parsed = tryParseJson(formula);
            if (parsed.success === false) {
                return failure({
                    bot,
                    tag,
                    space,
                    errorCode: 'invalid_formula',
                    errorMessage: parsed.error,
                });
            }
        }
        else if (!skipScripts) {
            if (isScript(value)) {
                const code = parseScript(value);
                return checkCode(bot, tag, space, code);
            }
            else if (isModule(value)) {
                const code = parseModule(value);
                return checkCode(bot, tag, space, code);
            }
        }
        return success();
    }
    const errors = [];
    for (let botId in botsState) {
        const bot = botsState[botId];
        for (let tag in bot.tags) {
            const value = bot.tags[tag];
            const result = checkValue(bot, tag, null, value);
            if (isFailure(result)) {
                errors.push(result.error);
            }
        }
        for (let space in bot.masks) {
            const masks = bot.masks[space];
            for (let tag in masks) {
                const value = masks[tag];
                const result = checkValue(bot, tag, space, value);
                if (isFailure(result)) {
                    errors.push(result.error);
                }
            }
        }
    }
    if (errors.length > 0) {
        console.error(`Found ${errors.length} errors in aux file:`);
        const byBot = groupBy(errors, (error) => error.bot.id);
        for (let botId in byBot) {
            const botErrors = byBot[botId];
            const bot = botErrors[0].bot;
            const system = calculateStringTagValue(null, bot, 'system', null);
            console.error(`Bot: ${bot.id}${system ? `, System: ${system}` : ''}`);
            for (let error of botErrors) {
                console.error(`  Tag: ${error.tag}${error.space ? `, Space: ${error.space}` : ''}, Error: (${error.errorCode}) ${error.errorMessage}`);
            }
        }
        process.exit(2);
    }
    else {
        console.log(`Aux file is valid! No errors found in ${Object.keys(botsState).length} bots.`);
    }
});
program
    .command('unpack-aux')
    .argument('<input>', 'The aux file/directory to convert to a file system.')
    .argument('<dir>', 'The directory to write the file system to.')
    .option('-o, --overwrite', 'Overwrite existing files.')
    .option('-r, --recursive', 'Recursively convert aux files in a directory.')
    .option('--write-systemless-bots', "Write bots that don't have a system tag. By default, these bots are written to the extra.aux file.")
    .option('--omit-extra-bots', 'Prevent writing extra.aux files.')
    .option('--preserve-bot-ids', 'Whether to not replace bot IDs with a placeholder. By default, bot IDs are replaced with a placeholder. This prevents bot IDs from being written to the file system and also causes pack-aux to generate new bot IDs. Use this flag to prevent this behavior.')
    .description('Generate a folder from an AUX file.')
    .action(async (input, dir, options) => {
    if (options.overwrite) {
        console.log('Overwriting existing files.');
    }
    if (options.recursive) {
        console.log('Recursively converting aux files in input directory.');
    }
    if (options.writeSystemlessBots) {
        console.log('Writing systemless bots. All bots will be written to the file system.');
    }
    if (options.omitExtraBots) {
        console.log('Omitting extra bots. No extra.aux file(s) will be written.');
    }
    if (options.omitBotIds) {
        console.log('Omitting bot IDs. Bot IDs will be replaced with placeholders.');
    }
    await auxGenFs(input, dir, options);
});
program
    .command('pack-aux')
    .argument('<dir>', 'The directory to read the file system from. If the directory does not contain an extra.aux file, then each directory will be read as a separate aux file.')
    .argument('<output>', 'The output file to write the aux file to. This should be the folder that each aux should be written to if the input directory contains multiple aux filesystems.')
    .option('-o, --overwrite', 'Overwrite existing files.')
    .option('-f, --filter', 'The bot filter to apply to the bots being read.')
    .option('--allow-duplicates', 'Whether to allow duplicate bots. If a duplicate is encoutered, then a new bot ID will be generated for the duplicate.')
    .option('-a, --aux-version <version>', 'The version of aux files to produce. If omitted, then version 1 will be used.')
    .description('Generate an AUX file from a folder.')
    .action(async (dir, output, options) => {
    if (options.overwrite) {
        console.log('Overwriting existing files.');
    }
    if (options.merge) {
        console.log('Merging output AUX file.');
    }
    if (options.recursive) {
        console.log('Recursively reading aux files in directory.');
    }
    if (options.allowDuplicates) {
        console.log('Allowing duplicate bots.');
    }
    options.auxVersion = z.coerce
        .number()
        .int()
        .min(1)
        .max(2)
        .default(1)
        .parse(options.auxVersion);
    await auxReadFs(dir, output, options);
});
program
    .command('generate-server-config')
    .option('-p, --pretty', 'Pretty print the output.')
    .description('Generate a server config for CasualOS.')
    .action(async () => {
    const serverConfigSchema = getServerConfigSchema();
    const metadata = getSchemaMetadata(serverConfigSchema);
    const result = await askForInputs(metadata, 'serverConfig');
    const isValid = serverConfigSchema.safeParse(result);
    if (isValid.success === false) {
        console.error('Generated config is invalid:');
        console.error(isValid.error.toString());
    }
    if (result) {
        const output = JSON.stringify(result, null, 2);
        console.log(output);
    }
    else {
        console.log(JSON.stringify(result));
    }
});
program
    .command('validate-server-config')
    .option('--json <config>', 'The JSON to validate.')
    .option('-f, --file <file>', 'The file to validate.')
    .description('Validate a server config for CasualOS.')
    .action(async (options) => {
    let configJson;
    if (options.json) {
        configJson = options.json;
    }
    else {
        const fullPath = path.resolve(options.file);
        configJson = await readFile(fullPath, 'utf-8');
    }
    const parsed = JSON.parse(configJson);
    const serverConfigSchema = getServerConfigSchema();
    const result = serverConfigSchema.safeParse(parsed);
    if (result.success == true) {
        console.log('Config is valid!');
    }
    else {
        console.error('Config is invalid:');
        console.error(result.error.toString());
        process.exit(1);
    }
});
program
    .command('upload-file')
    .option('-r, --record <record>', 'The record or record key to upload the file to. Use {userId} to use your personal record.')
    .option('-f, --file <file>', 'The file to upload.')
    .option('--json <json>', 'The JSON content of the file to upload.')
    .option('--text <text>', 'The text content of the file to upload.')
    .option('-m, --markers <...markers>', 'The markers to associate with the file.')
    .option('-d, --description <description>', 'The description of the file.')
    .description('Upload a file record to CasualOS.')
    .action(async (options) => {
    if (!options.record) {
        console.error('Record name or key is required.');
        process.exit(1);
    }
    else if (!options.json && !options.text && !options.file) {
        console.error('One of --json, --text, or --file is required.');
        process.exit(1);
    }
    const opts = program.optsWithGlobals();
    const client = await getClient(opts);
    const parsedSessionKey = parseSessionKey(client.sessionKey);
    const userId = parsedSessionKey?.[0] ?? '';
    let data;
    let mimeType;
    if (options.file) {
        const fullPath = path.resolve(options.file);
        data = await readFile(fullPath);
        mimeType = mime.getType(fullPath);
    }
    else if (options.json) {
        data = new TextEncoder().encode(options.json);
        mimeType = 'application/json';
    }
    else if (options.text) {
        data = new TextEncoder().encode(options.text);
        mimeType = 'text/plain';
    }
    else {
        console.error('One of --json, --text, or --file is required.');
        process.exit(1);
    }
    const record = options.record.replace('{userId}', userId);
    const result = await resolveRecordFileInfo(data, mimeType);
    if (result.success === false) {
        console.error(`Could not resolve file info (${result.errorCode}): ${result.errorMessage}`);
        process.exit(1);
    }
    else {
        console.log('Uploading file...');
        const recordFileResult = await client.recordFile({
            recordKey: record,
            fileByteLength: result.byteLength,
            fileMimeType: result.mimeType,
            fileSha256Hex: result.hash,
            fileDescription: options.description || undefined,
            markers: options.markers || undefined,
        });
        let url;
        if (recordFileResult.success === false) {
            if (recordFileResult.errorCode === 'file_already_exists') {
                url = recordFileResult.existingFileUrl;
            }
            else {
                console.error(`Could not create record file (${recordFileResult.errorCode}): ${recordFileResult.errorMessage}`);
                process.exit(1);
            }
        }
        else {
            const uploadResult = await uploadFile(recordFileResult, result.data, result.hash);
            if (uploadResult.success === false) {
                console.error(`Could not upload file (${uploadResult.errorCode}): ${uploadResult.errorMessage}`);
                process.exit(1);
            }
            url = uploadResult.url;
        }
        console.log('File uploaded successfully!');
        console.log('URL:', url);
        console.log('SHA256:', result.hash);
        console.log('Size:', result.byteLength, 'bytes');
        console.log('MIME TYPE:', result.mimeType);
    }
});
program
    .command('upload-package')
    .option('-r, --record <record>', 'The record or record key to upload the package to. Use {userId} to use your personal record.')
    .option('-a, --address <address>', 'The address to upload the package to.')
    .option('-k, --key <key>', 'The version to upload. Must be in the format X.Y.Z or one of "major", "minor", or "patch". If given as "major", "minor", or "patch", the version will be incremented based on the latest version.')
    .option('-f, --file <file>', 'The file to upload.')
    .option('--json <json>', 'The JSON content of the file to upload.')
    .option('-m, --markers <markers>', 'The markers to associate with the package.')
    .option('-d, --description <description>', 'The description of the file.')
    .option('-e, --entitlements <entitlements>', 'The entitlements for the package.')
    .option('--raw', 'Whether to output the raw response from the server instead of formatted output.')
    .description('Upload a package to CasualOS.')
    .action(async (options) => {
    if (!options.record) {
        console.error('Record name or key is required.');
        process.exit(1);
    }
    else if (!options.address) {
        console.error('Address is required.');
        process.exit(1);
    }
    else if (!options.key) {
        console.error('Package key is required.');
        process.exit(1);
    }
    else if (!options.json && !options.file) {
        console.error('One of --json or --file is required.');
        process.exit(1);
    }
    const raw = options.raw;
    const opts = program.optsWithGlobals();
    const client = await getClient(opts, raw);
    const address = options.address;
    const parsedSessionKey = parseSessionKey(client.sessionKey);
    const userId = parsedSessionKey?.[0] ?? '';
    const record = options.record.replace('{userId}', userId);
    let data;
    let mimeType;
    if (options.file) {
        const fullPath = path.resolve(options.file);
        data = await readFile(fullPath);
        mimeType = mime.getType(fullPath);
        if (mimeType !== 'application/json') {
            console.error('Only .aux or .json files are supported.');
            process.exit(1);
        }
    }
    else if (options.json) {
        data = new TextEncoder().encode(options.json);
        mimeType = 'application/json';
    }
    else {
        console.error('One of --json or --file is required.');
        process.exit(1);
    }
    let key;
    const parsedKey = parseVersionNumber(options.key);
    if (typeof parsedKey?.major === 'number') {
        key = parsedKey;
    }
    else if (options.key === 'major' ||
        options.key === 'minor' ||
        options.key === 'patch') {
        // fetch the latest version
        const latestResult = await client.getPackageVersion({
            recordName: record,
            address: address,
        });
        if (latestResult.success === false) {
            if (latestResult.errorCode !== 'data_not_found') {
                console.error(`Could not fetch latest package version (${latestResult.errorCode}): ${latestResult.errorMessage}`, latestResult);
                process.exit(1);
            }
            else {
                if (!raw) {
                    console.log('No existing package found. Starting at version 0.0.0');
                }
                key = { major: 0, minor: 0, patch: 0, tag: null };
            }
        }
        else {
            if (!raw) {
                console.log('Latest version:', formatVersionSpecifier(latestResult.item.key));
            }
            key = latestResult.item.key;
        }
        if (options.key === 'major') {
            key.major = (key.major ?? 0) + 1;
        }
        else if (options.key === 'minor') {
            key.minor = (key.minor ?? 0) + 1;
        }
        else if (options.key === 'patch') {
            key.patch = (key.patch ?? 0) + 1;
        }
    }
    if (typeof key?.major !== 'number') {
        console.error('Invalid version provided. Must be in the format X.Y.Z');
        process.exit(1);
    }
    const markers = options.markers?.split(' ');
    const entitlements = parseEntitlements(options.entitlements?.split(' '));
    const result = await resolveRecordFileInfo(data, mimeType);
    if (result.success === false) {
        console.error(`Could not resolve file info (${result.errorCode}): ${result.errorMessage}`);
        process.exit(1);
    }
    else {
        if (!raw) {
            console.log(`Uploading ${formatVersionSpecifier(key)}...`);
        }
        const recordPackageResult = await client.recordPackageVersion({
            recordName: record,
            item: {
                address: address,
                key: key,
                auxFileRequest: {
                    fileByteLength: result.byteLength,
                    fileMimeType: result.mimeType,
                    fileSha256Hex: result.hash,
                    fileDescription: options.description || undefined,
                },
                description: options.description || formatVersionSpecifier(key),
                markers: markers || undefined,
                entitlements: entitlements || undefined,
            },
        });
        if (recordPackageResult.success === false) {
            console.error(`Could not create record package (${recordPackageResult.errorCode}): ${recordPackageResult.errorMessage}`, recordPackageResult);
            process.exit(1);
        }
        const uploadResult = await uploadFile(recordPackageResult.auxFileResult, result.data, result.hash);
        if (uploadResult.success === false &&
            uploadResult.errorCode !== 'file_already_exists') {
            console.error(`Could not upload package (${uploadResult.errorCode}): ${uploadResult.errorMessage}`);
            process.exit(1);
        }
        if (!raw) {
            console.log('Package uploaded successfully!');
        }
        else {
            console.log(JSON.stringify({
                ...recordPackageResult,
                key,
            }));
        }
    }
});
setupInfraCommands(program.command('infra'), config);
/**
 * Validates the given file is a proper aux file.
 * This function checks if the file exists, is a file, and has the correct extension.
 * If the file is valid, it returns the parsed bot state from the aux file.
 * @param filePath The path to the file whose to be validated.
 * @param opts Optional options to skip parsing or contents validation.
 */
async function loadAuxFile(filePath) {
    const targetStat = await stat(filePath);
    if (!targetStat.isFile()) {
        return { success: false, error: 'Path is not a file.' };
    }
    try {
        const contents = JSON.parse(await readFile(filePath, { encoding: 'utf-8' }));
        const botsState = getBotsStateFromStoredAux(contents);
        if (!botsState) {
            return {
                success: false,
                error: `Aux file at ${filePath} is not a valid (or supported) aux file.`,
            };
        }
        return { success: true, botsState };
    }
    catch (err) {
        return {
            success: false,
            error: `Could not read or parse aux file at ${filePath}.\n\n${err}`,
        };
    }
}
async function requestFiles(opts) {
    opts = {
        query: 'target file or directory containing files (path)',
        allowedExtensions: new Set(['.aux']),
        ...opts,
    };
    const targetFD = sanitizePath(await askForInputs(getSchemaMetadata(z.string().min(1)), opts.query));
    if (!existsSync(targetFD))
        return { directory: null, files: [] };
    const targetStat = statSync(targetFD);
    const files = [];
    if (targetStat.isDirectory()) {
        for (let file of await readdir(targetFD)) {
            if (opts.allowedExtensions.has(path.extname(file).toLowerCase())) {
                files.push(file);
            }
        }
    }
    else if (targetStat.isFile()) {
        if (opts.allowedExtensions.has(path.extname(targetFD).toLowerCase())) {
            files.push(path.basename(targetFD));
        }
        else {
            console.warn(`Invalid file type provided.\nExpected one of ${Array.from(opts.allowedExtensions).join(' | ')}.\nGot: ${path.extname(targetFD).toLowerCase()}`);
            return;
        }
    }
    else {
        console.error('Unknown item at path.');
        return;
    }
    return { directory: getDir(targetFD), files };
}
async function requestOutputDirectory(query = 'output directory to write files to') {
    const outDir = sanitizePath(await askForInputs(getSchemaMetadata(z.string().min(1)), query));
    if (existsSync(outDir) && statSync(outDir).isDirectory())
        return outDir;
    console.error(`Directory does not exist or is not a directory.`);
    return null;
}
const fileTagPrefixes = [
    ['@', '.tsx'],
    [LIBRARY_SCRIPT_PREFIX, '.tsm'],
    [DNA_TAG_PREFIX, '.json'],
    [DATE_TAG_PREFIX, '.date.text'],
    [STRING_TAG_PREFIX, '.text'],
    [NUMBER_TAG_PREFIX, '.number.text'],
    [VECTOR_TAG_PREFIX, '.vector.text'],
    [ROTATION_TAG_PREFIX, '.rotation.text'],
];
const fileExtensions = [
    ['.tsx', '@'],
    ['.tsm', LIBRARY_SCRIPT_PREFIX],
    ['.json', DNA_TAG_PREFIX],
    ['.date.text', DATE_TAG_PREFIX],
    ['.txt', ''],
    ['.text', STRING_TAG_PREFIX],
    ['.number.text', NUMBER_TAG_PREFIX],
    ['.vector.text', VECTOR_TAG_PREFIX],
    ['.rotation.text', ROTATION_TAG_PREFIX],
];
async function auxGenFs(input, output, options) {
    if (!input) {
        input = await askForInputs(getSchemaMetadata(z.string().min(1)), 'The path to the AUX file to convert to a file system');
    }
    input = path.resolve(input);
    if (!existsSync(input)) {
        throw new Error(`The provided path does not exist: ${input}`);
    }
    let files = [];
    let extraDirectories = [];
    const inputStat = await stat(input);
    if (inputStat.isDirectory()) {
        const paths = await readdir(input);
        for (let fileOrFolder of paths) {
            const fileOrFolderPath = path.resolve(input, fileOrFolder);
            const stats = await stat(fileOrFolderPath);
            if (stats.isFile() && fileOrFolder.endsWith('.aux')) {
                files.push(fileOrFolderPath);
            }
            else if (options.recursive &&
                !fileOrFolder.startsWith('.') &&
                stats.isDirectory()) {
                extraDirectories.push(fileOrFolderPath);
            }
        }
    }
    else {
        files.push(input);
    }
    if (!output) {
        output = await askForInputs(getSchemaMetadata(z.string().min(1)), 'The directory to write the file system to');
    }
    output = path.resolve(output);
    // make the directory if it doesn't exist
    await mkdir(output, {
        recursive: true,
    });
    const flag = options.overwrite ? 'w' : 'wx';
    for (const file of files) {
        const fileData = await loadAuxFile(file);
        if (!fileData.success) {
            throw new Error(`Invalid aux file: ${file}.\n\n${fileData.error}`);
        }
        const auxName = path.parse(file).name;
        const botsState = fileData.botsState;
        const extraBotsState = {};
        for (let id in botsState) {
            const bot = botsState[id];
            if (!options.writeSystemlessBots && !hasValue(bot.tags.system)) {
                console.warn(`Adding ${id} to extra.aux.`);
                extraBotsState[id] = bot;
                continue;
            }
            const system = bot.tags.system ?? id;
            const dirName = system.replace(/\./g, path.sep);
            const dir = path.resolve(output, auxName, dirName);
            const finalId = options.preserveBotIds ? id : '{id}';
            const botJson = {
                id: finalId,
                tags: {},
            };
            if (hasValue(bot.space)) {
                botJson.space = bot.space;
            }
            if (hasValue(bot.tags.system)) {
                botJson.tags.system = bot.tags.system;
            }
            // make the directory if it doesn't exist
            await mkdir(dir, {
                recursive: true,
            });
            // Don't track tag masks
            for (const tag of Object.keys(bot.tags)) {
                const writable = !/[\\/]/.test(tag);
                if (!writable) {
                    console.warn(`Skipping tag with invalid characters: ${tag}`);
                }
                let value = bot.tags[tag];
                let written = false;
                if (writable && hasValue(value)) {
                    let defaultExtension = 'txt';
                    if (typeof value === 'object') {
                        let json = JSON.stringify(value, null, 2);
                        if (json.indexOf('\n') >= 0) {
                            value = json;
                            defaultExtension = 'json';
                        }
                    }
                    if (typeof value === 'string') {
                        for (let [prefix, ext] of fileTagPrefixes) {
                            if (value.startsWith(prefix)) {
                                // write the tag value to its own file
                                const filePath = path.resolve(dir, `${tag}${ext}`);
                                const fileContent = value.slice(prefix.length);
                                try {
                                    await writeFile(filePath, fileContent, {
                                        encoding: 'utf-8',
                                        flag,
                                    });
                                    written = true;
                                }
                                catch (err) {
                                    console.error(`Could not write file: ${filePath}.\n\n${err}\n`);
                                }
                                break;
                            }
                        }
                        if (!written && value.indexOf('\n') >= 0) {
                            // string has a newline, so write it to a text file
                            // if the tag does not have a file extension, add .txt
                            const fileName = tag.indexOf('.') >= 0
                                ? tag
                                : `${tag}.${defaultExtension}`;
                            const filePath = path.resolve(dir, fileName);
                            try {
                                await writeFile(filePath, value, {
                                    encoding: 'utf-8',
                                    flag,
                                });
                                written = true;
                            }
                            catch (err) {
                                console.error(`Could not write file: ${filePath}.\n\n${err}\n`);
                            }
                        }
                    }
                }
                if (!written) {
                    botJson.tags[tag] = bot.tags[tag];
                }
            }
            //  write the bot.json file
            const botAuxName = `${system}.bot.aux`;
            const botJsonPath = path.resolve(dir, botAuxName);
            try {
                const botAux = {
                    version: 1,
                    state: {
                        [finalId]: botJson,
                    },
                };
                await writeFile(botJsonPath, fastJsonStableStringify(botAux, {
                    space: 2,
                }), {
                    encoding: 'utf-8',
                    flag,
                });
            }
            catch (err) {
                console.error(`Could not write ${botAuxName} file: ${botJsonPath}.\n\n${err}\n`);
            }
            console.log(`Created: ${system}`);
        }
        // Always write the extra bots file so that we can do the reverse operation
        // and produce the original aux file.
        if (!options.omitExtraBots) {
            // write a aux file for the extra bots to the output directory
            const extraBotsFilePath = path.resolve(output, auxName, `extra.aux`);
            try {
                const aux = {
                    version: 1,
                    state: extraBotsState,
                };
                await writeFile(extraBotsFilePath, JSON.stringify(aux, null, 2), {
                    encoding: 'utf-8',
                    flag,
                });
            }
            catch (err) {
                console.error(`Could not write extra.aux file: ${extraBotsFilePath}.\n\n${err}\n`);
            }
        }
    }
    for (let extraDir of extraDirectories) {
        // Only allow one level of recursion
        await auxGenFs(extraDir, output, {
            ...options,
            recursive: false,
        });
    }
}
async function auxReadFs(input, output, options) {
    const { overwrite, auxVersion } = options;
    const failOnDuplicate = !options.allowDuplicates;
    if (!input) {
        input = await askForInputs(getSchemaMetadata(z.string().min(1)), 'The path to the directory to read into an AUX file.');
    }
    input = path.resolve(input);
    if (!existsSync(input)) {
        console.error(`The provided path does not exist: ${input}`);
        return;
    }
    if (!output) {
        output = await askForInputs(getSchemaMetadata(z.string().min(1)), 'The path to the output AUX file.');
    }
    output = path.resolve(output);
    let filterFunc = null;
    if (options.filter) {
        filterFunc = Function('$', options.filter);
    }
    const inputFiles = await readdir(input);
    const hasExtra = inputFiles.includes('extra.aux');
    if (!hasExtra) {
        await mkdir(output, {
            recursive: true,
        });
        for (let file of inputFiles) {
            const filePath = path.resolve(input, file);
            const outputPath = path.resolve(output, `${file}.aux`);
            const stats = await stat(filePath);
            if (stats.isDirectory()) {
                await auxReadFs(filePath, outputPath, options);
            }
        }
    }
    else {
        console.log('Reading aux files from directory:', input);
        console.log('Output will be written to:', output);
        // folder represents a single aux
        const botsState = await auxReadFsCore(input, filterFunc, failOnDuplicate);
        const storedAux = constructAux(botsState, auxVersion);
        const outputFolder = path.dirname(output);
        if (outputFolder) {
            await mkdir(path.resolve(outputFolder), {
                recursive: true,
            });
        }
        await writeFile(output, fastJsonStableStringify(storedAux, {
            space: 2,
        }), { encoding: 'utf-8', flag: overwrite ? 'w' : 'wx' });
    }
}
async function readAuxFile(filePath) {
    const targetStat = await stat(filePath);
    if (!targetStat.isFile()) {
        throw new Error(`Path is not a file: ${filePath}`);
    }
    const fileContents = await readFile(filePath, { encoding: 'utf-8' });
    const parseResult = tryParseJson(fileContents);
    if (parseResult.success === false) {
        return failure({
            errorCode: 'invalid_file_data',
            errorMessage: `Could not parse aux file at ${filePath} as JSON.\n\nError: ${parseResult.error}`,
        });
    }
    const storedAux = STORED_AUX_SCHEMA.safeParse(parseResult.value);
    if (storedAux.success === false) {
        return failure({
            errorCode: 'invalid_file_data',
            errorMessage: `Aux file at ${filePath} does not match the expected format.\n\nError: ${z.prettifyError(storedAux.error)}`,
            issues: storedAux.error.issues,
        });
    }
    return success(storedAux.data);
}
async function readAuxFileBots(filePath) {
    const auxFileResult = await readAuxFile(filePath);
    if (auxFileResult.success === false) {
        return failure(auxFileResult.error);
    }
    const botsState = getBotsStateFromStoredAux(auxFileResult.value);
    if (!botsState) {
        throw new Error(`Aux file at ${filePath} is not a valid (or supported) aux file.`);
    }
    return success(botsState);
}
async function assignBots(state, added, failOnDuplicate) {
    for (let id in added) {
        const b = added[id];
        if (!hasValue(b)) {
            continue;
        }
        if (id in state && hasValue(state[id])) {
            if (failOnDuplicate) {
                throw new Error(`Bot ${id} already exists in the bots state.`);
            }
            else {
                console.warn(`Bot ${id} already exists in the bots state. Generating new ID.`);
                id = uuid();
                b.id = id;
            }
        }
        state[id] = b;
    }
}
async function auxReadFsCore(input, filter, failOnDuplicate) {
    const botsState = {};
    console.log('Reading directory:', input);
    const inputFiles = await readdir(input);
    let tags = {};
    let hasBot = false;
    let botId = null;
    let botState = {};
    for (let file of inputFiles) {
        const filePath = path.join(input, file);
        const fileStat = await stat(filePath);
        if (fileStat.isDirectory()) {
            // If the file is a directory, we need to read its contents recursively
            const subState = await auxReadFsCore(filePath, filter, failOnDuplicate);
            assignBots(botsState, subState, failOnDuplicate);
        }
        else {
            if (file.endsWith('.aux')) {
                console.log(`Reading aux: ${file}`);
                const isSystemBotFile = file.endsWith('.bot.aux');
                const auxFile = await readAuxFileBots(filePath);
                if (isFailure(auxFile)) {
                    console.error(auxFile.error.errorMessage);
                    continue;
                }
                const auxBotsState = auxFile.value;
                // Get the first bot Id from the aux file
                if (isSystemBotFile && !botId) {
                    for (let id of Object.keys(auxBotsState)) {
                        if (hasValue(id)) {
                            if (id === '{id}') {
                                const newId = uuid();
                                const b = auxBotsState[id];
                                b.id = newId;
                                auxBotsState[newId] = b;
                                delete auxBotsState[id];
                                id = newId;
                                console.log(`Generated bot ID: ${id}`);
                            }
                            else {
                                console.log(`Found bot ID: ${id}`);
                            }
                            botId = id;
                            hasBot = true;
                            break;
                        }
                    }
                }
                else if (!isSystemBotFile) {
                    console.log('Reading extra aux file.\n\n');
                }
                assignBots(botState, auxBotsState, failOnDuplicate);
            }
            else {
                let readTag = false;
                for (let [ext, prefix] of fileExtensions) {
                    if (file.endsWith(ext)) {
                        const tagName = file.slice(0, -ext.length);
                        console.log(`Reading tag: ${tagName}`);
                        // If the file has a known extension, we can read it and add its contents to the bots state
                        const fileContents = prefix +
                            (await readFile(filePath, { encoding: 'utf-8' }));
                        tags[tagName] = fileContents;
                        hasBot = true;
                        readTag = true;
                        break;
                    }
                }
                if (!readTag) {
                    // If the file does not have a known extension, we can just read it as a string tag
                    console.log(`Reading tag: ${file}`);
                    const fileContents = await readFile(filePath, {
                        encoding: 'utf-8',
                    });
                    tags[file] = fileContents;
                    hasBot = true;
                    readTag = true;
                }
            }
        }
    }
    if (!botId && hasBot) {
        console.warn('No bot ID found for folder:', input);
        console.warn('Generating a random bot ID.');
        botId = uuid();
    }
    if (botId) {
        const existingBot = botState[botId];
        if (existingBot) {
            existingBot.tags = merge(existingBot.tags, tags);
        }
        else {
            // If the bot does not exist, we create a new bot with the tags
            botState[botId] = createBot(botId, tags);
        }
    }
    if (filter) {
        for (let id in botState) {
            const b = botState[id];
            if (!filter(b)) {
                console.log(`Bot ${id} does not match filter, skipping.`);
                delete botState[id];
            }
        }
    }
    assignBots(botsState, botState, failOnDuplicate);
    return botsState;
}
async function auxConvert() {
    const { directory: targetFD, files: auxFiles } = await requestFiles({
        allowedExtensions: new Set(['.aux']),
    });
    if (auxFiles.length < 1) {
        console.error(`No aux file found at/in the provided path.`);
        return;
    }
    const outDir = await requestOutputDirectory();
    if (!outDir) {
        console.error(`Invalid output directory provided.`);
        return;
    }
    let converted = 0;
    const prefix = outDir === targetFD ? '_' : '';
    for (let file of auxFiles) {
        try {
            await writeFile(path.join(outDir, `${prefix}${file}`), JSON.stringify(getBotsStateFromStoredAux(JSON.parse(await readFile(replaceWithBasename(targetFD, file), { encoding: 'utf-8' })))));
            converted++;
        }
        catch (err) {
            console.error(`Could not convert: ${file}.\n\n${err}\n`);
        }
    }
    console.log(`\n🍵 Converted ${converted}/${auxFiles.length} Files.\n--------------------------\n${auxFiles
        .map((f) => `|✔️ | ${f}`)
        .join('\n')}\n`);
}
async function query(client, procedure, input, shouldConfirm = true, isJavaScriptInput = false, repl = null) {
    const availableOperations = await client.listProcedures({});
    while (!procedure ||
        !availableOperations.procedures.find((p) => p.name === procedure)) {
        const response = await prompts({
            type: 'autocomplete',
            name: 'procedure',
            message: 'Select the procedure to execute',
            choices: availableOperations.procedures.map((op) => ({
                title: op.name,
                value: op.name,
            })),
            onState,
        });
        procedure = response.procedure;
    }
    const operation = availableOperations.procedures.find((p) => p.name === procedure);
    if (!operation) {
        console.error(`Could not find operation ${procedure}!`);
        return;
    }
    console.log('Your selected operation:', operation);
    let query;
    if (operation.query) {
        query = await askForInputs(operation.query, operation.name, repl);
        console.log('Your query:', query);
    }
    if (!input) {
        input = await askForInputs(operation.inputs, operation.name, repl);
    }
    else if (!isJavaScriptInput) {
        input = JSON.parse(input);
    }
    console.log('Your input:', input);
    let continueRequest = true;
    if (shouldConfirm) {
        const confirm = await prompts({
            type: 'confirm',
            name: 'continue',
            message: 'Do you want to continue?',
            initial: true,
            onState,
        });
        continueRequest = confirm.continue;
    }
    if (continueRequest) {
        const result = await callProcedure(client, operation.name, input, query);
        if (shouldConfirm) {
            if (typeof result === 'object' && Symbol.asyncIterator in result) {
                console.log('Result:');
                async function logResult() {
                    for await (let item of result) {
                        console.log(item);
                    }
                }
                logResult();
            }
            else {
                console.log('Result:', result);
            }
        }
        return result;
    }
    else {
        console.log('Cancelled.');
    }
}
async function callProcedure(client, operation, input, query) {
    while (true) {
        const result = await client.callProcedure(operation, input, query);
        if (result.success === false && result.errorCode === 'not_logged_in') {
            const loginResponse = await prompts({
                type: 'confirm',
                name: 'login',
                message: 'You are not logged in. Do you want to log in and try again?',
                initial: true,
                onState,
            });
            if (loginResponse.login) {
                const key = await login(client);
                if (!key) {
                    return result;
                }
            }
            else {
                return result;
            }
        }
        else if (result.success === false &&
            result.errorCode === 'invalid_origin') {
            const originResponse = await prompts({
                type: 'text',
                name: 'origin',
                message: 'The endpoint does not allow itself as an origin. Enter the origin to use for the request.',
                onState,
            });
            saveOrigin(client.endpoint, originResponse.origin);
        }
        else {
            return result;
        }
    }
}
async function getClient(opts, silent = false) {
    const client = await getClientWithoutSessionKey(opts, silent);
    if (opts.sessionKey) {
        client.sessionKey = opts.sessionKey;
    }
    else {
        client.sessionKey = await getOrRefreshSessionKey({
            endpoint: client.endpoint,
            origin: opts.origin,
        }, silent);
    }
    return client;
}
async function getClientWithoutSessionKey(opts, silent = false) {
    const endpoint = await getEndpoint(opts.endpoint);
    if (!silent) {
        printStatus(endpoint);
    }
    const client = createRecordsClient(endpoint);
    Object.assign(client.headers, getHeaders(endpoint, opts));
    return client;
}
async function getOrRefreshSessionKey(opts, silent = false) {
    const key = getSessionKey(opts.endpoint);
    const expiration = getSessionKeyExpiration(key);
    if (isExpired(expiration)) {
        return null;
    }
    else if (willExpire(expiration)) {
        return await replaceSessionKey(opts, key, silent);
    }
    return key;
}
function getSessionKey(endpoint) {
    return convertToString(config.get(`${endpoint}:sessionKey`));
}
function saveSessionKey(endpoint, key) {
    config.set(`${endpoint}:sessionKey`, key);
}
function saveLoginResult(client, result) {
    if (!result) {
        saveSessionKey(client.endpoint, null);
        client.sessionKey = null;
    }
    else {
        saveSessionKey(client.endpoint, result.sessionKey);
        client.sessionKey = result.sessionKey;
    }
}
async function login(client) {
    const response = await prompts([
        {
            type: 'select',
            name: 'type',
            message: 'Select the type of address to use for login.',
            choices: [
                {
                    title: 'Email',
                    value: 'email',
                },
                {
                    title: 'Phone',
                    value: 'phone',
                },
                {
                    title: 'Privo',
                    value: 'privo',
                },
            ],
            onState,
        },
    ]);
    if (response.type === 'privo') {
        return await loginWithPrivo(client);
    }
    else if (response.type === 'email' || response.type === 'phone') {
        const addressResponse = await prompts({
            type: 'text',
            name: 'address',
            message: 'Enter your address.',
            onState,
        });
        const addressType = response.type;
        const address = addressResponse.address;
        return await loginWithCode(client, address, addressType);
    }
}
async function loginWithCode(client, address, addressType) {
    const result = await client.requestLogin({
        address: address,
        addressType: addressType,
    });
    if (result.success) {
        const response = await prompts({
            type: 'text',
            name: 'code',
            message: 'Enter the code that was sent to your address.',
            onState,
        });
        const code = response.code;
        const loginResult = await client.completeLogin({
            code: code,
            requestId: result.requestId,
            userId: result.userId,
        });
        if (loginResult.success === true) {
            saveLoginResult(client, loginResult);
            console.log('Login successful!');
            return loginResult.sessionKey;
        }
        else {
            saveLoginResult(client, null);
            console.log('Failed to complete login:');
            console.log(loginResult);
            return null;
        }
    }
    else {
        saveLoginResult(client, null);
        console.log('Failed to create login request:');
        console.log(result);
        return null;
    }
}
async function loginWithPrivo(client) {
    const result = await client.requestPrivoLogin({});
    if (result.success === false) {
        saveLoginResult(client, null);
        console.log('Failed to request Privo login:');
        console.log(result);
        return null;
    }
    const open = (await import('open')).default;
    await open(result.authorizationUrl);
    const startTime = Date.now();
    const timeout = 1000 * 60 * 5; // 5 minutes
    while (Date.now() - startTime < timeout) {
        const loginResult = await client.completeOAuthLogin({
            requestId: result.requestId,
        });
        if (loginResult.success === true) {
            saveLoginResult(client, loginResult);
            console.log('Login successful!');
            return loginResult.sessionKey;
        }
        else {
            if (loginResult.errorCode === 'not_completed') {
                // Wait for a second before trying again.
                await new Promise((resolve) => setTimeout(resolve, 1000));
            }
            else {
                saveLoginResult(client, null);
                console.log('Failed to complete login:');
                console.log(loginResult);
                return null;
            }
        }
    }
    saveLoginResult(client, null);
    console.log('Failed to complete login:');
    console.log('Timed out');
    return null;
}
async function replaceSessionKey(opts, key, silent = false) {
    const client = await getClientWithoutSessionKey(opts, silent);
    const result = await client.replaceSession(undefined, {
        sessionKey: key,
    });
    if (result.success === true) {
        saveSessionKey(client.endpoint, result.sessionKey);
        if (!silent) {
            console.log('Session key replaced!');
        }
        return result.sessionKey;
    }
    saveSessionKey(client.endpoint, null);
    if (!silent) {
        console.log('Failed to replace session key:');
        console.log(result);
    }
    return null;
}
async function getEndpoint(endpoint) {
    if (endpoint) {
        return endpoint;
    }
    let savedEndpoint = getCurrentEndpoint();
    if (!savedEndpoint) {
        savedEndpoint = await updateEndpoint();
    }
    return savedEndpoint;
}
function getCurrentEndpoint() {
    return convertToString(config.get('currentEndpoint'));
}
async function updateEndpoint() {
    const response = await prompts({
        type: 'text',
        name: 'endpoint',
        message: 'Enter the endpoint to use for queries.',
        onState,
    });
    const savedEndpoint = response.endpoint;
    saveCurrentEndpoint(savedEndpoint);
    return savedEndpoint;
}
function saveCurrentEndpoint(endpoint) {
    config.set('currentEndpoint', endpoint);
    console.log('Endpoint updated to:', endpoint);
}
function saveOrigin(endpoint, origin) {
    config.set(`${endpoint}:origin`, origin);
}
function getOrigin(endpoint) {
    let origin = config.get(`${endpoint}:origin`);
    if (typeof origin === 'string' && origin) {
        return origin;
    }
    return endpoint;
}
function getHeaders(endpoint, opts) {
    return {
        origin: opts.origin ?? getOrigin(endpoint),
    };
}
function printStatus(endpoint) {
    if (!endpoint) {
        console.log('No endpoint is currently set.');
        console.log('Set an endpoint using the set-endpoint command.');
        return;
    }
    console.log('Current endpoint:', endpoint);
    const key = getSessionKey(endpoint);
    if (key) {
        const expire = getSessionKeyExpiration(key);
        if (expire < Date.now()) {
            console.log('The current session has expired.');
        }
        else {
            console.log('You are logged in.');
            // Get and display the user ID from the session key
            const parseResult = parseSessionKey(key);
            if (parseResult) {
                const [userId, sessionId] = parseResult;
                console.log('User ID:', userId);
                console.log('Session ID:', sessionId);
            }
            console.log('Session expires at:', new Date(expire).toString());
        }
    }
    else {
        console.log('You are not logged in.');
    }
}
async function main() {
    try {
        await program.parseAsync(process.argv);
    }
    catch (err) {
        console.error(err);
    }
}
function sanitizePath(input) {
    const unquoted = input ? input.replace(/^['"]|['"]$/g, '') : '';
    return path.resolve(path.normalize(unquoted));
}
function getDir(base) {
    return statSync(base).isDirectory() ? base : path.dirname(base);
}
function replaceWithBasename(base, filename) {
    return path.join(getDir(base), filename);
}
function convertToString(str) {
    if (typeof str === 'undefined' || str === null) {
        return str;
    }
    return String(str);
}
main();
//# sourceMappingURL=cli.js.map