import path from 'path';
import { fileURLToPath } from 'url';
import loadConfig from '../lib/config.js';
import { loadEnv } from '../lib/env.js';
import { checkGitStatus } from './utils.js';
import { createSeverityFormatter, cyan, gray } from './colors.js';
import { initProject } from './commands/init.js';
import { generateComponent, generatePage, generateBridge, generateGuard } from './commands/generate.js';
import { destroyComponent, destroyPage, destroyBridge, destroyGuard } from './commands/destroy.js';
import { buildProject, cleanProject, checkProject } from './commands/build.js';
import { serveProject, watchProject } from './commands/serve.js';
import { printHelp } from './commands/help.js';
import { runDoctor } from './commands/doctor.js';
import { runInspect } from './commands/inspect.js';
import { runStats } from './commands/stats.js';
import { runEnv } from './commands/env.js';
import { explainDiagnostic } from './commands/explain.js';
import { runTrace } from './commands/trace.js';
import { runAtlas, runQuery } from './commands/atlas.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const findProjectRoot = loadConfig.findProjectRoot;
/**
* Avenx CLI - Command Line Interface router for Avenx-JS.
*/
export class AvenxCLI {
/**
* Creates an instance of AvenxCLI.
* Initializes the base directory and framework directory paths.
* @param {object} [options]
*/
constructor(options = {}) {
this.baseDir = options.baseDir || findProjectRoot(process.cwd());
loadEnv(this.baseDir);
this.frameworkDir = path.join(__dirname, '..');
this.config = { ...loadConfig(this.baseDir), ...options };
// Tint compiler warnings yellow and errors red for every command that compiles
// (build, check, watch, serve). Stays inert when colors are unsupported.
this.config.logging = { ...this.config.logging, formatter: createSeverityFormatter() };
}
/**
* Serves the project on specified port and host.
* @param {number} port
* @param {string} host
* @param {boolean} [open] - Whether to open the browser automatically.
*/
serveProject(port, host, open) {
return serveProject(this, port, host, open);
}
/**
* Executes a given CLI command with provided arguments.
* @param {string} command - The command to run (e.g., 'init', 'generate', 'build', 'serve', 'help').
* @param {string[]} args - Additional arguments for the command.
*/
async run(command, args = []) {
const dryRun = args.includes('--dry-run') || args.includes('-d');
const force = args.includes('--force') || args.includes('-f');
const withTest = args.includes('--with-test');
const noTest = args.includes('--no-test');
// `avenx build` is production by default; `--dev` opts out. `--prod` is
// accepted so a script can state the intent it relies on. `serve` and
// `watch` are development unless the flag says otherwise.
const explicitDev = args.includes('--dev') || args.includes('--development');
const explicitProd = args.includes('--prod') || args.includes('--production');
if (explicitDev) {
this.config.mode = 'development';
} else if (explicitProd) {
this.config.mode = 'production';
} else if (command === 'serve' || command === 'watch' || command === 'w') {
this.config.mode = 'development';
}
let templateName = null;
const templateIndex = args.findIndex((arg) => arg === '--template' || arg === '-t');
if (templateIndex !== -1 && templateIndex + 1 < args.length) {
templateName = args[templateIndex + 1];
} else {
const templateInline = args.find((arg) => arg.startsWith('--template=') || arg.startsWith('-t='));
if (templateInline) {
templateName = templateInline.split('=').slice(1).join('=');
}
}
const filteredArgs = args.filter(
(arg, idx) =>
arg !== '--dry-run' &&
arg !== '-d' &&
arg !== '--force' &&
arg !== '-f' &&
arg !== '--no-color' &&
arg !== '--no-colors' &&
arg !== '--dev' &&
arg !== '--development' &&
arg !== '--prod' &&
arg !== '--production' &&
arg !== '--template' &&
arg !== '-t' &&
!(templateIndex !== -1 && idx === templateIndex + 1) &&
!arg.startsWith('--template=') &&
!arg.startsWith('-t=') &&
arg !== '--with-test' &&
arg !== '--no-test',
);
const type = filteredArgs[0];
const name = filteredArgs[1];
switch (command) {
case 'init':
if (!force) {
const proceed = await checkGitStatus();
if (!proceed) {
return;
}
}
await initProject(this, args);
process.exit(0);
break;
case 'generate':
case 'g':
if (!force) {
const proceed = await checkGitStatus();
if (!proceed) {
return;
}
}
if (type === 'bridge') {
generateBridge(this, name, dryRun, force, templateName);
} else if (type === 'guard') {
generateGuard(this, name, dryRun, force, templateName);
} else if (type === 'page' || type === 'p') {
generatePage(this, name, dryRun, force, templateName);
} else if (type === 'component' || type === 'c') {
generateComponent(this, name, dryRun, force, templateName, withTest, noTest);
} else {
// Default to component if type is not specified (e.g., `avenx g MyButton`)
generateComponent(this, type, dryRun, force, templateName, withTest, noTest);
}
break;
case 'destroy':
case 'd':
if (!force) {
const proceed = await checkGitStatus();
if (!proceed) {
return;
}
}
if (type === 'bridge') {
destroyBridge(this, name, dryRun);
} else if (type === 'guard') {
destroyGuard(this, name, dryRun);
} else if (type === 'page' || type === 'p') {
destroyPage(this, name, dryRun);
} else if (type === 'component' || type === 'c') {
destroyComponent(this, name, dryRun);
} else {
// Default to component if only one arg or type is 'component'
destroyComponent(this, name || type, dryRun);
}
break;
case 'build':
case 'b':
if (!force) {
const proceed = await checkGitStatus();
if (!proceed) {
return;
}
}
buildProject(this);
break;
case 'clean':
cleanProject(this);
break;
case 'check':
case 'lint':
checkProject(this, args);
break;
case 'doctor':
runDoctor(this);
break;
case 'env':
runEnv(this);
break;
case 'inspect':
case 'i':
runInspect(this);
break;
case 'stats':
case 's':
runStats(this, args);
break;
case 'serve': {
// Recording is opt-in and development-only. It is read here rather
// than from config alone so that turning it on is always a visible,
// deliberate act at the command line.
this.traceEnabled = args.includes('--trace');
const portIdx = args.findIndex((a) => a === '--port' || a === '-p' || a.startsWith('--port=') || a.startsWith('-p='));
const hostIdx = args.findIndex((a) => a === '--host' || a === '-h' || a.startsWith('--host=') || a.startsWith('-h='));
const open = args.includes('--open') || args.includes('-o');
if (args.includes('--no-live-reload') || args.includes('--live-reload=false')) {
this.config.server.liveReload = false;
}
// The positional port is the first bare argument that is not itself the
// value of a flag. Only args[0] used to be considered, so any leading
// flag — `serve --trace 8080`, `serve --open 8080` — silently fell back
// to the default port.
const flagValueIndexes = new Set();
for (const idx of [portIdx, hostIdx]) {
if (idx !== -1 && !args[idx].includes('=')) {
flagValueIndexes.add(idx + 1);
}
}
const positionalPort = args.find(
(arg, idx) => !arg.startsWith('-') && !flagValueIndexes.has(idx) && /^\d+$/.test(arg.trim()),
);
const rawPortVal = portIdx !== -1
? (args[portIdx].includes('=') ? args[portIdx].split('=').slice(1).join('=') : args[portIdx + 1])
: (positionalPort ?? null);
const rawPort = rawPortVal?.replace(/[^0-9]/g, '');
const port = rawPort
? parseInt(rawPort, 10)
: (process.env.PORT ? parseInt(String(process.env.PORT).replace(/[^0-9]/g, ''), 10) : null) || this.config.server?.port || 3000;
const rawHostVal = hostIdx !== -1
? (args[hostIdx].includes('=') ? args[hostIdx].split('=').slice(1).join('=') : args[hostIdx + 1])
: null;
const host = rawHostVal ? rawHostVal.trim().replace(/\/+$/g, '') : 'localhost';
this.serveProject(port, host, open);
break;
}
case 'watch':
case 'w':
console.log(cyan(`👀 Watching for changes in ${this.config.srcDir}/...\n`));
buildProject(this);
watchProject(this);
process.on('SIGINT', () => {
console.log(`\n${gray('Stopping watch...')}`);
process.exit(0);
});
break;
case 'trace':
runTrace(this, args);
break;
case 'atlas':
runAtlas(this, args);
break;
case 'impact':
runQuery(this, args, 'in');
break;
case 'why':
runQuery(this, args, 'out');
break;
case 'explain': {
const asJson = args.includes('--json');
const codeArg = args.find((a) => !a.startsWith('-'));
explainDiagnostic(this, codeArg, asJson);
break;
}
case 'help':
default:
printHelp();
break;
}
}
}