diff --git a/apps/playground/next-env.d.ts b/apps/playground/next-env.d.ts index c4b7818..9edff1c 100644 --- a/apps/playground/next-env.d.ts +++ b/apps/playground/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index c4b7818..9edff1c 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/dev/types/routes.d.ts"; +import "./.next/types/routes.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/packages/tools/create-basic-tools/.changeset/README.md b/packages/tools/create-basic-tools/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/packages/tools/create-basic-tools/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/packages/tools/create-basic-tools/.changeset/config.json b/packages/tools/create-basic-tools/.changeset/config.json new file mode 100644 index 0000000..d88011f --- /dev/null +++ b/packages/tools/create-basic-tools/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/packages/tools/create-basic-tools/CHANGELOG.md b/packages/tools/create-basic-tools/CHANGELOG.md new file mode 100644 index 0000000..e1b8d61 --- /dev/null +++ b/packages/tools/create-basic-tools/CHANGELOG.md @@ -0,0 +1,17 @@ +# @tpmjs/create-basic-tools + +## 1.0.0 + +### Major Changes + +- Initial release of @tpmjs/create-basic-tools - CLI generator for scaffolding production-ready TPMJS tool packages + + Features: + + - Interactive CLI with beautiful prompts using @clack/prompts + - Generates packages with minimum 2 tools (ideally 2-3) + - Zod 4 schemas - uses Zod directly (not jsonSchema wrapper) + - One file per tool in src/tools/.ts + - TPMJS validated against official schemas from @tpmjs/types + - Complete package generation ready to publish to npm + - Works both standalone and in monorepo packages/ folders diff --git a/packages/tools/create-basic-tools/README.md b/packages/tools/create-basic-tools/README.md new file mode 100644 index 0000000..b172518 --- /dev/null +++ b/packages/tools/create-basic-tools/README.md @@ -0,0 +1,200 @@ +# @tpmjs/create-basic-tools + +CLI generator for scaffolding production-ready TPMJS tool packages with 2-3 tools by default. + +## Features + +- 🎯 **Multi-tool packages**: Generates packages with minimum 2 tools (ideally 2-3) +- 🔧 **Zod 4 schemas**: Uses Zod directly (not jsonSchema wrapper) +- ⚡ **AI SDK v6**: Full compatibility with the latest AI SDK +- 📦 **One file per tool**: Clean `src/tools/.ts` structure +- ✅ **TPMJS validated**: Auto-validates against official TPMJS schemas +- 🎨 **Beautiful CLI**: Interactive prompts with @clack/prompts +- 📝 **Complete setup**: Generates package.json, tsconfig, tsup config, README, and more +- 🚀 **Publish ready**: Generated packages are ready to publish to npm immediately + +## Usage + +### Interactive Mode (Recommended) + +```bash +pnpmx @tpmjs/create-basic-tools +``` + +This will guide you through an interactive wizard that asks: + +1. **Package info**: name, description, author, license +2. **Tool definitions**: At least 2 tools (export name + description) +3. **Category**: Choose from 12 TPMJS categories +4. **Mode**: Simple (basic Zod schemas) or Advanced (full control) +5. **Output path**: Where to create the package +6. **Confirmation**: Review and confirm + +### Example Session + +```bash +$ pnpmx @tpmjs/create-basic-tools + +┌ create-tpmjs-tool +│ +◇ Package name +│ @myorg/content-tools +│ +◇ Package description +│ AI SDK tools for content processing +│ +◇ Tool #1 export name +│ summarizeText +│ +◇ Tool #1 description +│ Summarize a block of text into a concise overview. +│ +◇ Tool #2 export name +│ extractKeywords +│ +◇ Tool #2 description +│ Extract important keywords from text. +│ +◇ Add tool #3? (already have 2) +│ Yes +│ +◇ Tool #3 export name +│ classifySentiment +│ +◇ Tool #3 description +│ Classify the sentiment of text as positive, negative, or neutral. +│ +◇ Category +│ text-analysis +│ +◇ Mode +│ Simple Mode - Basic Zod schemas +│ +◇ Where should we create the package? +│ ./content-tools +│ +◇ Ready to generate? +│ Yes +│ +└ Success! Created @myorg/content-tools at ./content-tools +``` + +## Generated Package Structure + +``` +content-tools/ +├── src/ +│ ├── tools/ # One file per tool +│ │ ├── summarizeText.ts +│ │ ├── extractKeywords.ts +│ │ └── classifySentiment.ts +│ └── index.ts # Re-exports all tools +├── dist/ # Build output (after pnpm build) +│ ├── index.js +│ └── index.d.ts +├── package.json # With complete tpmjs field +├── tsconfig.json +├── tsup.config.ts +├── README.md +├── .gitignore +├── .npmignore +└── LICENSE +``` + +## Generated Tool File Example + +Each tool file follows this Zod-first pattern: + +```typescript +import { tool } from 'ai'; +import { z } from 'zod'; + +const SummarizeTextSchema = z.object({ + text: z.string().min(1, 'Text cannot be empty').describe('The input text to process.'), + options: z.object({ + language: z.string().default('en').describe('Language code (e.g., en, es, fr).'), + maxLength: z.number().int().positive().default(100).describe('Maximum length of output.'), + }).default({ language: 'en', maxLength: 100 }).describe('Optional configuration.'), +}); + +export const summarizeText = tool({ + description: 'Summarize a block of text into a concise overview.', + inputSchema: SummarizeTextSchema, + async execute(input: z.infer) { + // TODO: Implement the tool logic here + console.log('summarizeText called with:', input); + + return { + success: true, + message: 'Tool executed successfully. Replace this with your implementation.', + input, + }; + }, +}); +``` + +## After Generation + +Once the package is generated: + +```bash +cd content-tools + +# Install dependencies +pnpm install + +# Build the package +pnpm build + +# Type-check +pnpm type-check + +# Publish to npm +pnpm publish --access public +``` + +Your tools will appear on [tpmjs.com](https://tpmjs.com) within 2-15 minutes after publishing! + +## TPMJS Categories + +The generator validates against these official TPMJS categories: + +- `web-scraping` +- `data-processing` +- `file-operations` +- `communication` +- `database` +- `api-integration` +- `image-processing` +- `text-analysis` +- `automation` +- `ai-ml` +- `security` +- `monitoring` + +## Requirements + +- Node.js 18+ +- pnpm (recommended) + +## Development + +This is a generator package itself. To work on it: + +```bash +# Install dependencies +pnpm install + +# Build +pnpm build + +# Type-check +pnpm type-check + +# Test locally +node dist/index.js +``` + +## License + +MIT diff --git a/packages/tools/create-basic-tools/package.json b/packages/tools/create-basic-tools/package.json new file mode 100644 index 0000000..476dff1 --- /dev/null +++ b/packages/tools/create-basic-tools/package.json @@ -0,0 +1,32 @@ +{ + "name": "@tpmjs/create-basic-tools", + "version": "1.0.0", + "description": "CLI generator for scaffolding production-ready TPMJS tool packages", + "type": "module", + "bin": { + "create-basic-tools": "./dist/index.js" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "type-check": "tsc --noEmit" + }, + "keywords": ["tpmjs", "ai-sdk", "generator", "cli", "scaffold", "boilerplate"], + "dependencies": { + "@clack/prompts": "^0.7.0", + "commander": "^14.0.0", + "picocolors": "^1.0.0", + "validate-npm-package-name": "^5.0.0", + "zod": "^4.1.13" + }, + "devDependencies": { + "@tpmjs/tsconfig": "workspace:*", + "@tpmjs/types": "workspace:*", + "@types/validate-npm-package-name": "^4.0.2", + "tsup": "^8.3.5", + "typescript": "^5.9.3" + }, + "files": ["dist", "templates", "README.md"] +} diff --git a/packages/tools/create-basic-tools/src/cli.ts b/packages/tools/create-basic-tools/src/cli.ts new file mode 100644 index 0000000..27bca3b --- /dev/null +++ b/packages/tools/create-basic-tools/src/cli.ts @@ -0,0 +1,180 @@ +import * as path from 'node:path'; +import * as clack from '@clack/prompts'; +import { generateTsConfig, generateTsupConfig } from './generators/config-files.js'; +import { generatePackageJson } from './generators/package-json.js'; +import { generateReadme } from './generators/readme.js'; +import { generateIndexFile, generateToolFile } from './generators/source-code.js'; +import { promptCategoryAndMode, promptConfirmation, promptOutputPath } from './prompts/advanced.js'; +import { promptBasicInfo } from './prompts/basic.js'; +import { promptTools } from './prompts/tools.js'; +import type { GenerationResult, GeneratorConfig } from './types.js'; +import { + copyTemplate, + ensureDir, + getAbsolutePath, + pathExists, + writeFile, +} from './utils/file-writer.js'; +import * as logger from './utils/logger.js'; + +/** + * Runs the interactive CLI workflow + */ +export async function runInteractiveCLI(): Promise { + clack.intro(logger.bold('create-tpmjs-tool')); + + // Step 1: Get basic package info + const packageInfo = await promptBasicInfo(); + if (!packageInfo) { + clack.cancel('Operation cancelled'); + process.exit(0); + } + + // Step 2: Get tool definitions (minimum 2) + const tools = await promptTools(); + if (!tools || tools.length < 2) { + clack.cancel('Operation cancelled'); + process.exit(0); + } + + // Step 3: Get category and mode + const categoryAndMode = await promptCategoryAndMode(); + + if (!categoryAndMode) { + clack.cancel('Operation cancelled'); + process.exit(0); + } + + packageInfo.category = categoryAndMode.category; + + // Step 4: Get output path + const packageNameWithoutScope = packageInfo.name.split('/').pop() || packageInfo.name; + const defaultPath = `./${packageNameWithoutScope}`; + + const outputPath = await promptOutputPath(defaultPath); + if (!outputPath) { + clack.cancel('Operation cancelled'); + process.exit(0); + } + + const absoluteOutputPath = getAbsolutePath(outputPath); + + // Check if directory exists + if (await pathExists(absoluteOutputPath)) { + clack.log.error(`Directory already exists: ${absoluteOutputPath}`); + clack.cancel('Operation cancelled'); + process.exit(1); + } + + // Step 5: Confirm generation + const confirmed = await promptConfirmation(packageInfo.name, tools.length, outputPath); + if (!confirmed) { + clack.cancel('Operation cancelled'); + process.exit(0); + } + + // Step 6: Generate the package + const spinner = clack.spinner(); + spinner.start('Generating package...'); + + const config: GeneratorConfig = { + packageInfo, + tools, + outputPath: absoluteOutputPath, + mode: categoryAndMode.mode, + }; + + try { + const result = await generatePackage(config); + + spinner.stop('Package generated successfully!'); + + clack.outro(` +${logger.green('✓')} Success! Created ${logger.bold(packageInfo.name)} at ${logger.cyan(outputPath)} + + Files created: +${result.filesCreated.map((f) => ` ${f}`).join('\n')} + + Next steps: + ${logger.cyan(`cd ${outputPath}`)} + ${logger.cyan('pnpm install')} + ${logger.cyan('pnpm build')} + ${logger.cyan('pnpm type-check')} + ${logger.cyan('pnpm publish')} +`); + + return result; + } catch (error) { + spinner.stop('Generation failed'); + clack.log.error(error instanceof Error ? error.message : 'Unknown error occurred'); + clack.cancel('Operation failed'); + process.exit(1); + } +} + +/** + * Generates the package files + */ +async function generatePackage(config: GeneratorConfig): Promise { + const { packageInfo, tools, outputPath } = config; + const filesCreated: string[] = []; + + // Create directory structure + await ensureDir(outputPath); + await ensureDir(path.join(outputPath, 'src')); + await ensureDir(path.join(outputPath, 'src', 'tools')); + + // Generate package.json + const packageJsonPath = path.join(outputPath, 'package.json'); + await writeFile(packageJsonPath, generatePackageJson(config)); + filesCreated.push('package.json'); + + // Generate tool files + for (const tool of tools) { + const toolPath = path.join(outputPath, 'src', 'tools', `${tool.exportName}.ts`); + await writeFile(toolPath, generateToolFile(tool)); + filesCreated.push(`src/tools/${tool.exportName}.ts`); + } + + // Generate index.ts + const indexPath = path.join(outputPath, 'src', 'index.ts'); + await writeFile(indexPath, generateIndexFile(tools)); + filesCreated.push('src/index.ts'); + + // Generate tsconfig.json + const tsconfigPath = path.join(outputPath, 'tsconfig.json'); + await writeFile(tsconfigPath, generateTsConfig()); + filesCreated.push('tsconfig.json'); + + // Generate tsup.config.ts + const tsupConfigPath = path.join(outputPath, 'tsup.config.ts'); + await writeFile(tsupConfigPath, generateTsupConfig()); + filesCreated.push('tsup.config.ts'); + + // Generate README.md + const readmePath = path.join(outputPath, 'README.md'); + await writeFile(readmePath, generateReadme(config)); + filesCreated.push('README.md'); + + // Copy static templates + const gitignorePath = path.join(outputPath, '.gitignore'); + await copyTemplate('gitignore.txt', gitignorePath); + filesCreated.push('.gitignore'); + + const npmignorePath = path.join(outputPath, '.npmignore'); + await copyTemplate('npmignore.txt', npmignorePath); + filesCreated.push('.npmignore'); + + const licensePath = path.join(outputPath, 'LICENSE'); + await copyTemplate('license-mit.txt', licensePath, { + YEAR: new Date().getFullYear().toString(), + AUTHOR: packageInfo.author || 'Author Name', + }); + filesCreated.push('LICENSE'); + + return { + success: true, + outputPath, + filesCreated, + }; +} diff --git a/packages/tools/create-basic-tools/src/generators/config-files.ts b/packages/tools/create-basic-tools/src/generators/config-files.ts new file mode 100644 index 0000000..577bf7c --- /dev/null +++ b/packages/tools/create-basic-tools/src/generators/config-files.ts @@ -0,0 +1,45 @@ +/** + * Generates tsconfig.json content + */ +export function generateTsConfig(): string { + const config = { + compilerOptions: { + target: 'ES2022', + module: 'ESNext', + moduleResolution: 'bundler', + lib: ['ES2022'], + outDir: './dist', + rootDir: './src', + declaration: true, + declarationMap: true, + sourceMap: true, + strict: true, + esModuleInterop: true, + skipLibCheck: true, + forceConsistentCasingInFileNames: true, + resolveJsonModule: true, + allowSyntheticDefaultImports: true, + }, + include: ['src/**/*'], + exclude: ['node_modules', 'dist'], + }; + + return JSON.stringify(config, null, 2); +} + +/** + * Generates tsup.config.ts content + */ +export function generateTsupConfig(): string { + return `import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + treeshake: true, + splitting: false, +}); +`; +} diff --git a/packages/tools/create-basic-tools/src/generators/package-json.ts b/packages/tools/create-basic-tools/src/generators/package-json.ts new file mode 100644 index 0000000..0ddbde7 --- /dev/null +++ b/packages/tools/create-basic-tools/src/generators/package-json.ts @@ -0,0 +1,52 @@ +import type { GeneratorConfig } from '../types.js'; + +/** + * Generates the package.json content for a TPMJS tool package + */ +export function generatePackageJson(config: GeneratorConfig): string { + const { packageInfo, tools } = config; + + const pkg = { + name: packageInfo.name, + version: '0.0.1', + description: packageInfo.description, + type: 'module', + main: 'dist/index.js', + types: 'dist/index.d.ts', + scripts: { + build: 'tsup', + dev: 'tsup --watch', + 'type-check': 'tsc --noEmit', + }, + keywords: ['tpmjs-tool', 'ai-sdk', packageInfo.category], + author: packageInfo.author || '', + license: packageInfo.license, + tpmjs: { + category: packageInfo.category, + tools: tools.map((tool) => ({ + exportName: tool.exportName, + description: tool.description, + ...(tool.parameters && { parameters: tool.parameters }), + ...(tool.returns && { returns: tool.returns }), + ...(tool.aiAgent && { aiAgent: tool.aiAgent }), + })), + ...(tools.some((t) => t.env) && { + env: tools.flatMap((t) => t.env || []), + }), + ...(tools.some((t) => t.frameworks) && { + frameworks: Array.from(new Set(tools.flatMap((t) => t.frameworks || []))), + }), + }, + dependencies: { + ai: '^6.0.0', + zod: '^4.1.13', + }, + devDependencies: { + tsup: '^8.3.5', + typescript: '^5.9.3', + }, + files: ['dist', 'README.md'], + }; + + return JSON.stringify(pkg, null, 2); +} diff --git a/packages/tools/create-basic-tools/src/generators/readme.ts b/packages/tools/create-basic-tools/src/generators/readme.ts new file mode 100644 index 0000000..f9ac3fc --- /dev/null +++ b/packages/tools/create-basic-tools/src/generators/readme.ts @@ -0,0 +1,77 @@ +import type { GeneratorConfig } from '../types.js'; + +/** + * Generates README.md content for the tool package + */ +export function generateReadme(config: GeneratorConfig): string { + const { packageInfo, tools } = config; + + const toolsList = tools.map((tool) => `- **${tool.exportName}**: ${tool.description}`).join('\n'); + + const usageExample = tools[0]; + if (!usageExample) { + throw new Error('At least one tool is required'); + } + + return `# ${packageInfo.name} + +${packageInfo.description} + +## Installation + +\`\`\`bash +pnpm add ${packageInfo.name} +\`\`\` + +## Tools + +This package provides ${tools.length} tool${tools.length > 1 ? 's' : ''} for the AI SDK: + +${toolsList} + +## Usage + +\`\`\`typescript +import { ${usageExample.exportName} } from '${packageInfo.name}'; +import { generateText } from 'ai'; +import { openai } from '@ai-sdk/openai'; + +const result = await generateText({ + model: openai('gpt-4'), + prompt: 'Process this text for me', + tools: { + ${usageExample.exportName}, + }, +}); + +console.log(result.text); +\`\`\` + +## Development + +\`\`\`bash +# Install dependencies +pnpm install + +# Build the package +pnpm build + +# Type-check +pnpm type-check + +# Watch mode +pnpm dev +\`\`\` + +## Publishing + +1. Update the version in \`package.json\` +2. Build the package: \`pnpm build\` +3. Publish to npm: \`pnpm publish --access public\` +4. Your tools will appear on [tpmjs.com](https://tpmjs.com) within 2-15 minutes + +## License + +${packageInfo.license} +`; +} diff --git a/packages/tools/create-basic-tools/src/generators/source-code.ts b/packages/tools/create-basic-tools/src/generators/source-code.ts new file mode 100644 index 0000000..7f5d556 --- /dev/null +++ b/packages/tools/create-basic-tools/src/generators/source-code.ts @@ -0,0 +1,65 @@ +import type { ToolDefinition } from '../types.js'; + +/** + * Generates a single tool file with Zod schema + */ +export function generateToolFile(tool: ToolDefinition): string { + const { exportName, description } = tool; + + // Generate schema name (capitalize first letter + "Schema") + const schemaName = `${exportName.charAt(0).toUpperCase()}${exportName.slice(1)}Schema`; + + // Generate simple Zod schema (can be enhanced in advanced mode) + const schemaContent = generateSimpleSchema(); + + return `import { tool } from 'ai'; +import { z } from 'zod'; + +const ${schemaName} = z.object({ +${schemaContent} +}); + +export const ${exportName} = tool({ + description: '${description}', + inputSchema: ${schemaName}, + async execute(input: z.infer) { + // TODO: Implement the tool logic here + console.log('${exportName} called with:', input); + + return { + success: true, + message: 'Tool executed successfully. Replace this with your implementation.', + input, + }; + }, +}); +`; +} + +/** + * Generates a simple Zod schema with example fields + */ +function generateSimpleSchema(): string { + return ` text: z.string().min(1, 'Text cannot be empty').describe('The input text to process.'), + options: z.object({ + language: z.string().default('en').describe('Language code (e.g., en, es, fr).'), + maxLength: z.number().int().positive().default(100).describe('Maximum length of output.'), + }).default({ language: 'en', maxLength: 100 }).describe('Optional configuration.'),`; +} + +/** + * Generates the main index.ts file that re-exports all tools + */ +export function generateIndexFile(tools: ToolDefinition[]): string { + const exports = tools + .map((tool) => `export { ${tool.exportName} } from './tools/${tool.exportName}.js';`) + .join('\n'); + + return `/** + * TPMJS Tool Package + * Auto-generated by @tpmjs/create-basic-tools + */ + +${exports} +`; +} diff --git a/packages/tools/create-basic-tools/src/index.ts b/packages/tools/create-basic-tools/src/index.ts new file mode 100644 index 0000000..1289219 --- /dev/null +++ b/packages/tools/create-basic-tools/src/index.ts @@ -0,0 +1,30 @@ +#!/usr/bin/env node + +import { Command } from 'commander'; +import { runInteractiveCLI } from './cli.js'; + +const program = new Command(); + +program + .name('create-basic-tools') + .description('CLI generator for scaffolding production-ready TPMJS tool packages') + .version('0.0.1') + .option('--name ', 'Package name (e.g., @myorg/content-tools)') + .option('--description ', 'Package description') + .option('--category ', 'Tool category') + .option('--tool ', 'Tool definition (format: "exportName:description")') + .option('--output ', 'Output path') + .option('--yes', 'Skip confirmation prompt') + .action(async (options) => { + // For now, only support interactive mode + // CLI flags mode can be added later + if (options.name || options.description || options.category || options.tool) { + console.log('CLI flags mode is not yet implemented.'); + console.log('Please run without flags for interactive mode.'); + process.exit(1); + } + + await runInteractiveCLI(); + }); + +program.parse(); diff --git a/packages/tools/create-basic-tools/src/prompts/advanced.ts b/packages/tools/create-basic-tools/src/prompts/advanced.ts new file mode 100644 index 0000000..d44beff --- /dev/null +++ b/packages/tools/create-basic-tools/src/prompts/advanced.ts @@ -0,0 +1,91 @@ +import * as clack from '@clack/prompts'; +import { getAvailableCategories } from '../validation/tool-metadata.js'; + +/** + * Prompts for category and mode selection + */ +export async function promptCategoryAndMode(): Promise<{ + category: string; + mode: 'simple' | 'advanced'; +} | null> { + const categories = getAvailableCategories(); + + const category = await clack.select({ + message: 'Tool category', + options: categories.map((cat) => ({ + value: cat, + label: cat, + })), + }); + + if (clack.isCancel(category)) { + return null; + } + + const modeResult = await clack.select({ + message: 'Generation mode', + options: [ + { + value: 'simple', + label: 'Simple Mode - Basic Zod schemas (recommended)', + hint: 'Generates simple string/number parameters with defaults', + }, + { + value: 'advanced', + label: 'Advanced Mode - Full control', + hint: 'Configure parameters, returns, env vars, and AI agent guidance', + }, + ], + }); + + if (clack.isCancel(modeResult)) { + return null; + } + + const mode = modeResult as 'simple' | 'advanced'; + + return { + category: category as string, + mode, + }; +} + +/** + * Prompts for output path + */ +export async function promptOutputPath(defaultPath: string): Promise { + const outputPath = await clack.text({ + message: 'Where should we create the package?', + placeholder: defaultPath, + initialValue: defaultPath, + validate: (value) => { + if (!value) return 'Output path is required'; + }, + }); + + if (clack.isCancel(outputPath)) { + return null; + } + + return outputPath as string; +} + +/** + * Prompts for final confirmation + */ +export async function promptConfirmation( + packageName: string, + toolCount: number, + outputPath: string +): Promise { + const confirmed = await clack.confirm({ + message: `Ready to generate ${packageName} with ${toolCount} tools at ${outputPath}?`, + initialValue: true, + }); + + if (clack.isCancel(confirmed)) { + return false; + } + + return confirmed; +} diff --git a/packages/tools/create-basic-tools/src/prompts/basic.ts b/packages/tools/create-basic-tools/src/prompts/basic.ts new file mode 100644 index 0000000..0e00b5b --- /dev/null +++ b/packages/tools/create-basic-tools/src/prompts/basic.ts @@ -0,0 +1,70 @@ +import * as clack from '@clack/prompts'; +import type { PackageInfo } from '../types.js'; +import { validateNpmPackageName } from '../validation/package-name.js'; + +/** + * Prompts for basic package information + */ +export async function promptBasicInfo(): Promise { + const name = await clack.text({ + message: 'Package name', + placeholder: '@myorg/content-tools', + validate: (value) => { + if (!value) return 'Package name is required'; + const validation = validateNpmPackageName(value); + if (!validation.valid) { + return validation.errors?.[0] || 'Invalid package name'; + } + }, + }); + + if (clack.isCancel(name)) { + return null; + } + + const description = await clack.text({ + message: 'Package description', + placeholder: 'AI SDK tools for content processing', + validate: (value) => { + if (!value) return 'Description is required'; + if (value.length < 10) return 'Description must be at least 10 characters'; + }, + }); + + if (clack.isCancel(description)) { + return null; + } + + const author = await clack.text({ + message: 'Author name', + placeholder: 'Your Name', + initialValue: '', + }); + + if (clack.isCancel(author)) { + return null; + } + + const license = await clack.select({ + message: 'License', + options: [ + { value: 'MIT', label: 'MIT' }, + { value: 'Apache-2.0', label: 'Apache-2.0' }, + { value: 'ISC', label: 'ISC' }, + { value: 'BSD-3-Clause', label: 'BSD-3-Clause' }, + ], + initialValue: 'MIT', + }); + + if (clack.isCancel(license)) { + return null; + } + + return { + name: name as string, + description: description as string, + author: author as string, + license: license as string, + category: '', // Will be set later + }; +} diff --git a/packages/tools/create-basic-tools/src/prompts/tools.ts b/packages/tools/create-basic-tools/src/prompts/tools.ts new file mode 100644 index 0000000..7e00da0 --- /dev/null +++ b/packages/tools/create-basic-tools/src/prompts/tools.ts @@ -0,0 +1,99 @@ +import * as clack from '@clack/prompts'; +import type { ToolDefinition } from '../types.js'; +import { validateExportName } from '../validation/package-name.js'; +import { validateDescription } from '../validation/tool-metadata.js'; + +/** + * Prompts for tool definitions (minimum 2 tools required) + */ +export async function promptTools(): Promise { + const tools: ToolDefinition[] = []; + + clack.note( + 'Define at least 2 tools for your package.\nExamples: summarizeText, extractKeywords, classifySentiment', + 'Tool Definitions' + ); + + // First tool (required) + const tool1 = await promptSingleTool(1); + if (!tool1) return null; + tools.push(tool1); + + // Second tool (required) + const tool2 = await promptSingleTool(2); + if (!tool2) return null; + tools.push(tool2); + + // Additional tools (optional) + let continueAdding = true; + let toolNumber = 3; + + while (continueAdding) { + const addMore = await clack.confirm({ + message: `Add tool #${toolNumber}? (already have ${tools.length})`, + initialValue: toolNumber === 3, // Default yes for the 3rd tool + }); + + if (clack.isCancel(addMore)) { + return null; + } + + if (!addMore) { + continueAdding = false; + } else { + const tool = await promptSingleTool(toolNumber); + if (!tool) return null; + tools.push(tool); + toolNumber++; + } + } + + return tools; +} + +/** + * Prompts for a single tool definition + */ +async function promptSingleTool(number: number): Promise { + const exportName = await clack.text({ + message: `Tool #${number} export name`, + placeholder: number === 1 ? 'summarizeText' : number === 2 ? 'extractKeywords' : 'myTool', + validate: (value) => { + if (!value) return 'Export name is required'; + const validation = validateExportName(value as string); + if (!validation.valid) { + return validation.error || 'Invalid export name'; + } + }, + }); + + if (clack.isCancel(exportName)) { + return null; + } + + const description = await clack.text({ + message: `Tool #${number} description`, + placeholder: + number === 1 + ? 'Summarize a block of text into a concise overview.' + : number === 2 + ? 'Extract important keywords from text.' + : 'Description of what this tool does.', + validate: (value) => { + if (!value) return 'Description is required'; + const validation = validateDescription(value as string); + if (!validation.valid) { + return validation.error || 'Invalid description'; + } + }, + }); + + if (clack.isCancel(description)) { + return null; + } + + return { + exportName: exportName as string, + description: description as string, + }; +} diff --git a/packages/tools/create-basic-tools/src/types.ts b/packages/tools/create-basic-tools/src/types.ts new file mode 100644 index 0000000..c414762 --- /dev/null +++ b/packages/tools/create-basic-tools/src/types.ts @@ -0,0 +1,60 @@ +/** + * Shared TypeScript types for the TPMJS tool generator + */ + +export interface ToolDefinition { + exportName: string; + description: string; + parameters?: ParameterDefinition[]; + returns?: ReturnDefinition; + env?: string[]; + frameworks?: string[]; + aiAgent?: { + systemPrompt?: string; + model?: string; + }; +} + +export interface ParameterDefinition { + name: string; + type: 'string' | 'number' | 'boolean' | 'object' | 'array'; + description: string; + required: boolean; + default?: unknown; +} + +export interface ReturnDefinition { + type: string; + description: string; +} + +export interface PackageInfo { + name: string; + description: string; + author: string; + license: string; + category: string; +} + +export interface GeneratorConfig { + packageInfo: PackageInfo; + tools: ToolDefinition[]; + outputPath: string; + mode: 'simple' | 'advanced'; +} + +export interface CLIOptions { + name?: string; + description?: string; + category?: string; + tool?: string[]; + output?: string; + yes?: boolean; +} + +export interface GenerationResult { + success: boolean; + outputPath: string; + filesCreated: string[]; + error?: string; +} diff --git a/packages/tools/create-basic-tools/src/utils/file-writer.ts b/packages/tools/create-basic-tools/src/utils/file-writer.ts new file mode 100644 index 0000000..f85a8da --- /dev/null +++ b/packages/tools/create-basic-tools/src/utils/file-writer.ts @@ -0,0 +1,60 @@ +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * Ensures a directory exists, creating it if necessary + */ +export async function ensureDir(dirPath: string): Promise { + await fs.mkdir(dirPath, { recursive: true }); +} + +/** + * Writes a file with the given content + */ +export async function writeFile(filePath: string, content: string): Promise { + await fs.writeFile(filePath, content, 'utf-8'); +} + +/** + * Copies a template file to the destination + */ +export async function copyTemplate( + templateName: string, + destPath: string, + replacements?: Record +): Promise { + const templatePath = path.join(__dirname, '../../templates', templateName); + let content = await fs.readFile(templatePath, 'utf-8'); + + // Apply replacements if provided + if (replacements) { + for (const [key, value] of Object.entries(replacements)) { + content = content.replaceAll(`{{${key}}}`, value); + } + } + + await writeFile(destPath, content); +} + +/** + * Checks if a path exists + */ +export async function pathExists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +/** + * Gets the absolute path, resolving relative paths + */ +export function getAbsolutePath(inputPath: string): string { + return path.resolve(process.cwd(), inputPath); +} diff --git a/packages/tools/create-basic-tools/src/utils/logger.ts b/packages/tools/create-basic-tools/src/utils/logger.ts new file mode 100644 index 0000000..388a228 --- /dev/null +++ b/packages/tools/create-basic-tools/src/utils/logger.ts @@ -0,0 +1,41 @@ +import pc from 'picocolors'; + +/** + * Logger utilities with colored output + */ + +export function success(message: string): void { + console.log(pc.green(`✓ ${message}`)); +} + +export function error(message: string): void { + console.error(pc.red(`✗ ${message}`)); +} + +export function warn(message: string): void { + console.warn(pc.yellow(`⚠ ${message}`)); +} + +export function info(message: string): void { + console.log(pc.blue(`ℹ ${message}`)); +} + +export function dim(message: string): void { + console.log(pc.dim(message)); +} + +export function bold(message: string): string { + return pc.bold(message); +} + +export function cyan(message: string): string { + return pc.cyan(message); +} + +export function yellow(message: string): string { + return pc.yellow(message); +} + +export function green(message: string): string { + return pc.green(message); +} diff --git a/packages/tools/create-basic-tools/src/validation/package-name.ts b/packages/tools/create-basic-tools/src/validation/package-name.ts new file mode 100644 index 0000000..b73fb45 --- /dev/null +++ b/packages/tools/create-basic-tools/src/validation/package-name.ts @@ -0,0 +1,89 @@ +import validatePackageName from 'validate-npm-package-name'; + +export interface PackageNameValidation { + valid: boolean; + errors?: string[]; + warnings?: string[]; +} + +/** + * Validates an npm package name + */ +export function validateNpmPackageName(name: string): PackageNameValidation { + const result = validatePackageName(name); + + if (result.validForNewPackages) { + return { + valid: true, + warnings: result.warnings, + }; + } + + return { + valid: false, + errors: result.errors, + warnings: result.warnings, + }; +} + +/** + * Validates that export name is a valid JavaScript identifier + */ +export function validateExportName(name: string): { valid: boolean; error?: string } { + if (!name || name.length === 0) { + return { valid: false, error: 'Export name cannot be empty' }; + } + + // JavaScript identifier regex: must start with letter, $, or _, followed by letters, digits, $, or _ + const identifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; + + if (!identifierRegex.test(name)) { + return { + valid: false, + error: 'Export name must be a valid JavaScript identifier (e.g., myTool, summarizeText)', + }; + } + + // Check for reserved words + const reservedWords = [ + 'break', + 'case', + 'catch', + 'class', + 'const', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'export', + 'extends', + 'finally', + 'for', + 'function', + 'if', + 'import', + 'in', + 'instanceof', + 'new', + 'return', + 'super', + 'switch', + 'this', + 'throw', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', + 'yield', + ]; + + if (reservedWords.includes(name)) { + return { valid: false, error: `"${name}" is a reserved JavaScript keyword` }; + } + + return { valid: true }; +} diff --git a/packages/tools/create-basic-tools/src/validation/tool-metadata.ts b/packages/tools/create-basic-tools/src/validation/tool-metadata.ts new file mode 100644 index 0000000..098569d --- /dev/null +++ b/packages/tools/create-basic-tools/src/validation/tool-metadata.ts @@ -0,0 +1,72 @@ +import { TPMJS_CATEGORIES, TpmjsToolDefinitionSchema } from '@tpmjs/types/tpmjs'; +import type { TpmjsCategory } from '@tpmjs/types/tpmjs'; +import type { ToolDefinition } from '../types.js'; + +/** + * Validates a tool definition against TPMJS schema + */ +export function validateToolDefinition(tool: ToolDefinition): { + valid: boolean; + errors?: string[]; +} { + const result = TpmjsToolDefinitionSchema.safeParse(tool); + + if (result.success) { + return { valid: true }; + } + + const errors = result.error.issues.map((issue) => { + const path = issue.path.join('.'); + return `${path}: ${issue.message}`; + }); + + return { valid: false, errors }; +} + +/** + * Validates that category is one of the allowed TPMJS categories + */ +export function validateCategory(category: string): { + valid: boolean; + error?: string; +} { + if (!TPMJS_CATEGORIES.includes(category as TpmjsCategory)) { + return { + valid: false, + error: `Category must be one of: ${TPMJS_CATEGORIES.join(', ')}`, + }; + } + + return { valid: true }; +} + +/** + * Validates tool description length + */ +export function validateDescription(description: string): { + valid: boolean; + error?: string; +} { + if (description.length < 20) { + return { + valid: false, + error: 'Description must be at least 20 characters', + }; + } + + if (description.length > 500) { + return { + valid: false, + error: 'Description must be at most 500 characters', + }; + } + + return { valid: true }; +} + +/** + * Get all available TPMJS categories + */ +export function getAvailableCategories(): readonly TpmjsCategory[] { + return TPMJS_CATEGORIES; +} diff --git a/packages/tools/create-basic-tools/templates/gitignore.txt b/packages/tools/create-basic-tools/templates/gitignore.txt new file mode 100644 index 0000000..3a5c1d0 --- /dev/null +++ b/packages/tools/create-basic-tools/templates/gitignore.txt @@ -0,0 +1,6 @@ +node_modules +dist +.env +.env.local +*.log +.DS_Store diff --git a/packages/tools/create-basic-tools/templates/license-mit.txt b/packages/tools/create-basic-tools/templates/license-mit.txt new file mode 100644 index 0000000..7e7f03f --- /dev/null +++ b/packages/tools/create-basic-tools/templates/license-mit.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) {{YEAR}} {{AUTHOR}} + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/tools/create-basic-tools/templates/npmignore.txt b/packages/tools/create-basic-tools/templates/npmignore.txt new file mode 100644 index 0000000..bd6dabf --- /dev/null +++ b/packages/tools/create-basic-tools/templates/npmignore.txt @@ -0,0 +1,7 @@ +src +tsconfig.json +tsup.config.ts +.env +.env.local +*.log +.DS_Store diff --git a/packages/tools/create-basic-tools/tsconfig.json b/packages/tools/create-basic-tools/tsconfig.json new file mode 100644 index 0000000..8934438 --- /dev/null +++ b/packages/tools/create-basic-tools/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@tpmjs/tsconfig/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "composite": false, + "incremental": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/tools/create-basic-tools/tsup.config.ts b/packages/tools/create-basic-tools/tsup.config.ts new file mode 100644 index 0000000..0015f1e --- /dev/null +++ b/packages/tools/create-basic-tools/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + shims: true, + banner: { + js: '#!/usr/bin/env node', + }, +}); diff --git a/packages/ui/src/ToolHealthBadge/ToolHealthBadge.tsx b/packages/ui/src/ToolHealthBadge/ToolHealthBadge.tsx index d9e3927..9e00517 100644 --- a/packages/ui/src/ToolHealthBadge/ToolHealthBadge.tsx +++ b/packages/ui/src/ToolHealthBadge/ToolHealthBadge.tsx @@ -22,10 +22,14 @@ export function ToolHealthBadge({ }: ToolHealthBadgeProps): React.ReactElement | null { const isBroken = importHealth === 'BROKEN' || executionHealth === 'BROKEN'; + console.log('🏥 [ToolHealthBadge] Render:', { importHealth, executionHealth, isBroken }); + if (!isBroken) { return null; } + console.log('🚨 [ToolHealthBadge] Showing BROKEN badge!'); + return ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62b6a34..fe8c06e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -502,6 +502,40 @@ importers: specifier: ^5.9.3 version: 5.9.3 + packages/tools/create-basic-tools: + dependencies: + '@clack/prompts': + specifier: ^0.7.0 + version: 0.7.0 + commander: + specifier: ^14.0.0 + version: 14.0.2 + picocolors: + specifier: ^1.0.0 + version: 1.1.1 + validate-npm-package-name: + specifier: ^5.0.0 + version: 5.0.1 + zod: + specifier: ^4.1.13 + version: 4.1.13 + devDependencies: + '@tpmjs/tsconfig': + specifier: workspace:* + version: link:../../config/tsconfig + '@tpmjs/types': + specifier: workspace:* + version: link:../../types + '@types/validate-npm-package-name': + specifier: ^4.0.2 + version: 4.0.2 + tsup: + specifier: ^8.3.5 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + packages/tools/createBlogPost: dependencies: zod: @@ -947,6 +981,14 @@ packages: '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@clack/core@0.3.5': + resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} + + '@clack/prompts@0.7.0': + resolution: {integrity: sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==} + bundledDependencies: + - is-unicode-supported + '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -2479,6 +2521,9 @@ packages: '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/validate-npm-package-name@4.0.2': + resolution: {integrity: sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==} + '@typescript-eslint/eslint-plugin@8.48.0': resolution: {integrity: sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -5973,6 +6018,10 @@ packages: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -6647,6 +6696,17 @@ snapshots: '@chevrotain/utils@11.0.3': {} + '@clack/core@0.3.5': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.7.0': + dependencies: + '@clack/core': 0.3.5 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@emnapi/core@1.7.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -7941,6 +8001,8 @@ snapshots: '@types/uuid@9.0.8': {} + '@types/validate-npm-package-name@4.0.2': {} + '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -12366,6 +12428,8 @@ snapshots: uuid@9.0.1: {} + validate-npm-package-name@5.0.1: {} + vary@1.1.2: {} vfile-location@5.0.3: