mirror of
https://github.com/robonen/questlang.git
synced 2026-03-20 02:44:47 +00:00
fix: lint, type errors
test: specify types for path checks in interpreter tests test: add optional chaining in lexer tests to prevent errors test: add optional chaining in parser tests to prevent errors fix: clean up whitespace and formatting in AST and lexer files feat: enhance CLI with improved error handling and user prompts fix: update index and interpreter files for better type handling fix: clean up parser code for better readability and error handling build: enable minification in tsdown configuration
This commit is contained in:
130
src/cli.ts
130
src/cli.ts
@@ -1,9 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import type { QuestInterpreter } from '.';
|
||||
import type { InitialNode, NodeDefinition } from './ast.js';
|
||||
import fs from 'node:fs';
|
||||
import process from 'node:process';
|
||||
import * as p from '@clack/prompts';
|
||||
import { QuestLang, type QuestInterpreter } from '.';
|
||||
import { type NodeDefinition, type InitialNode } from './ast';
|
||||
import { QuestLang } from '.';
|
||||
|
||||
/**
|
||||
* Beautiful command-line interface for QuestLang using clack
|
||||
@@ -14,9 +16,9 @@ class ClackCLI {
|
||||
*/
|
||||
public async run(): Promise<void> {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
|
||||
console.clear();
|
||||
|
||||
|
||||
p.intro('🎮 QuestLang Interpreter');
|
||||
|
||||
if (args.length === 0) {
|
||||
@@ -25,32 +27,32 @@ class ClackCLI {
|
||||
}
|
||||
|
||||
const command = args[0];
|
||||
|
||||
|
||||
switch (command) {
|
||||
case 'play':
|
||||
if (args.length < 2) {
|
||||
p.outro('❌ Usage: questlang-clack play <file.ql>');
|
||||
process.exit(1);
|
||||
}
|
||||
await this.playQuest(args[1]);
|
||||
await this.playQuest(args[1]!);
|
||||
break;
|
||||
|
||||
|
||||
case 'validate':
|
||||
if (args.length < 2) {
|
||||
p.outro('❌ Usage: questlang-clack validate <file.ql>');
|
||||
process.exit(1);
|
||||
}
|
||||
await this.validateQuest(args[1]);
|
||||
await this.validateQuest(args[1]!);
|
||||
break;
|
||||
|
||||
|
||||
case 'analyze':
|
||||
if (args.length < 2) {
|
||||
p.outro('❌ Usage: questlang-clack analyze <file.ql>');
|
||||
process.exit(1);
|
||||
}
|
||||
await this.analyzeQuest(args[1]);
|
||||
await this.analyzeQuest(args[1]!);
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
p.outro(`❌ Unknown command: ${command}`);
|
||||
process.exit(1);
|
||||
@@ -59,7 +61,7 @@ class ClackCLI {
|
||||
|
||||
private async showInteractiveMenu(): Promise<void> {
|
||||
const questFiles = this.findQuestFiles();
|
||||
|
||||
|
||||
if (questFiles.length === 0) {
|
||||
p.outro('❌ No .ql quest files found in current directory');
|
||||
process.exit(1);
|
||||
@@ -107,25 +109,26 @@ class ClackCLI {
|
||||
return fs.readdirSync('.')
|
||||
.filter(file => file.endsWith('.ql'))
|
||||
.sort();
|
||||
} catch {
|
||||
}
|
||||
catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async playQuest(filename: string): Promise<void> {
|
||||
const spinner = p.spinner();
|
||||
|
||||
|
||||
try {
|
||||
spinner.start('Loading quest...');
|
||||
|
||||
|
||||
const source = this.readFile(filename);
|
||||
const interpreter = QuestLang.interpret(source);
|
||||
|
||||
|
||||
// Validate first
|
||||
const validation = interpreter.validate();
|
||||
if (!validation.isValid) {
|
||||
spinner.stop('❌ Quest validation failed');
|
||||
|
||||
|
||||
p.log.error('Validation errors:');
|
||||
validation.errors.forEach(error => p.log.error(` • ${error}`));
|
||||
p.outro('Fix the errors and try again');
|
||||
@@ -134,12 +137,12 @@ class ClackCLI {
|
||||
|
||||
const questInfo = interpreter.getQuestInfo();
|
||||
spinner.stop('✅ Quest loaded successfully');
|
||||
|
||||
|
||||
p.note(`📖 ${questInfo.goal}`, `🎮 ${questInfo.name}`);
|
||||
|
||||
await this.gameLoop(interpreter);
|
||||
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop('❌ Error loading quest');
|
||||
p.log.error(error instanceof Error ? error.message : String(error));
|
||||
p.outro('Failed to start quest');
|
||||
@@ -150,7 +153,7 @@ class ClackCLI {
|
||||
private async gameLoop(interpreter: QuestInterpreter): Promise<void> {
|
||||
while (!interpreter.getState().isComplete) {
|
||||
const currentNode = interpreter.getCurrentNode();
|
||||
|
||||
|
||||
if (!currentNode) {
|
||||
p.log.error('Current node not found');
|
||||
break;
|
||||
@@ -161,13 +164,13 @@ class ClackCLI {
|
||||
|
||||
if (currentNode.nodeType === 'действие') {
|
||||
const choices = interpreter.getAvailableChoices();
|
||||
|
||||
|
||||
const choice = await p.select({
|
||||
message: 'What do you want to do?',
|
||||
options: choices.map((choice, index) => ({
|
||||
value: index,
|
||||
label: choice.text
|
||||
}))
|
||||
label: choice.text,
|
||||
})),
|
||||
});
|
||||
|
||||
if (p.isCancel(choice)) {
|
||||
@@ -176,21 +179,28 @@ class ClackCLI {
|
||||
}
|
||||
|
||||
const result = interpreter.executeChoice(choice as number);
|
||||
|
||||
|
||||
if (!result.success) {
|
||||
p.log.error(`Error: ${result.error}`);
|
||||
break;
|
||||
}
|
||||
} else if (currentNode.nodeType === 'начальный') {
|
||||
}
|
||||
else if (currentNode.nodeType === 'начальный') {
|
||||
// Auto-advance from initial nodes to first transition
|
||||
const initialNode = currentNode as InitialNode;
|
||||
if (initialNode.transitions && initialNode.transitions.length > 0) {
|
||||
const result = interpreter.moveToNode(initialNode.transitions[0]);
|
||||
const firstTransition = initialNode.transitions[0];
|
||||
if (!firstTransition) {
|
||||
p.log.error('First transition is undefined');
|
||||
break;
|
||||
}
|
||||
const result = interpreter.moveToNode(firstTransition);
|
||||
if (!result.success) {
|
||||
p.log.error(`Error: ${result.error}`);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
p.log.error('Initial node has no transitions');
|
||||
break;
|
||||
}
|
||||
@@ -203,9 +213,9 @@ class ClackCLI {
|
||||
const finalNode = interpreter.getCurrentNode();
|
||||
p.note(
|
||||
finalNode?.description || 'Quest completed',
|
||||
`🏆 ${state.endingTitle}`
|
||||
`🏆 ${state.endingTitle}`,
|
||||
);
|
||||
|
||||
|
||||
const playAgain = await p.confirm({
|
||||
message: 'Would you like to play again?',
|
||||
});
|
||||
@@ -213,7 +223,8 @@ class ClackCLI {
|
||||
if (!p.isCancel(playAgain) && playAgain) {
|
||||
interpreter.reset();
|
||||
await this.gameLoop(interpreter);
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
p.outro('Thanks for playing! 🎉');
|
||||
}
|
||||
}
|
||||
@@ -221,26 +232,28 @@ class ClackCLI {
|
||||
|
||||
private async validateQuest(filename: string): Promise<void> {
|
||||
const spinner = p.spinner();
|
||||
|
||||
|
||||
try {
|
||||
spinner.start('Validating quest...');
|
||||
|
||||
|
||||
const source = this.readFile(filename);
|
||||
const validation = QuestLang.validate(source);
|
||||
|
||||
|
||||
if (validation.isValid) {
|
||||
spinner.stop('✅ Validation completed');
|
||||
p.log.success('Quest is valid!');
|
||||
p.outro('🎉 No issues found');
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
spinner.stop('❌ Validation failed');
|
||||
|
||||
|
||||
p.log.error('Validation errors:');
|
||||
validation.errors.forEach(error => p.log.error(` • ${error}`));
|
||||
p.outro('Fix the errors and try again');
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop('❌ Error during validation');
|
||||
p.log.error(error instanceof Error ? error.message : String(error));
|
||||
p.outro('Validation failed');
|
||||
@@ -250,58 +263,59 @@ class ClackCLI {
|
||||
|
||||
private async analyzeQuest(filename: string): Promise<void> {
|
||||
const spinner = p.spinner();
|
||||
|
||||
|
||||
try {
|
||||
spinner.start('Analyzing quest...');
|
||||
|
||||
|
||||
const source = this.readFile(filename);
|
||||
const interpreter = QuestLang.interpret(source);
|
||||
const questInfo = interpreter.getQuestInfo();
|
||||
|
||||
|
||||
spinner.stop('✅ Analysis completed');
|
||||
|
||||
|
||||
const nodes = Object.values(interpreter.getProgram().graph.nodes) as NodeDefinition[];
|
||||
const initialNodes = nodes.filter(n => n.nodeType === 'начальный');
|
||||
const actionNodes = nodes.filter(n => n.nodeType === 'действие');
|
||||
const endingNodes = nodes.filter(n => n.nodeType === 'концовка');
|
||||
|
||||
|
||||
p.note(
|
||||
`📊 Quest Analysis Results\n\n` +
|
||||
`Total nodes: ${nodes.length}\n` +
|
||||
` • Initial nodes: ${initialNodes.length}\n` +
|
||||
` • Action nodes: ${actionNodes.length}\n` +
|
||||
` • Ending nodes: ${endingNodes.length}`,
|
||||
`📖 ${questInfo.name}`
|
||||
`📊 Quest Analysis Results\n\n`
|
||||
+ `Total nodes: ${nodes.length}\n`
|
||||
+ ` • Initial nodes: ${initialNodes.length}\n`
|
||||
+ ` • Action nodes: ${actionNodes.length}\n`
|
||||
+ ` • Ending nodes: ${endingNodes.length}`,
|
||||
`📖 ${questInfo.name}`,
|
||||
);
|
||||
|
||||
|
||||
// Analyze paths
|
||||
const paths = interpreter.getAllPaths();
|
||||
|
||||
|
||||
if (paths.length > 0) {
|
||||
const avgPathLength = paths.reduce((sum, path) => sum + path.length, 0) / paths.length;
|
||||
const shortestPath = Math.min(...paths.map(path => path.length));
|
||||
const longestPath = Math.max(...paths.map(path => path.length));
|
||||
|
||||
|
||||
p.log.info('📈 Path Analysis:');
|
||||
p.log.info(` • Possible paths: ${paths.length}`);
|
||||
p.log.info(` • Average path length: ${avgPathLength.toFixed(1)} steps`);
|
||||
p.log.info(` • Shortest path: ${shortestPath} steps`);
|
||||
p.log.info(` • Longest path: ${longestPath} steps`);
|
||||
}
|
||||
|
||||
|
||||
// Validation
|
||||
const validation = interpreter.validate();
|
||||
|
||||
|
||||
if (validation.isValid) {
|
||||
p.log.success('✅ Quest structure is valid');
|
||||
p.outro('🎉 Analysis completed successfully');
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
p.log.warn('⚠️ Quest has validation issues:');
|
||||
validation.errors.forEach(error => p.log.warn(` • ${error}`));
|
||||
p.outro('Consider fixing these issues');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
spinner.stop('❌ Error during analysis');
|
||||
p.log.error(error instanceof Error ? error.message : String(error));
|
||||
p.outro('Analysis failed');
|
||||
@@ -313,14 +327,14 @@ class ClackCLI {
|
||||
if (!fs.existsSync(filename)) {
|
||||
throw new Error(`File not found: ${filename}`);
|
||||
}
|
||||
|
||||
|
||||
return fs.readFileSync(filename, 'utf-8');
|
||||
}
|
||||
}
|
||||
|
||||
const cli = new ClackCLI();
|
||||
|
||||
cli.run().catch(error => {
|
||||
cli.run().catch((error) => {
|
||||
p.log.error(`Unexpected error: ${error}`);
|
||||
p.outro('❌ CLI crashed');
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user