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:
Ajax Davis 2025-12-04 23:34:37 +10:00
parent 8b000cf0ca
commit 38c85bd904
28 changed files with 1426 additions and 2 deletions

View file

@ -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 };
}

View file

@ -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;
}