diff options
Diffstat (limited to 'tree-sitter/dsk/dsk-cli/src/commands')
-rw-r--r-- | tree-sitter/dsk/dsk-cli/src/commands/build.ts | 429 | ||||
-rw-r--r-- | tree-sitter/dsk/dsk-cli/src/commands/dev.ts | 105 | ||||
-rw-r--r-- | tree-sitter/dsk/dsk-cli/src/commands/highlight.ts | 141 | ||||
-rw-r--r-- | tree-sitter/dsk/dsk-cli/src/commands/new.ts | 485 | ||||
-rw-r--r-- | tree-sitter/dsk/dsk-cli/src/commands/package.ts | 66 | ||||
-rw-r--r-- | tree-sitter/dsk/dsk-cli/src/commands/self.ts | 71 | ||||
-rw-r--r-- | tree-sitter/dsk/dsk-cli/src/commands/test.ts | 50 |
7 files changed, 1347 insertions, 0 deletions
diff --git a/tree-sitter/dsk/dsk-cli/src/commands/build.ts b/tree-sitter/dsk/dsk-cli/src/commands/build.ts new file mode 100644 index 0000000..811788f --- /dev/null +++ b/tree-sitter/dsk/dsk-cli/src/commands/build.ts @@ -0,0 +1,429 @@ +/** + * Build Command - Core Build Process + * + * Compiles grammar.js into C static library and JavaScript package + */ + +import { Command } from 'commander'; +import { execa } from 'execa'; +import chalk from 'chalk'; +import { existsSync, mkdirSync, writeFileSync, copyFileSync, readFileSync, cpSync } from 'fs'; +import { join, dirname, basename } from 'path'; +import { fileURLToPath } from 'url'; + +/** + * Create the build command + */ +export function createBuildCommand(): Command { + const buildCommand = new Command('build'); + + buildCommand + .description('Build the DSL parser and packages') + .option('-v, --verbose', 'Show detailed build output') + .option('--skip-c', 'Skip C library generation') + .option('--skip-js', 'Skip JavaScript package generation') + .action(async (options) => { + console.log(chalk.blue('๐๏ธ Building DSL parser and packages...')); + console.log(); + + try { + await runBuildProcess(options); + + console.log(); + console.log(chalk.green('โ Build completed successfully!')); + console.log(); + console.log(chalk.blue('Generated artifacts:')); + console.log(` ${chalk.gray('โข')} C library: generated/c/lib/`); + console.log(` ${chalk.gray('โข')} C headers: generated/c/include/`); + console.log(` ${chalk.gray('โข')} JS package: generated/js/`); + + } catch (error) { + console.error(chalk.red('โ Build failed:'), error instanceof Error ? error.message : error); + process.exit(1); + } + }); + + return buildCommand; +} + +/** + * Run the complete build process + */ +async function runBuildProcess(options: { verbose?: boolean; skipC?: boolean; skipJs?: boolean }): Promise<void> { + // Verify we're in a DSL project directory + if (!existsSync('grammar.js')) { + throw new Error('No grammar.js found. Are you in a DSL project directory?'); + } + + // Ensure CommonJS semantics locally so tree-sitter can load grammar.js (uses module.exports) + ensureCommonJsPackageJson(); + + // Step 1: Generate parser with Tree-sitter + console.log(chalk.blue('1๏ธโฃ Generating parser with Tree-sitter...')); + await generateParser(options.verbose); + console.log(chalk.green(' โ Parser generated')); + + // Step 2: Build C library (unless skipped) + if (!options.skipC) { + console.log(chalk.blue('2๏ธโฃ Building C static library...')); + await buildCLibrary(options.verbose); + console.log(chalk.green(' โ C library built')); + } else { + console.log(chalk.yellow(' โญ๏ธ Skipping C library build')); + } + + // Step 3: Build JavaScript package (unless skipped) + if (!options.skipJs) { + console.log(chalk.blue('3๏ธโฃ Building JavaScript package...')); + await buildJavaScriptPackage(options.verbose); + console.log(chalk.green(' โ JavaScript package built')); + } else { + console.log(chalk.yellow(' โญ๏ธ Skipping JavaScript package build')); + } +} + +/** + * Step 1: Generate parser using tree-sitter generate + */ +async function generateParser(verbose?: boolean): Promise<void> { + try { + // Check if tree-sitter CLI is available + await checkTreeSitterAvailable(); + + // Run tree-sitter generate + const result = await execa('tree-sitter', ['generate'], { + stdio: verbose ? 'inherit' : 'pipe' + }); + + if (!verbose && result.stdout) { + console.log(chalk.gray(` ${result.stdout.split('\n').slice(-2, -1)[0] || 'Generated successfully'}`)); + } + + } catch (error: any) { + if (error.command) { + throw new Error(`Tree-sitter generation failed: ${error.message}`); + } + throw error; + } +} + +/** + * Step 2: Build C static library + */ +async function buildCLibrary(verbose?: boolean): Promise<void> { + // Create output directories + const libDir = 'generated/c/lib'; + const includeDir = 'generated/c/include'; + + mkdirSync(libDir, { recursive: true }); + mkdirSync(includeDir, { recursive: true }); + + // Get project name from grammar.js or directory + const projectName = getProjectName(); + + // Compile parser.c to object file + const objectFile = join(libDir, `${projectName}.o`); + const libraryFile = join(libDir, `lib${projectName}.a`); + + try { + // Detect C compiler + const compiler = await detectCCompiler(); + console.log(chalk.gray(` Using compiler: ${compiler}`)); + + // Compile to object file + const compileArgs = [ + '-c', // Compile only, don't link + '-fPIC', // Position independent code + '-O2', // Optimize + 'src/parser.c', // Input file + '-o', objectFile // Output file + ]; + + await execa(compiler, compileArgs, { + stdio: verbose ? 'inherit' : 'pipe' + }); + + // Create static library with ar + const arArgs = [ + 'rcs', // Create archive, insert files, write symbol table + libraryFile, // Output library + objectFile // Input object file + ]; + + await execa('ar', arArgs, { + stdio: verbose ? 'inherit' : 'pipe' + }); + + // Generate header file + await generateHeaderFile(projectName, includeDir); + + console.log(chalk.gray(` Library: ${libraryFile}`)); + console.log(chalk.gray(` Header: ${join(includeDir, projectName + '.h')}`)); + + } catch (error: any) { + throw new Error(`C library build failed: ${error.message}`); + } +} + +/** + * Step 3: Build JavaScript package + */ +async function buildJavaScriptPackage(verbose?: boolean): Promise<void> { + const jsDir = 'generated/js'; + + // Create JS package directory + mkdirSync(jsDir, { recursive: true }); + + // Copy JS addon templates + await copyJSAddonTemplates(jsDir); + + // Copy src directory from tree-sitter generation + await copySourceFiles(jsDir); + + // Update binding.gyp based on available files + await updateBindingGyp(jsDir); + + // Update package.json with project name + await updateJSPackageJson(jsDir); + + // Detect runtime and install dependencies + const runtime = await detectRuntime(); + console.log(chalk.gray(` Using runtime: ${runtime}`)); + + try { + // Install dependencies and build native addon + if (runtime === 'bun') { + await execa('bun', ['install'], { + cwd: jsDir, + stdio: verbose ? 'inherit' : 'pipe' + }); + } else { + await execa('npm', ['install'], { + cwd: jsDir, + stdio: verbose ? 'inherit' : 'pipe' + }); + } + + console.log(chalk.gray(` Package: ${jsDir}/`)); + + } catch (error: any) { + throw new Error(`JavaScript package build failed: ${error.message}`); + } +} + +/** + * Check if tree-sitter CLI is available + */ +async function checkTreeSitterAvailable(): Promise<void> { + try { + await execa('tree-sitter', ['--version'], { stdio: 'pipe' }); + } catch (error) { + throw new Error( + 'tree-sitter CLI not found. Please install it:\n' + + ' npm install -g tree-sitter-cli\n' + + ' # or\n' + + ' brew install tree-sitter' + ); + } +} + +/** + * Detect available C compiler + */ +async function detectCCompiler(): Promise<string> { + const compilers = ['clang', 'gcc', 'cc']; + + for (const compiler of compilers) { + try { + await execa(compiler, ['--version'], { stdio: 'pipe' }); + return compiler; + } catch { + // Try next compiler + } + } + + throw new Error( + 'No C compiler found. Please install one:\n' + + ' macOS: xcode-select --install\n' + + ' Linux: sudo apt install build-essential (Ubuntu) or equivalent' + ); +} + +/** + * Detect runtime (bun vs npm) + */ +async function detectRuntime(): Promise<'bun' | 'npm'> { + try { + await execa('bun', ['--version'], { stdio: 'pipe' }); + return 'bun'; + } catch { + return 'npm'; + } +} + +/** + * Get project name from grammar.js or directory name + */ +function getProjectName(): string { + try { + const grammarContent = readFileSync('grammar.js', 'utf-8'); + const nameMatch = grammarContent.match(/name:\s*['"]([^'"]+)['"]/); + if (nameMatch) { + return nameMatch[1]; + } + } catch { + // Fall back to directory name + } + + return basename(process.cwd()); +} + +/** + * Ensure the current directory is treated as CommonJS for Node resolution, + * so that tree-sitter can load `grammar.js` (which uses module.exports). + * If no local package.json exists, create a minimal one with "type": "commonjs". + */ +function ensureCommonJsPackageJson(): void { + const packageJsonPath = 'package.json'; + if (!existsSync(packageJsonPath)) { + const projectName = basename(process.cwd()); + const minimal = { + name: `${projectName}-dsl`, + private: true, + type: 'commonjs', + description: `DSL project for ${projectName} generated by DSK`, + license: 'MIT' + } as const; + writeFileSync(packageJsonPath, JSON.stringify(minimal, null, 2)); + console.log(chalk.gray(' Created local package.json with { "type": "commonjs" }')); + } +} + +/** + * Generate C header file + */ +async function generateHeaderFile(projectName: string, includeDir: string): Promise<void> { + const headerContent = `#ifndef TREE_SITTER_${projectName.toUpperCase()}_H_ +#define TREE_SITTER_${projectName.toUpperCase()}_H_ + +typedef struct TSLanguage TSLanguage; + +#ifdef __cplusplus +extern "C" { +#endif + +const TSLanguage *tree_sitter_${projectName}(void); + +#ifdef __cplusplus +} +#endif + +#endif // TREE_SITTER_${projectName.toUpperCase()}_H_ +`; + + writeFileSync(join(includeDir, `${projectName}.h`), headerContent); +} + +/** + * Copy JS addon template files + */ +async function copyJSAddonTemplates(jsDir: string): Promise<void> { + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const templateDir = join(__dirname, '..', '..', 'templates', 'js-addon'); + + if (!existsSync(templateDir)) { + throw new Error(`JS addon template directory not found: ${templateDir}`); + } + + // Copy all template files + const { processTemplate } = await import('../utils/template-processor.js'); + const projectName = getProjectName(); + + const templateContext = { + architecture: { + name: projectName, + paradigm: 'mixed' as const, + purpose: 'DSL', + dataPhilosophy: 'mixed' as const + }, + features: { controlFlow: [], dataStructures: [], functionTypes: [] }, + syntax: { + comments: { type: 'line_comment', pattern: '//' }, + identifiers: { pattern: '[a-zA-Z_][a-zA-Z0-9_]*', examples: ['identifier'] }, + numbers: { pattern: '\\d+', examples: ['42'] }, + strings: { pattern: '"[^"]*"', examples: ['"string"'] }, + variables: { keyword: 'let', operator: '=', terminator: ';', example: 'let x = 42;' }, + paradigmExamples: {} + } + }; + + processTemplate(templateDir, jsDir, templateContext); +} + +/** + * Copy source files from tree-sitter generation to JS package + */ +async function copySourceFiles(jsDir: string): Promise<void> { + const srcDir = 'src'; + const targetSrcDir = join(jsDir, 'src'); + + if (!existsSync(srcDir)) { + throw new Error('src/ directory not found. Run tree-sitter generate first.'); + } + + // Copy the entire src directory + cpSync(srcDir, targetSrcDir, { recursive: true }); + + console.log(chalk.gray(` Copied src files to ${targetSrcDir}`)); +} + +/** + * Update binding.gyp based on available source files + */ +async function updateBindingGyp(jsDir: string): Promise<void> { + const bindingGypPath = join(jsDir, 'binding.gyp'); + const scannerPath = join(jsDir, 'src', 'scanner.c'); + + if (existsSync(bindingGypPath)) { + let bindingGyp = JSON.parse(readFileSync(bindingGypPath, 'utf-8')); + + // Add scanner.c if it exists + if (existsSync(scannerPath)) { + const target = bindingGyp.targets[0]; + if (!target.sources.includes('src/scanner.c')) { + target.sources.push('src/scanner.c'); + console.log(chalk.gray(` Added scanner.c to build`)); + } + } else { + console.log(chalk.gray(` No scanner.c found, skipping`)); + } + + writeFileSync(bindingGypPath, JSON.stringify(bindingGyp, null, 2)); + } +} + +/** + * Update package.json in JS package + */ +async function updateJSPackageJson(jsDir: string): Promise<void> { + const projectName = getProjectName(); + const packageJsonPath = join(jsDir, 'package.json'); + + if (existsSync(packageJsonPath)) { + let packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')); + + // Update name and description + packageJson.name = `tree-sitter-${projectName}`; + packageJson.description = `Tree-sitter parser for ${projectName}`; + + // Update keywords + if (packageJson.keywords && Array.isArray(packageJson.keywords)) { + packageJson.keywords = packageJson.keywords.map((keyword: string) => + keyword === '__DSL_NAME__' ? projectName : keyword + ); + } + + writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + } +} diff --git a/tree-sitter/dsk/dsk-cli/src/commands/dev.ts b/tree-sitter/dsk/dsk-cli/src/commands/dev.ts new file mode 100644 index 0000000..92b2867 --- /dev/null +++ b/tree-sitter/dsk/dsk-cli/src/commands/dev.ts @@ -0,0 +1,105 @@ +/** + * Dev Command - Watch grammar.js and rebuild/test on change + */ + +import { Command } from 'commander'; +import chokidar from 'chokidar'; +import chalk from 'chalk'; +import { existsSync } from 'fs'; +import { execa } from 'execa'; + +/** + * Create the dev command + */ +export function createDevCommand(): Command { + const devCommand = new Command('dev'); + + devCommand + .description('Watch grammar.js and run build + test on changes') + .option('-v, --verbose', 'Show detailed build output') + .option('--quiet', 'Suppress non-error logs') + .option('--debounce <ms>', 'Debounce delay in milliseconds', '150') + .action(async (options) => { + if (!existsSync('grammar.js')) { + console.error(chalk.red('โ No grammar.js found. Are you in a DSL project directory?')); + process.exit(1); + } + + const verbose: boolean = Boolean(options.verbose); + const quiet: boolean = Boolean(options.quiet); + const debounceMs: number = Number.parseInt(options.debounce, 10) || 150; + + if (!quiet) { + console.log(chalk.blue('๐ Watching grammar.js for changes...')); + } + + // Initial build + test + await runBuildAndTest({ verbose, quiet }); + + // Watcher with debounced rebuilds + const watcher = chokidar.watch('grammar.js', { ignoreInitial: true }); + let isRunning = false; + let rerunRequested = false; + let debounceTimer: NodeJS.Timeout | null = null; + + const runOnce = async () => { + if (isRunning) { + rerunRequested = true; + return; + } + isRunning = true; + if (!quiet) { + console.log(chalk.yellow('โป Change detected. Rebuilding...')); + } + try { + await runBuildAndTest({ verbose, quiet }); + if (!quiet) { + console.log(chalk.green('โ Rebuild and tests completed.')); + } + } catch (e) { + // Errors already printed by build/test + } finally { + isRunning = false; + if (rerunRequested) { + rerunRequested = false; + runOnce(); + } + } + }; + + const debouncedTrigger = () => { + if (debounceTimer) clearTimeout(debounceTimer); + debounceTimer = setTimeout(runOnce, debounceMs); + }; + + watcher.on('change', debouncedTrigger); + }); + + return devCommand; +} + +async function runBuildAndTest(opts: { verbose?: boolean; quiet?: boolean }): Promise<void> { + const { verbose, quiet } = opts; + if (!quiet) { + console.log(chalk.blue('๐๏ธ Building...')); + } + try { + const buildArgs = ['build', ...(verbose ? ['--verbose'] : [])]; + await execa('dsk', buildArgs, { stdio: 'inherit' }); + } catch (error: any) { + console.error(chalk.red('โ Build failed. Fix errors and save again.')); + throw error; + } + + if (!quiet) { + console.log(chalk.blue('๐งช Testing...')); + } + try { + await execa('dsk', ['test'], { stdio: 'inherit' }); + } catch (error: any) { + console.error(chalk.red('โ Tests failed. Fix tests and save again.')); + throw error; + } +} + + diff --git a/tree-sitter/dsk/dsk-cli/src/commands/highlight.ts b/tree-sitter/dsk/dsk-cli/src/commands/highlight.ts new file mode 100644 index 0000000..fd0d419 --- /dev/null +++ b/tree-sitter/dsk/dsk-cli/src/commands/highlight.ts @@ -0,0 +1,141 @@ +/** + * Highlight Command - Generate Tree-sitter highlights and editor scaffolds + */ + +import { Command } from 'commander'; +import chalk from 'chalk'; +import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs'; +import { join } from 'path'; + +/** + * Create the highlight command + */ +export function createHighlightCommand(): Command { + const highlightCommand = new Command('highlight'); + + highlightCommand + .description('Generate Tree-sitter highlights and editor scaffolds (tree-sitter, neovim, emacs, vscode)') + .action(async () => { + if (!existsSync('grammar.js')) { + console.error(chalk.red('โ No grammar.js found. Are you in a DSL project directory?')); + process.exit(1); + } + + const languageName = getProjectName(); + + // Tree-sitter highlights + const tsOutDir = 'generated/editors/tree-sitter'; + mkdirSync(tsOutDir, { recursive: true }); + const highlights = ` +; Minimal highlights scaffold. Extend as needed. + +; Comments +(comment) @comment + +; Strings +(string) @string + +; Numbers +(number) @number + +; Keywords (example) +[("if") ("else") ("let") ("function")] @keyword +`; + const tsFile = join(tsOutDir, 'highlights.scm'); + writeFileSync(tsFile, highlights.trim() + '\n', 'utf-8'); + console.log(chalk.green(`โ Generated ${tsFile}`)); + + // Neovim instructions + const nvimDir = 'generated/editors/neovim'; + mkdirSync(nvimDir, { recursive: true }); + const nvimMd = `# Neovim setup for ${languageName} + +1. Copy queries to your runtimepath: queries/${languageName}/highlights.scm +2. Configure nvim-treesitter: + +\`\`\`lua +require('nvim-treesitter.configs').setup({ + ensure_installed = {}, + highlight = { enable = true }, +}) +\`\`\` +`; + writeFileSync(join(nvimDir, 'setup-instructions.md'), nvimMd, 'utf-8'); + console.log(chalk.green(`โ Generated ${join(nvimDir, 'setup-instructions.md')}`)); + + // Emacs major mode (minimal) + const emacsDir = 'generated/editors/emacs'; + mkdirSync(emacsDir, { recursive: true }); + const emacsEl = `;;; ${languageName}-mode.el --- ${languageName} mode -*- lexical-binding: t; -*- + +(require 'treesit) + +(define-derived-mode ${languageName}-mode prog-mode "${languageName}" + "Major mode for ${languageName} using Tree-sitter." + (when (treesit-ready-p '${languageName}) + (treesit-parser-create '${languageName}))) + +(add-to-list 'auto-mode-alist '("\\.${languageName}$" . ${languageName}-mode)) + +(provide '${languageName}-mode) +`; + writeFileSync(join(emacsDir, `${languageName}-mode.el`), emacsEl, 'utf-8'); + console.log(chalk.green(`โ Generated ${join(emacsDir, `${languageName}-mode.el`)}`)); + + // VS Code: TextMate grammar + language configuration (basic placeholders) + const vscodeSyntaxDir = 'generated/editors/vscode/syntaxes'; + const vscodeDir = 'generated/editors/vscode'; + mkdirSync(vscodeSyntaxDir, { recursive: true }); + mkdirSync(vscodeDir, { recursive: true }); + const scope = `source.${languageName}`; + const tmLanguage = { + name: languageName, + scopeName: scope, + patterns: [ + { include: '#comment' }, + { include: '#string' }, + { include: '#number' }, + { include: '#keyword' } + ], + repository: { + comment: { patterns: [{ name: 'comment.line.double-slash', match: '//.*$' }] }, + string: { patterns: [{ name: 'string.quoted.double', begin: '"', end: '"' }] }, + number: { patterns: [{ name: 'constant.numeric', match: '-?\\b[0-9]+(\\.[0-9]+)?\\b' }] }, + keyword: { patterns: [{ name: 'keyword.control', match: '\\b(if|else|let|function)\\b' }] } + } + } as const; + writeFileSync(join(vscodeSyntaxDir, `${languageName}.tmLanguage.json`), JSON.stringify(tmLanguage, null, 2)); + const langConfig = { + comments: { lineComment: '//' }, + brackets: [["{","}"],["[","]"],["(",")"]], + autoClosingPairs: [ + { open: '"', close: '"' }, + { open: '{', close: '}' }, + { open: '(', close: ')' }, + { open: '[', close: ']' } + ], + surroundingPairs: [ + { open: '"', close: '"' }, + { open: '{', close: '}' }, + { open: '(', close: ')' }, + { open: '[', close: ']' } + ] + }; + writeFileSync(join(vscodeDir, 'language-configuration.json'), JSON.stringify(langConfig, null, 2)); + console.log(chalk.green(`โ Generated VS Code syntax and configuration`)); + }); + + return highlightCommand; +} + +function getProjectName(): string { + try { + const grammarContent = readFileSync('grammar.js', 'utf-8'); + const nameMatch = grammarContent.match(/name:\s*['"]([^'"]+)['"]/); + if (nameMatch) return nameMatch[1]; + } catch {} + const parts = process.cwd().split(/[\\/]/); + return parts[parts.length - 1] || 'dsl'; +} + + diff --git a/tree-sitter/dsk/dsk-cli/src/commands/new.ts b/tree-sitter/dsk/dsk-cli/src/commands/new.ts new file mode 100644 index 0000000..1829a21 --- /dev/null +++ b/tree-sitter/dsk/dsk-cli/src/commands/new.ts @@ -0,0 +1,485 @@ +/** + * New Command - Interactive Grammar Scaffolding + * + * Creates a new DSL project with paradigm-aware grammar generation + */ + +import { Command } from 'commander'; +import inquirer from 'inquirer'; +import chalk from 'chalk'; +import { existsSync, mkdirSync, writeFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { inferPattern, validatePattern, generateCustomPattern } from '../utils/inference.js'; +import { generateGrammar, generateGrammarFile } from '../utils/grammar-generator.js'; +import { processTemplate } from '../utils/template-processor.js'; + +// Type definitions for our interactive flow +export interface LanguageArchitecture { + name: string; + purpose: string; + paradigm: 'functional' | 'object-oriented' | 'procedural' | 'declarative' | 'mixed'; + dataPhilosophy: 'immutable' | 'mutable' | 'mixed'; +} + +export interface LanguageFeatures { + controlFlow: string[]; + dataStructures: string[]; + functionTypes: string[]; +} + +export interface LanguageSyntax { + comments: { type: string; pattern: string }; + identifiers: { pattern: string; examples: string[] }; + numbers: { pattern: string; examples: string[] }; + strings: { pattern: string; examples: string[] }; + variables: { keyword: string; operator: string; terminator: string; example: string }; + paradigmExamples: { [key: string]: string }; +} + +/** + * Create the new command + */ +export function createNewCommand(): Command { + const newCommand = new Command('new'); + + newCommand + .description('Create a new DSL project') + .argument('<name>', 'Name of the DSL project') + .option('-i, --interactive', 'Use interactive grammar scaffolding') + .option('-t, --template <template>', 'Use a specific template (default: basic)') + .action(async (name: string, options) => { + console.log(chalk.blue('๐ Creating DSL project:'), chalk.bold(name)); + + if (options.interactive) { + await runInteractiveFlow(name); + } else { + await createBasicProject(name, options.template || 'basic'); + } + }); + + return newCommand; +} + +/** + * Run the interactive grammar scaffolding flow + */ +async function runInteractiveFlow(projectName: string): Promise<void> { + console.log(); + console.log(chalk.green('๐ฏ Welcome to the DSK Grammar Scaffolder!')); + console.log(chalk.gray('I\'ll ask you a few questions about your new language.')); + console.log(chalk.gray('Just provide examples, and I\'ll build a starter grammar for you.')); + console.log(); + + try { + // Phase A: Language Architecture & Paradigm + console.log(chalk.blue('๐ Phase A: Language Architecture & Paradigm')); + const architecture = await gatherArchitecture(projectName); + console.log(chalk.green('โ'), 'Architecture defined'); + + // Phase B: Core Language Features + console.log(); + console.log(chalk.blue('๐ง Phase B: Core Language Features')); + const features = await gatherFeatures(architecture); + console.log(chalk.green('โ'), 'Features defined'); + + // Phase C: Syntax & Tokens + console.log(); + console.log(chalk.blue('๐ค Phase C: Syntax & Tokens')); + const syntax = await gatherSyntax(architecture, features); + console.log(chalk.green('โ'), 'Syntax defined'); + + // Generate the project + console.log(); + console.log(chalk.blue('๐๏ธ Generating project...')); + await generateProject(projectName, architecture, features, syntax); + + // Success message + console.log(); + console.log(chalk.green('๐ All done!')); + console.log(`Your ${chalk.bold('grammar.js')} has been created with rules for:`); + console.log(` ${chalk.gray('โข')} Comments, Identifiers, Numbers, Strings`); + console.log(` ${chalk.gray('โข')} Variable Declarations`); + console.log(` ${chalk.gray('โข')} ${architecture.paradigm} language constructs`); + console.log(); + console.log(chalk.yellow('To start editing and testing, run:')); + console.log(` ${chalk.cyan('cd')} ${projectName}`); + console.log(` ${chalk.cyan('dsk dev')}`); + console.log(); + + } catch (error) { + console.error(chalk.red('โ Error during interactive flow:'), error); + process.exit(1); + } +} + +/** + * Phase A: Gather language architecture information + */ +async function gatherArchitecture(projectName: string): Promise<LanguageArchitecture> { + const purposeAnswer = await inquirer.prompt({ + type: 'input', + name: 'purpose', + message: 'What is your language designed for? (e.g., configuration, scripting, domain modeling)', + default: 'General purpose scripting' + }); + + const paradigmAnswer = await inquirer.prompt({ + type: 'list', + name: 'paradigm', + message: 'What programming style does your language follow?', + choices: [ + { name: 'Functional (immutable data, functions as first-class)', value: 'functional' }, + { name: 'Object-Oriented (classes, inheritance, methods)', value: 'object-oriented' }, + { name: 'Procedural (step-by-step instructions, functions)', value: 'procedural' }, + { name: 'Declarative (describe what, not how)', value: 'declarative' }, + { name: 'Mixed (combination of above)', value: 'mixed' } + ], + default: 'procedural' + }); + + const dataAnswer = await inquirer.prompt({ + type: 'list', + name: 'dataPhilosophy', + message: 'How does your language handle data?', + choices: [ + { name: 'Immutable by default (functional style)', value: 'immutable' }, + { name: 'Mutable variables (imperative style)', value: 'mutable' }, + { name: 'Mixed approach', value: 'mixed' } + ], + default: 'mutable' + }); + + return { + name: projectName, + purpose: purposeAnswer.purpose, + paradigm: paradigmAnswer.paradigm, + dataPhilosophy: dataAnswer.dataPhilosophy + }; +} + +/** + * Phase B: Gather core language features + */ +async function gatherFeatures(architecture: LanguageArchitecture): Promise<LanguageFeatures> { + const controlFlowAnswer = await inquirer.prompt({ + type: 'checkbox', + name: 'controlFlow', + message: 'What control structures does your language support?', + choices: [ + { name: 'Conditionals (if/else)', value: 'conditionals', checked: true }, + { name: 'Loops (for, while)', value: 'loops' }, + { name: 'Pattern matching', value: 'pattern_matching' }, + { name: 'Exception handling (try/catch)', value: 'exceptions' }, + { name: 'Early returns/breaks', value: 'early_returns' } + ] + }); + + const dataStructuresAnswer = await inquirer.prompt({ + type: 'checkbox', + name: 'dataStructures', + message: 'What built-in data structures does your language have?', + choices: [ + { name: 'Arrays/Lists: [1, 2, 3]', value: 'arrays', checked: true }, + { name: 'Objects/Maps: {key: value}', value: 'objects' }, + { name: 'Tuples: (a, b, c)', value: 'tuples' }, + { name: 'Sets: {1, 2, 3}', value: 'sets' } + ] + }); + + const functionTypesAnswer = await inquirer.prompt({ + type: 'checkbox', + name: 'functionTypes', + message: 'How are functions defined in your language?', + choices: [ + { name: 'Named functions: function foo() { ... }', value: 'named', checked: true }, + { name: 'Anonymous functions: (x) => x + 1', value: 'anonymous' }, + { name: 'Methods on objects: obj.method()', value: 'methods' }, + { name: 'First-class functions (can be passed around)', value: 'first_class' } + ] + }); + + return { + controlFlow: controlFlowAnswer.controlFlow, + dataStructures: dataStructuresAnswer.dataStructures, + functionTypes: functionTypesAnswer.functionTypes + }; +} + +/** + * Phase C: Gather syntax and token information + */ +async function gatherSyntax(architecture: LanguageArchitecture, features: LanguageFeatures): Promise<LanguageSyntax> { + const syntax: Partial<LanguageSyntax> = {}; + + // Comments + const commentAnswer = await inquirer.prompt({ + type: 'input', + name: 'comment', + message: 'How do you write a single-line comment? (e.g., //, #, --, ;)', + default: '//' + }); + + const commentPrefix = String(commentAnswer.comment).trim().split(/\s+/)[0]; + syntax.comments = { + type: 'line_comment', + pattern: commentPrefix + }; + + // Identifiers with inference + syntax.identifiers = await gatherTokenWithInference( + 'identifier', + 'Provide 3-5 examples of valid identifiers', + 'Now provide 2-3 examples of invalid identifiers (optional)', + ['myVar', 'userName', '_private'] + ); + + // Numbers with inference + syntax.numbers = await gatherTokenWithInference( + 'number', + 'Provide examples of numbers in your language', + 'Provide examples of invalid numbers (optional)', + ['42', '3.14', '-17'] + ); + + // Strings with inference (treat input as a single example, not space-split) + const stringValid = await inquirer.prompt({ + type: 'input', + name: 'examples', + message: 'Provide an example of a string literal', + default: '"hello world"' + }); + const stringInvalid = await inquirer.prompt({ + type: 'input', + name: 'examples', + message: 'Provide examples of invalid strings (optional)', + default: '' + }); + const stringValidExamples = stringValid.examples ? [String(stringValid.examples)] : ['"hello world"']; + const stringInvalidExamples = stringInvalid.examples ? [String(stringInvalid.examples)] : []; + const stringResult = inferPattern(stringValidExamples, stringInvalidExamples); + if (stringResult.pattern && stringResult.confidence > 0.7) { + syntax.strings = { pattern: stringResult.pattern.regex.source, examples: stringValidExamples }; + } else { + syntax.strings = { pattern: '"[^"]*"', examples: stringValidExamples }; + } + + // Variable declarations + const varAnswer = await inquirer.prompt({ + type: 'input', + name: 'example', + message: 'Show me how you declare a variable x with value 42 (helps identify keywords)', + default: 'let x = 42;' + }); + + const varParts = parseVariableDeclaration(varAnswer.example); + syntax.variables = varParts; + + // Paradigm-specific examples + syntax.paradigmExamples = {}; + + if (architecture.paradigm === 'object-oriented' || architecture.paradigm === 'mixed') { + const classAnswer = await inquirer.prompt({ + type: 'input', + name: 'classExample', + message: 'Show me how you define a class with a method', + default: 'class Person { getName() { return this.name; } }' + }); + syntax.paradigmExamples.class = classAnswer.classExample; + } + + if (architecture.paradigm === 'functional' || architecture.paradigm === 'mixed') { + const funcAnswer = await inquirer.prompt({ + type: 'input', + name: 'funcExample', + message: 'Show me how you define and call a function', + default: 'function add(a, b) { return a + b; }' + }); + syntax.paradigmExamples.function = funcAnswer.funcExample; + } + + if (architecture.paradigm === 'declarative') { + const ruleAnswer = await inquirer.prompt({ + type: 'input', + name: 'ruleExample', + message: 'Show me a typical declaration/rule in your language', + default: 'rule user_can_edit when user.role == "admin"' + }); + syntax.paradigmExamples.rule = ruleAnswer.ruleExample; + } + + return syntax as LanguageSyntax; +} + +/** + * Gather token information with automatic pattern inference + */ +async function gatherTokenWithInference( + tokenType: string, + validPrompt: string, + invalidPrompt: string, + defaultExamples: string[] +): Promise<{ pattern: string; examples: string[] }> { + + // Get valid examples + const validAnswer = await inquirer.prompt({ + type: 'input', + name: 'examples', + message: `${validPrompt} (separate with spaces)`, + default: defaultExamples.join(' ') + }); + + const validExamples = validAnswer.examples.split(/\s+/).filter((ex: string) => ex.length > 0); + + // Get invalid examples (optional) + const invalidAnswer = await inquirer.prompt({ + type: 'input', + name: 'examples', + message: `${invalidPrompt} (separate with spaces, or press Enter to skip)`, + default: '' + }); + + const invalidExamples = invalidAnswer.examples + ? invalidAnswer.examples.split(/\s+/).filter((ex: string) => ex.length > 0) + : []; + + // Try inference + const result = inferPattern(validExamples, invalidExamples); + + if (result.pattern && result.confidence > 0.7) { + // Successful inference - confirm with user + const confirmAnswer = await inquirer.prompt({ + type: 'confirm', + name: 'confirmed', + message: `I've inferred the pattern for ${tokenType} as: ${chalk.cyan(result.pattern.regex.source)}. Does this look correct?`, + default: true + }); + + if (confirmAnswer.confirmed) { + return { + pattern: result.pattern.regex.source, + examples: validExamples + }; + } + } + + // Inference failed or user rejected - offer alternatives + console.log(chalk.yellow(`I couldn't determine a reliable pattern from those examples.`)); + + const fallbackAnswer = await inquirer.prompt({ + type: 'list', + name: 'option', + message: 'How would you like to proceed?', + choices: [ + { name: 'Provide a custom regular expression', value: 'custom_regex' }, + { name: 'Generate a simple pattern from examples', value: 'simple_pattern' }, + { name: 'Try different examples', value: 'retry' } + ] + }); + + if (fallbackAnswer.option === 'custom_regex') { + const regexAnswer = await inquirer.prompt({ + type: 'input', + name: 'regex', + message: `Enter a regular expression for ${tokenType}:`, + validate: (input: string) => { + const validation = validatePattern(input, validExamples, invalidExamples); + return validation.isValid || validation.errors.join(', '); + } + }); + + return { + pattern: regexAnswer.regex, + examples: validExamples + }; + } + + if (fallbackAnswer.option === 'simple_pattern') { + const simplePattern = generateCustomPattern(validExamples, invalidExamples); + return { + pattern: simplePattern, + examples: validExamples + }; + } + + // Retry with different examples + return await gatherTokenWithInference(tokenType, validPrompt, invalidPrompt, defaultExamples); +} + +/** + * Parse variable declaration to extract components + */ +function parseVariableDeclaration(example: string): { keyword: string; operator: string; terminator: string; example: string } { + // Simple parsing - look for common patterns + const patterns = [ + /^(\w+)\s+(\w+)\s*([=:])\s*[^;]*([;]?)/, // let x = 42; + /^(\w+)\s*([=:])\s*[^;]*([;]?)/, // x = 42; + ]; + + for (const pattern of patterns) { + const match = example.match(pattern); + if (match) { + return { + keyword: match[1] || '', + operator: match[3] || '=', + terminator: match[4] || ';', + example + }; + } + } + + // Fallback + return { + keyword: 'let', + operator: '=', + terminator: ';', + example + }; +} + +/** + * Generate the project with collected information + */ +async function generateProject( + name: string, + architecture: LanguageArchitecture, + features: LanguageFeatures, + syntax: LanguageSyntax +): Promise<void> { + // Create project directory + if (existsSync(name)) { + throw new Error(`Directory ${name} already exists`); + } + + mkdirSync(name, { recursive: true }); + + // Generate grammar.js file + console.log(chalk.gray('๐ง Generating grammar.js...')); + const grammar = generateGrammar(architecture, features, syntax); + const grammarContent = generateGrammarFile(grammar); + writeFileSync(join(name, 'grammar.js'), grammarContent, 'utf-8'); + + // Process template files + console.log(chalk.gray('๐ Creating project structure...')); + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const templateDir = join(__dirname, '..', '..', 'templates', 'default'); + + const templateContext = { + architecture, + features, + syntax + }; + + processTemplate(templateDir, name, templateContext); + + console.log(chalk.gray(`๐ Created project directory: ${name}/`)); + console.log(chalk.green('โจ Project generated successfully!')); +} + +/** + * Create a basic project without interactive flow + */ +async function createBasicProject(name: string, template: string): Promise<void> { + console.log(chalk.yellow(`๐ง Basic project creation (template: ${template}) coming soon!`)); +} diff --git a/tree-sitter/dsk/dsk-cli/src/commands/package.ts b/tree-sitter/dsk/dsk-cli/src/commands/package.ts new file mode 100644 index 0000000..515c2ce --- /dev/null +++ b/tree-sitter/dsk/dsk-cli/src/commands/package.ts @@ -0,0 +1,66 @@ +/** + * Package Command - Create distributable artifacts for C and JS outputs + */ + +import { Command } from 'commander'; +import chalk from 'chalk'; +import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs'; +import { join } from 'path'; +import { execa } from 'execa'; + +/** + * Create the package command + */ +export function createPackageCommand(): Command { + const pkgCommand = new Command('package'); + + pkgCommand + .description('Package C and JS artifacts into distributable archives') + .action(async () => { + if (!existsSync('grammar.js')) { + console.error(chalk.red('โ No grammar.js found. Are you in a DSL project directory?')); + process.exit(1); + } + + console.log(chalk.blue('๐ฆ Packaging artifacts...')); + + // Ensure build is up to date + await execa('dsk', ['build'], { stdio: 'inherit' }); + + const distDir = 'dist'; + mkdirSync(distDir, { recursive: true }); + + // Zip C outputs (simple tar.gz using system tar to avoid extra deps) + const cDir = 'generated/c'; + if (existsSync(cDir)) { + const cArchive = join(distDir, 'c-artifacts.tar.gz'); + await execa('tar', ['-czf', cArchive, '-C', 'generated', 'c'], { stdio: 'inherit' }); + console.log(chalk.green(`โ C artifacts: ${cArchive}`)); + } + + // Pack JS package + const jsDir = 'generated/js'; + if (existsSync(jsDir)) { + // Prefer bun pack, fallback to npm pack + let pkgPath = ''; + try { + const { stdout } = await execa('bun', ['pack'], { cwd: jsDir }); + pkgPath = stdout.trim(); + } catch { + const { stdout } = await execa('npm', ['pack'], { cwd: jsDir }); + pkgPath = stdout.trim(); + } + const fileName = pkgPath.split(/\s|\n/).pop() as string; + const srcPath = join(jsDir, fileName); + const destPath = join(distDir, fileName); + await execa('bash', ['-lc', `cp ${JSON.stringify(srcPath)} ${JSON.stringify(destPath)}`]); + console.log(chalk.green(`โ JS package: ${destPath}`)); + } + + console.log(chalk.blue('๐ Packaging complete.')); + }); + + return pkgCommand; +} + + diff --git a/tree-sitter/dsk/dsk-cli/src/commands/self.ts b/tree-sitter/dsk/dsk-cli/src/commands/self.ts new file mode 100644 index 0000000..2b43fd6 --- /dev/null +++ b/tree-sitter/dsk/dsk-cli/src/commands/self.ts @@ -0,0 +1,71 @@ +/** + * Self Packaging Command - Build and package the dsk CLI for distribution + */ + +import { Command } from 'commander'; +import chalk from 'chalk'; +import { execa } from 'execa'; +import { existsSync, mkdirSync, readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +/** + * Create the self:package command + */ +export function createSelfPackageCommand(): Command { + const cmd = new Command('self:package'); + + cmd + .description('Build and package the dsk CLI into a .tgz for distribution') + .option('-v, --verbose', 'Show detailed build output') + .action(async (options) => { + const projectRoot = resolveProjectRoot(); + if (!existsSync(join(projectRoot, 'package.json'))) { + console.error(chalk.red('โ Could not locate package.json for dsk-cli')); + process.exit(1); + } + + const pkg = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf-8')); + console.log(chalk.blue(`๐ฆ Packaging ${pkg.name}@${pkg.version}`)); + + // 1) Build TypeScript โ dist + console.log(chalk.blue('๐๏ธ Building CLI...')); + await execa('bun', ['x', 'tsc'], { cwd: projectRoot, stdio: options.verbose ? 'inherit' : 'inherit' }); + + // 2) Pack npm tarball using bun pack (fallback to npm pack) + console.log(chalk.blue('๐งฐ Creating package tarball...')); + let tgzName = ''; + try { + const { stdout } = await execa('bun', ['pack'], { cwd: projectRoot }); + tgzName = stdout.trim().split(/\s|\n/).pop() || ''; + } catch { + const { stdout } = await execa('npm', ['pack'], { cwd: projectRoot }); + tgzName = stdout.trim().split(/\s|\n/).pop() || ''; + } + + if (!tgzName) { + console.error(chalk.red('โ Failed to determine generated package filename')); + process.exit(1); + } + + const releaseDir = join(projectRoot, 'release'); + mkdirSync(releaseDir, { recursive: true }); + const src = join(projectRoot, tgzName); + const dest = join(releaseDir, tgzName); + await execa('bash', ['-lc', `mv -f ${JSON.stringify(src)} ${JSON.stringify(dest)}`]); + + console.log(chalk.green(`โ Created ${dest}`)); + }); + + return cmd; +} + +function resolveProjectRoot(): string { + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + // When compiled, this file lives under dist/commands. Project root is two levels up. + const candidate = join(__dirname, '..', '..'); + return candidate; +} + + diff --git a/tree-sitter/dsk/dsk-cli/src/commands/test.ts b/tree-sitter/dsk/dsk-cli/src/commands/test.ts new file mode 100644 index 0000000..694acc7 --- /dev/null +++ b/tree-sitter/dsk/dsk-cli/src/commands/test.ts @@ -0,0 +1,50 @@ +/** + * Test Command - Wrapper around `tree-sitter test` + * + * Streams test output directly to the console. + */ + +import { Command } from 'commander'; +import { execa } from 'execa'; +import chalk from 'chalk'; +import { existsSync } from 'fs'; + +/** + * Create the test command + */ +export function createTestCommand(): Command { + const testCommand = new Command('test'); + + testCommand + .description('Run tree-sitter tests and stream output') + .allowExcessArguments(true) + .option('-u, --update', 'Update expected outputs (snapshots)') + .option('-f, --filter <regex>', 'Only run tests whose descriptions match the regex') + .option('--cwd <dir>', 'Run tests with a different working directory') + .option('-v, --verbose', 'Show verbose output (passes through to tree-sitter)') + .argument('[patterns...]', 'Optional test patterns (filenames or test names)') + .action(async (patterns: string[], options) => { + if (!existsSync('grammar.js')) { + console.error(chalk.red('โ No grammar.js found. Are you in a DSL project directory?')); + process.exit(1); + } + + try { + console.log(chalk.blue('๐งช Running tests...')); + const args = ['test']; + if (options.update) args.push('-u'); + if (options.filter) args.push('-f', String(options.filter)); + if (options.verbose) args.push('--verbose'); + args.push(...patterns); + await execa('tree-sitter', args, { stdio: 'inherit', cwd: options.cwd || process.cwd() }); + } catch (error: any) { + const message = error?.message || error || 'Unknown error'; + console.error(chalk.red('โ Tests failed:'), message); + process.exit(1); + } + }); + + return testCommand; +} + + |