aiwg
Version:
Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo
243 lines (211 loc) • 8.44 kB
JavaScript
/**
* Project-Local Bundle Scaffolding
*
* Creates a complete `.aiwg/<type>/<name>/` bundle with a valid manifest,
* a starter artifact (skill or rule), and a README that includes the
* identical-form portability reminder.
*
* The output is byte-identical-shaped to upstream addon directories so
* `aiwg promote` can graduate it without any rewrite.
*
* @design @.aiwg/architecture/adr-identical-form-portability.md (#1038)
* @implements #1050
*/
import { mkdir, writeFile, stat } from 'fs/promises';
import { join } from 'path';
const TYPE_TO_DIR = {
extension: 'extensions',
addon: 'addons',
framework: 'frameworks',
plugin: 'plugins',
};
const NAME_REGEX = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
function buildManifest(opts) {
const { type, name, description = `Project-local ${type} '${name}'` } = opts;
const base = {
id: name,
type,
name,
version: '0.1.0',
description,
manifestVersion: '1',
platforms: { claude: 'full' },
keywords: [type, 'project-local'],
deployment: { pathTemplate: '.{platform}/skills/{id}.md' },
};
if (type === 'addon') {
base['addonConfig'] = { entry: { skills: 'skills/' } };
}
else if (type === 'framework') {
base['frameworkConfig'] = { path: 'src/' };
}
else if (type === 'plugin') {
base['pluginConfig'] = { payloadType: 'addon', payloadPath: 'payload/' };
}
return base;
}
function readme(opts) {
const { type, name, description = '' } = opts;
return `# ${name}
${description || `Project-local ${type} bundle.`}
## What this is
A project-local AIWG ${type} living under \`.aiwg/${TYPE_TO_DIR[type]}/${name}/\`.
Discovered automatically by \`aiwg use\` and deployed alongside upstream
artifacts.
## Layout
\`\`\`
.aiwg/${TYPE_TO_DIR[type]}/${name}/
├── manifest.json # Bundle metadata (validated by aiwg)
├── README.md # This file
└── ${type === 'framework' ? 'src/' : type === 'plugin' ? 'payload/' : 'skills/ or rules/'}
\`\`\`
## Usage
Deploy to your configured providers:
\`\`\`bash
aiwg use ${name}
\`\`\`
Inspect health:
\`\`\`bash
aiwg doctor --project-local
\`\`\`
Remove (preserves source under \`.aiwg/\`):
\`\`\`bash
aiwg remove ${name}
\`\`\`
## Identical-form portability
This directory is shaped **byte-identical** to upstream
\`agentic/code/addons/${name}/\`. To graduate, run:
\`\`\`bash
aiwg promote ${name} --dry-run # preview
aiwg promote ${name} # copy to upstream
aiwg promote ${name} --to corpus ~/my-corpus/ # or to a private corpus
\`\`\`
Keep this directory shaped like upstream so \`aiwg promote\` works.
## Customization tips
- Edit \`manifest.json\` to set a real \`description\`, bump \`version\` to
\`1.0.0\` when stable, and add platforms beyond \`claude\` if needed.
- Add new artifacts under \`skills/\`, \`rules/\`, \`agents/\`, or \`commands/\`
per AIWG conventions.
- Use \`@\`-references for cross-artifact links: \`@$AIWG_ROOT/...\` for
upstream paths, \`@.aiwg/...\` for project-local references (note: the
latter will block promotion unless \`--force\` is passed).
## See also
- \`docs/customization/project-local-quickstart.md\` — first bundle in 5 minutes
- \`docs/customization/project-local-lifecycle.md\` — full lifecycle reference
- \`docs/customization/extensions-vs-addons-vs-frameworks-vs-plugins.md\` — pick the right type
`;
}
const STARTER_SKILL = (name) => `---
name: ${name}-skill
description: Starter skill for the ${name} project-local bundle. Customize this.
---
# ${name}-skill
Replace this body with the workflow your skill performs.
## When to use
Describe the trigger conditions for this skill.
## Steps
1. Step one
2. Step two
<!-- TIP: keep front-matter \`name\` matching the file basename for predictable -->
<!-- TIP: deploy paths and traceability with the SKILL.md frontmatter schema. -->
`;
const STARTER_RULE = (name) => `---
id: ${name}-rule
---
# ${name}-rule
Describe the rule. Keep it concise — rules are loaded into every relevant
agent context, so terseness pays off.
## Why
Explain the motivation. Future-you will thank you.
## How to apply
State when this rule fires and what action it requires.
`;
const STARTER_AGENT = (name) => `---
name: ${name}-agent
description: Starter agent for the ${name} project-local bundle.
model: sonnet
---
# ${name}-agent
You are a specialist for [domain]. Customize this body with the agent's
focus, allowed tools, and termination conditions.
`;
export async function scaffoldProjectLocalBundle(options) {
const projectDir = options.projectDir ?? process.cwd();
const refuseOnExists = options.refuseOnExists ?? true;
const dryRun = options.dryRun ?? false;
if (!NAME_REGEX.test(options.name)) {
throw new Error(`Invalid bundle name '${options.name}': must be kebab-case (a-z, 0-9, '-'), no leading/trailing hyphen.`);
}
const dirName = TYPE_TO_DIR[options.type];
const bundlePath = join(projectDir, '.aiwg', dirName, options.name);
// Refuse to overwrite
try {
await stat(bundlePath);
if (refuseOnExists) {
return { bundlePath, filesCreated: [], alreadyExists: true, dryRun };
}
}
catch {
// Doesn't exist — good
}
if (dryRun) {
// Compute the file list without writing anything.
const starter = options.starter ?? (options.type === 'framework' || options.type === 'plugin' ? 'minimal' : 'skill');
const filesCreated = ['manifest.json', 'README.md'];
if (starter === 'skill')
filesCreated.push(`skills/${options.name}-skill/SKILL.md`);
else if (starter === 'rule')
filesCreated.push(`rules/${options.name}.md`);
else if (starter === 'agent')
filesCreated.push(`agents/${options.name}.md`);
if (options.type === 'framework')
filesCreated.push('src/.gitkeep');
if (options.type === 'plugin')
filesCreated.push('payload/.gitkeep');
return { bundlePath, filesCreated, alreadyExists: false, dryRun: true };
}
await mkdir(bundlePath, { recursive: true });
const filesCreated = [];
// Pick starter
const starter = options.starter ?? (options.type === 'framework' || options.type === 'plugin' ? 'minimal' : 'skill');
// manifest.json
const manifest = buildManifest(options);
await writeFile(join(bundlePath, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
filesCreated.push('manifest.json');
// README.md
await writeFile(join(bundlePath, 'README.md'), readme(options), 'utf-8');
filesCreated.push('README.md');
// Starter artifact
if (starter === 'skill') {
const skillDir = join(bundlePath, 'skills', `${options.name}-skill`);
await mkdir(skillDir, { recursive: true });
await writeFile(join(skillDir, 'SKILL.md'), STARTER_SKILL(options.name), 'utf-8');
filesCreated.push(`skills/${options.name}-skill/SKILL.md`);
}
else if (starter === 'rule') {
const rulesDir = join(bundlePath, 'rules');
await mkdir(rulesDir, { recursive: true });
await writeFile(join(rulesDir, `${options.name}.md`), STARTER_RULE(options.name), 'utf-8');
filesCreated.push(`rules/${options.name}.md`);
}
else if (starter === 'agent') {
const agentsDir = join(bundlePath, 'agents');
await mkdir(agentsDir, { recursive: true });
await writeFile(join(agentsDir, `${options.name}.md`), STARTER_AGENT(options.name), 'utf-8');
filesCreated.push(`agents/${options.name}.md`);
}
// 'minimal' = no starter artifact (operator fills it in)
// Type-specific stub directories
if (options.type === 'framework') {
await mkdir(join(bundlePath, 'src'), { recursive: true });
await writeFile(join(bundlePath, 'src', '.gitkeep'), '# Framework source files go here\n', 'utf-8');
filesCreated.push('src/.gitkeep');
}
if (options.type === 'plugin') {
await mkdir(join(bundlePath, 'payload'), { recursive: true });
await writeFile(join(bundlePath, 'payload', '.gitkeep'), '# Plugin payload files go here\n', 'utf-8');
filesCreated.push('payload/.gitkeep');
}
return { bundlePath, filesCreated, alreadyExists: false };
}
//# sourceMappingURL=project-local-scaffold.js.map