Package Architecture
This guide explains how to create new packages for AI Scaffolding and how the package architecture works.
Package Structure
Each package in AI Scaffolding follows a consistent structure:
Directory Layout
packages/<package-name>/
├── files/ # Template files for the package
│ ├── contracts/ # Package-specific content
│ ├── src/ # Package-specific content
│ └── ...
├── prompt.ts # Interactive prompts for the package
└── builder.ts # Logic for generating the package filesCreating a New Package
To create a new package for AI Scaffolding, you need to implement the following components:
1. Create Prompt Module
First, create a prompt.ts file that defines the configuration options for your package:
packages/<package-name>/
├── files/ # Template files for the package
│ ├── contracts/ # Package-specific content
│ ├── src/ # Package-specific content
│ └── ...
├── prompt.ts # Interactive prompts for the package
└── builder.ts # Logic for generating the package filesimport { checkbox, confirm, input } from '@inquirer/prompts'
import { PackagePrompt } from '../../types.js';
export interface MyPackageConfig {
useFeatureA: boolean
useFeatureB: boolean
// Add your configuration options
[key: string]: unknown
}
const prompt: PackagePrompt<MyPackageConfig> = async (ctx) => {
// Get package name
const name = await input({
message: 'Enter the name of your package:',
default: 'mypackage',
transformer: (value: string, { isFinal }) =>
isFinal ? `@${ctx.projectName.toLowerCase()}/${value}` : value
});
// Collect configuration options
const useFeatureA = await confirm({
message: 'Do you want to include Feature A?',
default: true
});
const useFeatureB = await confirm({
message: 'Do you want to include Feature B?',
default: false
});
// Return the configuration
return {
name,
useFeatureA,
useFeatureB
}
}
export default prompt;2. Implement Builder Class
Next, create a builder.ts file that extends the PackageBuilder base class:
packages/<package-name>/
├── files/ # Template files for the package
│ ├── contracts/ # Package-specific content
│ ├── src/ # Package-specific content
│ └── ...
├── prompt.ts # Interactive prompts for the package
└── builder.ts # Logic for generating the package files import path from 'path'
import { fileURLToPath } from 'url'
import picocolors from 'picocolors'
import { ProjectConfigV2 } from '../../types.js'
import { ListrTaskArg, PackageBuilder, ListrTaskList } from '../../builders/PackageBuilder.js'
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const { white } = picocolors;
export default class MyPackageBuilder extends PackageBuilder {
constructor(config: ProjectConfigV2) {
super(
config,
path.join(__dirname, 'files'),
config.packages.mypackage.name
);
}
public build(task: ListrTaskArg, nextSteps: string[]): ListrTaskList {
this._addNextSteps(nextSteps);
return task.newListr([
{
title: 'Copy package files',
task: this.copyPackageFiles.bind(this)
},
{
title: 'Create package.json',
task: this.createPackageJson.bind(this)
},
{
title: 'Create README.md',
task: async () => {
await this.createReadme({
packageManagerCmd: this.getPackageManagerCmd()
});
}
},
{
title: 'Parse template files',
task: async () => {
await this.parseAllFiles({
exclude: ['README.md', 'package.json']
});
}
}
]);
}
private _addNextSteps(nextSteps: string[]) {
// Add any package-specific next steps
const pkgCmd = this.config.packageManager;
const { packages } = this.config;
nextSteps.push(
white(`Run \`${pkgCmd} ${packages.mypackage.name} start\``)
);
}
}3. Prepare Template Files
Create a files directory with all the template files for your package:
packages/<package-name>/
├── files/ # Template files for the package
│ ├── contracts/ # Package-specific content
│ ├── src/ # Package-specific content
│ └── ...
├── prompt.ts # Interactive prompts for the package
└── builder.ts # Logic for generating the package files├── package.json # Will be processed as a Handlebars template
├── README.md # Will be processed as a Handlebars template
├── src/ # Package source files
│ ├── index.ts # Package source files
│ └── ...
└── ... # Other package files4. Register Your Package
Finally, register your package in src/packages/index.ts:
import mypackagePrompt, { MyPackageConfig } from './mypackage/prompt.js'
import MyPackageBuilder from './mypackage/builder.js'
export const Packages: PackageExport = {
hardhat: {
prompt: hardhatPrompt as PackagePrompt<HardhatConfig>,
builder: HardhatBuilder
},
vite: {
prompt: vitePrompt as PackagePrompt<ViteConfig>,
builder: ViteBuilder
},
// Add your new package here
mypackage: {
prompt: mypackagePrompt as PackagePrompt<MyPackageConfig>,
builder: MyPackageBuilder
}
};
export async function packagesPrompt() {
const packages = await checkbox<string>({
message: 'Select packages to install:',
choices: [
{ name: 'Hardhat', value: 'hardhat' },
{ name: 'Vite', value: 'vite' },
// Add your new package to the choices
{ name: 'My Package', value: 'mypackage' },
],
required: true,
pageSize: 20,
});
// ...
}
// Export your config type
export { MyPackageConfig } from './mypackage/prompt.js'Advanced Package Features
Conditional File Generation
You can conditionally generate files based on user configuration:
public build(task: ListrTaskArg, nextSteps: string[]): ListrTaskList {
// ...
if (this.config.packages.mypackage.useFeatureA) {
// Only copy feature A files if the user selected that option
await this.copyFiles('feature-a-templates', 'feature-a');
}
// ...
}Integration with Other Packages
To interact with other packages in the project:
public build(task: ListrTaskArg, nextSteps: string[]): ListrTaskList {
// Check if hardhat is enabled to add integrations
if (this.config.global.shouldAddHardhat) {
await this.copyFiles('hardhat-integration', 'hardhat-integration');
}
// ...
}Testing Your Package
To test your package during development:
- Build the project
pnpm build- Run the CLI with your package
pnpn dev tmp/test-project- Select your package during the prompt phase
? Select packages to install:
My Package- Verify the generated files match your expectations
Best Practices
- Modular Design: Keep your package focused on a single responsibility
- Consistent Naming: Follow the naming conventions of existing packages
- Clear Prompts: Make your configuration prompts clear and provide sensible defaults
- Error Handling: Add proper error handling in your builder
- Documentation: Add README files with clear instructions for your package
- Next Steps: Add next steps to the builder to guide the user on what to do next
Next Steps
Now that you understand how to create new packages, you can: