- Create comprehensive CLI with oclif framework - Add commands: auth, tool, agent, collection, mcp, publish - Add doctor, playground, and update commands - Add /cli/auth page for browser-based OAuth flow - Publish to npm as @tpmjs/cli@0.1.2
376 lines
9.2 KiB
TypeScript
376 lines
9.2 KiB
TypeScript
import { Args, Command, Flags } from '@oclif/core';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import * as readline from 'node:readline';
|
|
import { createOutput } from '../../lib/output.js';
|
|
|
|
const CATEGORIES = [
|
|
'research',
|
|
'web',
|
|
'data',
|
|
'documentation',
|
|
'engineering',
|
|
'security',
|
|
'statistics',
|
|
'ops',
|
|
'agent',
|
|
'sandbox',
|
|
'utilities',
|
|
'html',
|
|
'compliance',
|
|
];
|
|
|
|
export default class ToolInit extends Command {
|
|
static description = 'Initialize a new TPMJS tool package';
|
|
|
|
static examples = [
|
|
'<%= config.bin %> <%= command.id %>',
|
|
'<%= config.bin %> <%= command.id %> my-tool',
|
|
'<%= config.bin %> <%= command.id %> --template minimal',
|
|
];
|
|
|
|
static flags = {
|
|
template: Flags.string({
|
|
char: 't',
|
|
description: 'Template to use',
|
|
options: ['minimal', 'rich'],
|
|
default: 'minimal',
|
|
}),
|
|
category: Flags.string({
|
|
char: 'c',
|
|
description: 'Tool category',
|
|
options: CATEGORIES,
|
|
}),
|
|
force: Flags.boolean({
|
|
char: 'f',
|
|
description: 'Overwrite existing files',
|
|
default: false,
|
|
}),
|
|
yes: Flags.boolean({
|
|
char: 'y',
|
|
description: 'Skip prompts and use defaults',
|
|
default: false,
|
|
}),
|
|
verbose: Flags.boolean({
|
|
char: 'v',
|
|
description: 'Show verbose output',
|
|
default: false,
|
|
}),
|
|
};
|
|
|
|
static args = {
|
|
name: Args.string({
|
|
description: 'Tool name (creates directory if not exists)',
|
|
required: false,
|
|
}),
|
|
};
|
|
|
|
private rl?: readline.Interface;
|
|
|
|
async run(): Promise<void> {
|
|
const { args, flags } = await this.parse(ToolInit);
|
|
const output = createOutput(flags);
|
|
|
|
this.rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout,
|
|
});
|
|
|
|
try {
|
|
// Gather configuration
|
|
const config = await this.gatherConfig(args.name as string | undefined, flags, output);
|
|
|
|
// Determine target directory
|
|
const targetDir = config.name ? path.resolve(config.name) : process.cwd();
|
|
|
|
// Check if files exist
|
|
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
if (fs.existsSync(packageJsonPath) && !flags.force) {
|
|
output.error('package.json already exists. Use --force to overwrite.');
|
|
return;
|
|
}
|
|
|
|
// Create directory if needed
|
|
if (!fs.existsSync(targetDir)) {
|
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
}
|
|
|
|
// Generate files
|
|
await this.generateFiles(targetDir, config, flags.template, output);
|
|
|
|
output.success('TPMJS tool initialized successfully!');
|
|
output.text('');
|
|
output.text('Next steps:');
|
|
output.listItem('Install dependencies: npm install');
|
|
output.listItem('Implement your tool in src/index.ts');
|
|
output.listItem('Validate: tpm tool validate');
|
|
output.listItem('Build: npm run build');
|
|
output.listItem('Publish: npm publish');
|
|
} finally {
|
|
this.rl?.close();
|
|
}
|
|
}
|
|
|
|
private async gatherConfig(
|
|
name: string | undefined,
|
|
flags: { category?: string; yes: boolean },
|
|
output: ReturnType<typeof createOutput>
|
|
): Promise<{
|
|
name: string;
|
|
description: string;
|
|
category: string;
|
|
author: string;
|
|
}> {
|
|
if (flags.yes) {
|
|
return {
|
|
name: name || 'my-tpmjs-tool',
|
|
description: 'A TPMJS tool',
|
|
category: flags.category || 'utilities',
|
|
author: process.env.USER || 'unknown',
|
|
};
|
|
}
|
|
|
|
output.heading('TPMJS Tool Initialization');
|
|
output.text('');
|
|
|
|
const toolName = name || (await this.prompt('Tool name: ')) || 'my-tpmjs-tool';
|
|
const description = (await this.prompt('Description: ')) || 'A TPMJS tool';
|
|
|
|
let category = flags.category;
|
|
if (!category) {
|
|
output.text('');
|
|
output.text('Available categories:');
|
|
CATEGORIES.forEach((cat, i) => {
|
|
output.text(` ${i + 1}. ${cat}`);
|
|
});
|
|
const catIndex = await this.prompt('Category (number or name): ');
|
|
const index = parseInt(catIndex, 10);
|
|
if (index > 0 && index <= CATEGORIES.length) {
|
|
category = CATEGORIES[index - 1];
|
|
} else if (CATEGORIES.includes(catIndex)) {
|
|
category = catIndex;
|
|
} else {
|
|
category = 'utilities';
|
|
}
|
|
}
|
|
|
|
const author = (await this.prompt(`Author (${process.env.USER}): `)) || process.env.USER || 'unknown';
|
|
|
|
return { name: toolName, description, category: category ?? 'utilities', author };
|
|
}
|
|
|
|
private prompt(question: string): Promise<string> {
|
|
return new Promise((resolve) => {
|
|
this.rl?.question(question, (answer) => {
|
|
resolve(answer.trim());
|
|
});
|
|
});
|
|
}
|
|
|
|
private async generateFiles(
|
|
targetDir: string,
|
|
config: { name: string; description: string; category: string; author: string },
|
|
template: string,
|
|
output: ReturnType<typeof createOutput>
|
|
): Promise<void> {
|
|
const spinner = output.spinner('Generating files...');
|
|
|
|
// package.json
|
|
const packageJson = {
|
|
name: config.name.startsWith('@') ? config.name : config.name,
|
|
version: '0.1.0',
|
|
description: config.description,
|
|
type: 'module',
|
|
main: './dist/index.js',
|
|
types: './dist/index.d.ts',
|
|
exports: {
|
|
'.': {
|
|
import: './dist/index.js',
|
|
types: './dist/index.d.ts',
|
|
},
|
|
},
|
|
files: ['dist'],
|
|
keywords: ['tpmjs', 'mcp', 'ai-tools', config.category],
|
|
author: config.author,
|
|
license: 'MIT',
|
|
scripts: {
|
|
build: 'tsup',
|
|
dev: 'tsup --watch',
|
|
'type-check': 'tsc --noEmit',
|
|
prepublishOnly: 'npm run build',
|
|
},
|
|
dependencies: {
|
|
ai: '^4.0.0',
|
|
},
|
|
devDependencies: {
|
|
'@types/node': '^20.0.0',
|
|
tsup: '^8.0.0',
|
|
typescript: '^5.0.0',
|
|
},
|
|
tpmjs: {
|
|
name: config.name.replace(/@[^/]+\//, '').replace(/-/g, ' '),
|
|
description: config.description,
|
|
category: config.category,
|
|
tools: [
|
|
{
|
|
name: 'myTool',
|
|
description: 'Description of what this tool does',
|
|
},
|
|
],
|
|
...(template === 'rich'
|
|
? {
|
|
documentation: 'https://github.com/yourname/' + config.name + '#readme',
|
|
examples: [
|
|
{
|
|
title: 'Basic usage',
|
|
code: 'const result = await myTool.execute({ input: "hello" });',
|
|
},
|
|
],
|
|
}
|
|
: {}),
|
|
},
|
|
};
|
|
|
|
fs.writeFileSync(
|
|
path.join(targetDir, 'package.json'),
|
|
JSON.stringify(packageJson, null, 2)
|
|
);
|
|
|
|
// tsconfig.json
|
|
const tsconfig = {
|
|
compilerOptions: {
|
|
target: 'ES2022',
|
|
module: 'ESNext',
|
|
moduleResolution: 'bundler',
|
|
declaration: true,
|
|
declarationMap: true,
|
|
strict: true,
|
|
esModuleInterop: true,
|
|
skipLibCheck: true,
|
|
outDir: './dist',
|
|
rootDir: './src',
|
|
},
|
|
include: ['src'],
|
|
exclude: ['node_modules', 'dist'],
|
|
};
|
|
|
|
fs.writeFileSync(
|
|
path.join(targetDir, 'tsconfig.json'),
|
|
JSON.stringify(tsconfig, null, 2)
|
|
);
|
|
|
|
// tsup.config.ts
|
|
const tsupConfig = `import { defineConfig } from 'tsup';
|
|
|
|
export default defineConfig({
|
|
entry: ['src/index.ts'],
|
|
format: ['esm'],
|
|
dts: true,
|
|
clean: true,
|
|
sourcemap: true,
|
|
});
|
|
`;
|
|
|
|
fs.writeFileSync(path.join(targetDir, 'tsup.config.ts'), tsupConfig);
|
|
|
|
// Create src directory
|
|
const srcDir = path.join(targetDir, 'src');
|
|
if (!fs.existsSync(srcDir)) {
|
|
fs.mkdirSync(srcDir, { recursive: true });
|
|
}
|
|
|
|
// src/index.ts
|
|
const indexTs = `import { jsonSchema, tool } from 'ai';
|
|
|
|
/**
|
|
* ${config.description}
|
|
*/
|
|
export const myTool = tool({
|
|
description: '${config.description}',
|
|
parameters: jsonSchema<{ input: string }>({
|
|
type: 'object',
|
|
properties: {
|
|
input: {
|
|
type: 'string',
|
|
description: 'The input to process',
|
|
},
|
|
},
|
|
required: ['input'],
|
|
}),
|
|
async execute({ input }) {
|
|
// TODO: Implement your tool logic here
|
|
return {
|
|
result: \`Processed: \${input}\`,
|
|
};
|
|
},
|
|
});
|
|
|
|
export default myTool;
|
|
`;
|
|
|
|
fs.writeFileSync(path.join(srcDir, 'index.ts'), indexTs);
|
|
|
|
// block.ts (required for TPMJS validation)
|
|
const blockTs = `import { myTool } from './src/index.js';
|
|
|
|
export const block = {
|
|
name: '${config.name}',
|
|
tools: { myTool },
|
|
};
|
|
`;
|
|
|
|
fs.writeFileSync(path.join(targetDir, 'block.ts'), blockTs);
|
|
|
|
// README.md
|
|
const readme = `# ${config.name}
|
|
|
|
${config.description}
|
|
|
|
## Installation
|
|
|
|
\`\`\`bash
|
|
npm install ${config.name}
|
|
\`\`\`
|
|
|
|
## Usage
|
|
|
|
\`\`\`typescript
|
|
import { myTool } from '${config.name}';
|
|
|
|
const result = await myTool.execute({ input: 'hello' });
|
|
console.log(result);
|
|
\`\`\`
|
|
|
|
## Category
|
|
|
|
${config.category}
|
|
|
|
## License
|
|
|
|
MIT
|
|
`;
|
|
|
|
fs.writeFileSync(path.join(targetDir, 'README.md'), readme);
|
|
|
|
// .gitignore
|
|
const gitignore = `node_modules/
|
|
dist/
|
|
*.log
|
|
.DS_Store
|
|
`;
|
|
|
|
fs.writeFileSync(path.join(targetDir, '.gitignore'), gitignore);
|
|
|
|
spinner.stop();
|
|
|
|
output.text('');
|
|
output.text('Files created:');
|
|
output.listItem('package.json');
|
|
output.listItem('tsconfig.json');
|
|
output.listItem('tsup.config.ts');
|
|
output.listItem('src/index.ts');
|
|
output.listItem('block.ts');
|
|
output.listItem('README.md');
|
|
output.listItem('.gitignore');
|
|
}
|
|
}
|