diff --git a/index.ts b/index.ts index ada42a1..5201291 100644 --- a/index.ts +++ b/index.ts @@ -1,69 +1,102 @@ #!/usr/bin/env node -import { Command } from 'commander'; -import inquirer from 'inquirer'; -import fs from 'fs-extra'; -import path from 'path'; -import { execSync } from 'child_process'; -import chalk from 'chalk'; -import ora from 'ora'; -import { fileURLToPath } from 'url'; +import chalk from "chalk"; +import { execSync } from "child_process"; +import { Command } from "commander"; +import fs from "fs-extra"; +import inquirer from "inquirer"; +import ora from "ora"; +import path from "path"; +import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Types for better type safety -type LanguageKey = 'typescript' | 'python'; -type TemplateKey = 'sample-app' | 'browser-use' | 'stagehand'; +type LanguageKey = "typescript" | "python"; +type TemplateKey = + | "sample-app" + | "browser-use" + | "stagehand" + | "persistent-browser"; type LanguageInfo = { name: string; shorthand: string }; -type TemplateInfo = { name: string; description: string; languages: LanguageKey[] }; +type TemplateInfo = { + name: string; + description: string; + languages: LanguageKey[]; +}; // String constants -const LANGUAGE_TYPESCRIPT = 'typescript'; -const LANGUAGE_PYTHON = 'python'; -const TEMPLATE_SAMPLE_APP = 'sample-app'; -const TEMPLATE_BROWSER_USE = 'browser-use'; -const TEMPLATE_STAGEHAND = 'stagehand'; -const LANGUAGE_SHORTHAND_TS = 'ts'; -const LANGUAGE_SHORTHAND_PY = 'py'; +const LANGUAGE_TYPESCRIPT = "typescript"; +const LANGUAGE_PYTHON = "python"; +const TEMPLATE_SAMPLE_APP = "sample-app"; +const TEMPLATE_BROWSER_USE = "browser-use"; +const TEMPLATE_STAGEHAND = "stagehand"; +const TEMPLATE_PERSISTENT_BROWSER = "persistent-browser"; +const LANGUAGE_SHORTHAND_TS = "ts"; +const LANGUAGE_SHORTHAND_PY = "py"; // Configuration constants const LANGUAGES: Record = { - [LANGUAGE_TYPESCRIPT]: { name: 'TypeScript', shorthand: LANGUAGE_SHORTHAND_TS }, - [LANGUAGE_PYTHON]: { name: 'Python', shorthand: LANGUAGE_SHORTHAND_PY } + [LANGUAGE_TYPESCRIPT]: { + name: "TypeScript", + shorthand: LANGUAGE_SHORTHAND_TS, + }, + [LANGUAGE_PYTHON]: { name: "Python", shorthand: LANGUAGE_SHORTHAND_PY }, }; const TEMPLATES: Record = { - [TEMPLATE_SAMPLE_APP]: { - name: 'Sample App', - description: 'Extracts page title using Playwright', - languages: [LANGUAGE_TYPESCRIPT, LANGUAGE_PYTHON] + [TEMPLATE_SAMPLE_APP]: { + name: "Sample App", + description: "Extracts page title using Playwright", + languages: [LANGUAGE_TYPESCRIPT, LANGUAGE_PYTHON], }, [TEMPLATE_BROWSER_USE]: { - name: 'Browser Use', - description: 'Implements Browser Use SDK', - languages: [LANGUAGE_PYTHON] + name: "Browser Use", + description: "Implements Browser Use SDK", + languages: [LANGUAGE_PYTHON], + }, + [TEMPLATE_STAGEHAND]: { + name: "Stagehand", + description: "Implements the Stagehand SDK", + languages: [LANGUAGE_TYPESCRIPT], }, - [TEMPLATE_STAGEHAND]: { - name: 'Stagehand', - description: 'Implements the Stagehand SDK', - languages: [LANGUAGE_TYPESCRIPT] + [TEMPLATE_PERSISTENT_BROWSER]: { + name: "Persistent Browser", + description: + "Implements a persistent browser that maintains state across invocations", + languages: [LANGUAGE_TYPESCRIPT], }, }; -const INVOKE_SAMPLES: Record = { - 'typescript-sample-app': 'kernel invoke ts-basic get-page-title --payload \'{"url": "https://www.google.com"}\'', - 'python-sample-app': 'kernel invoke python-basic get-page-title --payload \'{"url": "https://www.google.com"}\'', - 'python-browser-use': 'kernel invoke python-bu bu-task --payload \'{"task": "Compare the price of gpt-4o and DeepSeek-V3"}\'', - 'typescript-stagehand': 'kernel invoke ts-stagehand stagehand-task --payload \'{"query": "Best wired earbuds"}\'' +const INVOKE_SAMPLES: Record< + LanguageKey, + Partial> +> = { + [LANGUAGE_TYPESCRIPT]: { + [TEMPLATE_SAMPLE_APP]: + 'kernel invoke ts-basic get-page-title --payload \'{"url": "https://www.google.com"}\'', + [TEMPLATE_STAGEHAND]: + 'kernel invoke ts-stagehand stagehand-task --payload \'{"query": "Best wired earbuds"}\'', + [TEMPLATE_PERSISTENT_BROWSER]: + 'kernel invoke ts-persistent-browser persistent-browser-task --payload \'{"url": "https://news.ycombinator.com/"}\'', + }, + [LANGUAGE_PYTHON]: { + [TEMPLATE_SAMPLE_APP]: + 'kernel invoke python-basic get-page-title --payload \'{"url": "https://www.google.com"}\'', + [TEMPLATE_BROWSER_USE]: + 'kernel invoke python-bu bu-task --payload \'{"task": "Compare the price of gpt-4o and DeepSeek-V3"}\'', + [TEMPLATE_PERSISTENT_BROWSER]: + 'kernel invoke python-persistent-browser persistent-browser-task --payload \'{"url": "https://news.ycombinator.com/"}\'', + }, }; const CONFIG = { - templateBasePath: path.resolve(__dirname, '../templates'), - defaultAppName: 'my-kernel-app', + templateBasePath: path.resolve(__dirname, "../templates"), + defaultAppName: "my-kernel-app", installCommands: { - typescript: 'npm install', - python: 'uv venv' - } + typescript: "npm install", + python: "uv venv", + }, }; // Helper for extracting error messages @@ -76,48 +109,55 @@ function getErrorMessage(error: unknown): string { function normalizeLanguage(language: string): LanguageKey | null { if (language === LANGUAGE_SHORTHAND_TS) return LANGUAGE_TYPESCRIPT; if (language === LANGUAGE_SHORTHAND_PY) return LANGUAGE_PYTHON; - return (LANGUAGES[language as LanguageKey]) ? language as LanguageKey : null; + return LANGUAGES[language as LanguageKey] ? (language as LanguageKey) : null; } // Validate if a template is available for the selected language -function isTemplateValidForLanguage(template: string, language: LanguageKey): boolean { +function isTemplateValidForLanguage( + template: string, + language: LanguageKey +): boolean { return ( - TEMPLATES[template as TemplateKey] !== undefined && + TEMPLATES[template as TemplateKey] !== undefined && TEMPLATES[template as TemplateKey].languages.includes(language) ); } // Get list of templates available for a language -function getAvailableTemplatesForLanguage(language: LanguageKey): { name: string; value: string }[] { +function getAvailableTemplatesForLanguage( + language: LanguageKey +): { name: string; value: string }[] { return Object.entries(TEMPLATES) .filter(([_, { languages }]) => languages.includes(language)) - .map(([value, { name, description }]) => ({ - name: `${name} - ${description}`, - value + .map(([value, { name, description }]) => ({ + name: `${name} - ${description}`, + value, })); } // Prompt for app name if not provided async function promptForAppName(providedAppName?: string): Promise { if (providedAppName) return providedAppName; - - const { appName } = await inquirer.prompt([{ - type: 'input', - name: 'appName', - message: 'What is the name of your project?', - default: CONFIG.defaultAppName, - validate: (input: string): boolean | string => { - if (/^([A-Za-z\-_\d])+$/.test(input)) return true; - return 'Project name may only include letters, numbers, underscores and hyphens.'; - } - }]); - + + const { appName } = await inquirer.prompt([ + { + type: "input", + name: "appName", + message: "What is the name of your project?", + default: CONFIG.defaultAppName, + validate: (input: string): boolean | string => { + if (/^([A-Za-z\-_\d])+$/.test(input)) return true; + return "Project name may only include letters, numbers, underscores and hyphens."; + }, + }, + ]); + return appName; } // Prompt for programming language async function promptForLanguage( - providedLanguage?: string, + providedLanguage?: string, supportedLanguages: LanguageKey[] = Object.keys(LANGUAGES) as LanguageKey[] ): Promise { // If language provided, normalize it @@ -126,44 +166,49 @@ async function promptForLanguage( if (normalizedLanguage && supportedLanguages.includes(normalizedLanguage)) { return normalizedLanguage; } - + // If provided but not valid, we'll warn user later and prompt anyway } - - const { language } = await inquirer.prompt([{ - type: 'list', - name: 'language', - message: 'Choose a programming language:', - choices: Object.entries(LANGUAGES) - .filter(([key]) => supportedLanguages.includes(key as LanguageKey)) - .map(([value, { name }]) => ({ - name, value - })) - }]); - + + const { language } = await inquirer.prompt([ + { + type: "list", + name: "language", + message: "Choose a programming language:", + choices: Object.entries(LANGUAGES) + .filter(([key]) => supportedLanguages.includes(key as LanguageKey)) + .map(([value, { name }]) => ({ + name, + value, + })), + }, + ]); + return language; } // Prompt for template async function promptForTemplate( - language: LanguageKey, + language: LanguageKey, providedTemplate?: string ): Promise { // If template provided and valid for language, use it if ( - providedTemplate && + providedTemplate && isTemplateValidForLanguage(providedTemplate, language) ) { return providedTemplate as TemplateKey; } - - const { template } = await inquirer.prompt([{ - type: 'list', - name: 'template', - message: 'Choose a template:', - choices: getAvailableTemplatesForLanguage(language), - }]); - + + const { template } = await inquirer.prompt([ + { + type: "list", + name: "template", + message: "Choose a template:", + choices: getAvailableTemplatesForLanguage(language), + }, + ]); + return template as TemplateKey; } @@ -171,154 +216,218 @@ async function promptForTemplate( async function prepareProjectDirectory(appPath: string): Promise { // Check if directory exists if (fs.existsSync(appPath)) { - const { overwrite } = await inquirer.prompt([{ - type: 'confirm', - name: 'overwrite', - message: `Directory ${path.basename(appPath)} already exists. Overwrite?`, - default: false - }]); - + const { overwrite } = await inquirer.prompt([ + { + type: "confirm", + name: "overwrite", + message: `Directory ${path.basename( + appPath + )} already exists. Overwrite?`, + default: false, + }, + ]); + if (!overwrite) { - console.log(chalk.yellow('Operation cancelled.')); + console.log(chalk.yellow("Operation cancelled.")); process.exit(0); } - + fs.removeSync(appPath); } - + fs.mkdirSync(appPath, { recursive: true }); } // Copy template files to project directory -function copyTemplateFiles(appPath: string, language: LanguageKey, template: TemplateKey): void { +function copyTemplateFiles( + appPath: string, + language: LanguageKey, + template: TemplateKey +): void { const templatePath = path.resolve( CONFIG.templateBasePath, language, template ); - + // Ensure the template exists if (!fs.existsSync(templatePath)) { throw new Error(`Template not found: ${templatePath}`); } - + fs.copySync(templatePath, appPath); } // Set up project dependencies based on language -async function setupDependencies(appPath: string, language: LanguageKey): Promise { +async function setupDependencies( + appPath: string, + language: LanguageKey +): Promise { const installCommand = CONFIG.installCommands[language]; - const spinner = ora(`Setting up ${LANGUAGES[language].name} environment...`).start(); - + const spinner = ora( + `Setting up ${LANGUAGES[language].name} environment...` + ).start(); + try { - execSync(installCommand, { cwd: appPath, stdio: 'pipe' }); - spinner.succeed(`${LANGUAGES[language].name} environment set up successfully`); + execSync(installCommand, { cwd: appPath, stdio: "pipe" }); + spinner.succeed( + `${LANGUAGES[language].name} environment set up successfully` + ); return; } catch (error) { spinner.fail(`Failed to set up ${LANGUAGES[language].name} environment`); console.error(chalk.red(`Error: ${getErrorMessage(error)}`)); - + // Provide manual instructions if (language === LANGUAGE_TYPESCRIPT) { - console.log(chalk.yellow('\nPlease install dependencies manually:')); + console.log(chalk.yellow("\nPlease install dependencies manually:")); console.log(` cd ${path.basename(appPath)}`); - console.log(' npm install'); + console.log(" npm install"); } else if (language === LANGUAGE_PYTHON) { - console.log(chalk.yellow('\nPlease install dependencies manually:')); + console.log(chalk.yellow("\nPlease install dependencies manually:")); console.log(` cd ${path.basename(appPath)}`); - console.log(' uv venv && source .venv/bin/activate && uv sync'); + console.log(" uv venv && source .venv/bin/activate && uv sync"); } } } // Print success message with next steps -function printNextSteps(appName: string, language: LanguageKey, template: TemplateKey): void { +function printNextSteps( + appName: string, + language: LanguageKey, + template: TemplateKey +): void { // Determine which sample command to show based on language and template - const deployCommand = language === LANGUAGE_TYPESCRIPT && template === TEMPLATE_SAMPLE_APP ? 'kernel deploy index.ts' - : language === LANGUAGE_TYPESCRIPT && template === TEMPLATE_STAGEHAND ? 'kernel deploy index.ts --env OPENAI_API_KEY=XXX' - : language === LANGUAGE_PYTHON && template === TEMPLATE_SAMPLE_APP ? 'kernel deploy main.py' - : language === LANGUAGE_PYTHON && template === TEMPLATE_BROWSER_USE ? 'kernel deploy main.py --env OPENAI_API_KEY=XXX' - : ''; - - - console.log(chalk.green(` + const deployCommand = + language === LANGUAGE_TYPESCRIPT && template === TEMPLATE_SAMPLE_APP + ? "kernel deploy index.ts" + : language === LANGUAGE_TYPESCRIPT && template === TEMPLATE_STAGEHAND + ? "kernel deploy index.ts --env OPENAI_API_KEY=XXX" + : language === LANGUAGE_PYTHON && template === TEMPLATE_SAMPLE_APP + ? "kernel deploy main.py" + : language === LANGUAGE_PYTHON && template === TEMPLATE_BROWSER_USE + ? "kernel deploy main.py --env OPENAI_API_KEY=XXX" + : ""; + + console.log( + chalk.green(` 🎉 Kernel app created successfully! Next steps: cd ${appName} export KERNEL_API_KEY= - ${language === LANGUAGE_PYTHON ? 'uv venv && source .venv/bin/activate && uv sync' : ''} + ${ + language === LANGUAGE_PYTHON + ? "uv venv && source .venv/bin/activate && uv sync" + : "" + } ${deployCommand} - ${INVOKE_SAMPLES[`${language}-${template}`]} - `)); + ${INVOKE_SAMPLES[language][template]} + `) + ); } // Main program const program = new Command(); program - .name('create-kernel-app') - .description('Create a new Kernel application') - .version('0.1.0') - .argument('[app-name]', 'Name of your Kernel app') - .option('-l, --language ', `Programming language (${LANGUAGE_TYPESCRIPT}/${LANGUAGE_SHORTHAND_TS}, ${LANGUAGE_PYTHON}/${LANGUAGE_SHORTHAND_PY})`) - .option('-t, --template