tpmjs/packages/tools/create-basic-tools/src/cli.ts
Ajax Davis 38c85bd904 feat: add @tpmjs/create-basic-tools CLI generator
- Interactive CLI generator for scaffolding TPMJS tool packages
- 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/<toolName>.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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 23:34:37 +10:00

180 lines
5.4 KiB
TypeScript

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<GenerationResult> {
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<GenerationResult> {
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,
};
}