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>
This commit is contained in:
parent
c4161f1d35
commit
051e58c23d
28 changed files with 1426 additions and 2 deletions
2
apps/playground/next-env.d.ts
vendored
2
apps/playground/next-env.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
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.
|
||||
|
|
|
|||
2
apps/web/next-env.d.ts
vendored
2
apps/web/next-env.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
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.
|
||||
|
|
|
|||
8
packages/tools/create-basic-tools/.changeset/README.md
Normal file
8
packages/tools/create-basic-tools/.changeset/README.md
Normal file
|
|
@ -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)
|
||||
11
packages/tools/create-basic-tools/.changeset/config.json
Normal file
11
packages/tools/create-basic-tools/.changeset/config.json
Normal file
|
|
@ -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": []
|
||||
}
|
||||
17
packages/tools/create-basic-tools/CHANGELOG.md
Normal file
17
packages/tools/create-basic-tools/CHANGELOG.md
Normal file
|
|
@ -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/<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
|
||||
200
packages/tools/create-basic-tools/README.md
Normal file
200
packages/tools/create-basic-tools/README.md
Normal file
|
|
@ -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/<toolName>.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<typeof SummarizeTextSchema>) {
|
||||
// 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
|
||||
32
packages/tools/create-basic-tools/package.json
Normal file
32
packages/tools/create-basic-tools/package.json
Normal file
|
|
@ -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"]
|
||||
}
|
||||
180
packages/tools/create-basic-tools/src/cli.ts
Normal file
180
packages/tools/create-basic-tools/src/cli.ts
Normal file
|
|
@ -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<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,
|
||||
};
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
77
packages/tools/create-basic-tools/src/generators/readme.ts
Normal file
77
packages/tools/create-basic-tools/src/generators/readme.ts
Normal file
|
|
@ -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}
|
||||
`;
|
||||
}
|
||||
|
|
@ -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<typeof ${schemaName}>) {
|
||||
// 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}
|
||||
`;
|
||||
}
|
||||
30
packages/tools/create-basic-tools/src/index.ts
Normal file
30
packages/tools/create-basic-tools/src/index.ts
Normal file
|
|
@ -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 <name>', 'Package name (e.g., @myorg/content-tools)')
|
||||
.option('--description <description>', 'Package description')
|
||||
.option('--category <category>', 'Tool category')
|
||||
.option('--tool <tool...>', 'Tool definition (format: "exportName:description")')
|
||||
.option('--output <path>', '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();
|
||||
91
packages/tools/create-basic-tools/src/prompts/advanced.ts
Normal file
91
packages/tools/create-basic-tools/src/prompts/advanced.ts
Normal file
|
|
@ -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<string | null> {
|
||||
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<boolean> {
|
||||
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;
|
||||
}
|
||||
70
packages/tools/create-basic-tools/src/prompts/basic.ts
Normal file
70
packages/tools/create-basic-tools/src/prompts/basic.ts
Normal file
|
|
@ -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<PackageInfo | null> {
|
||||
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
|
||||
};
|
||||
}
|
||||
99
packages/tools/create-basic-tools/src/prompts/tools.ts
Normal file
99
packages/tools/create-basic-tools/src/prompts/tools.ts
Normal file
|
|
@ -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<ToolDefinition[] | null> {
|
||||
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<ToolDefinition | null> {
|
||||
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,
|
||||
};
|
||||
}
|
||||
60
packages/tools/create-basic-tools/src/types.ts
Normal file
60
packages/tools/create-basic-tools/src/types.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
60
packages/tools/create-basic-tools/src/utils/file-writer.ts
Normal file
60
packages/tools/create-basic-tools/src/utils/file-writer.ts
Normal file
|
|
@ -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<void> {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a file with the given content
|
||||
*/
|
||||
export async function writeFile(filePath: string, content: string): Promise<void> {
|
||||
await fs.writeFile(filePath, content, 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a template file to the destination
|
||||
*/
|
||||
export async function copyTemplate(
|
||||
templateName: string,
|
||||
destPath: string,
|
||||
replacements?: Record<string, string>
|
||||
): Promise<void> {
|
||||
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<boolean> {
|
||||
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);
|
||||
}
|
||||
41
packages/tools/create-basic-tools/src/utils/logger.ts
Normal file
41
packages/tools/create-basic-tools/src/utils/logger.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
node_modules
|
||||
dist
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
.DS_Store
|
||||
21
packages/tools/create-basic-tools/templates/license-mit.txt
Normal file
21
packages/tools/create-basic-tools/templates/license-mit.txt
Normal file
|
|
@ -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.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
src
|
||||
tsconfig.json
|
||||
tsup.config.ts
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
.DS_Store
|
||||
11
packages/tools/create-basic-tools/tsconfig.json
Normal file
11
packages/tools/create-basic-tools/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "@tpmjs/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"composite": false,
|
||||
"incremental": false
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
12
packages/tools/create-basic-tools/tsup.config.ts
Normal file
12
packages/tools/create-basic-tools/tsup.config.ts
Normal file
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
|
|
@ -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 (
|
||||
<Badge variant="error" size={size} className={className}>
|
||||
<Icon icon="x" size="sm" className="mr-1" />
|
||||
|
|
|
|||
64
pnpm-lock.yaml
generated
64
pnpm-lock.yaml
generated
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue