diff --git a/pgpm/cli/src/commands/init/index.ts b/pgpm/cli/src/commands/init/index.ts index 080078269..1e904f2a5 100644 --- a/pgpm/cli/src/commands/init/index.ts +++ b/pgpm/cli/src/commands/init/index.ts @@ -1,7 +1,9 @@ +import { execSync } from 'child_process'; import fs from 'fs'; import path from 'path'; import { + BoilerplateSkill, DEFAULT_TEMPLATE_REPO, DEFAULT_TEMPLATE_TOOL_NAME, inspectTemplate, @@ -275,6 +277,38 @@ interface InitContext { createWorkspace?: boolean; } +function installSkills(skills: BoilerplateSkill[], cwd: string): void { + const failed: string[] = []; + + for (const entry of skills) { + const source = entry.source.includes('://') + ? entry.source + : `https://github.com/${entry.source}`; + + for (const skill of entry.skills) { + const cmd = `npx --yes skills add ${source} --skill ${skill} --yes`; + try { + execSync(cmd, { + cwd, + stdio: ['pipe', 'inherit', 'inherit'], + timeout: 120_000, + }); + } catch { + failed.push(` npx skills add ${source} --skill ${skill}`); + } + } + } + + if (failed.length > 0) { + process.stdout.write('\n⚠️ Some skills could not be installed automatically.\n'); + process.stdout.write('Run the following commands manually:\n\n'); + for (const cmd of failed) { + process.stdout.write(`${cmd}\n`); + } + process.stdout.write('\n'); + } +} + async function handleWorkspaceInit( argv: Partial>, prompter: Inquirerer, @@ -329,6 +363,19 @@ async function handleWorkspaceInit( process.stdout.write('\n'); } + // Install skills declared in .boilerplate.json + const templateInfo = inspectTemplate({ + fromPath: ctx.fromPath, + templateRepo: ctx.templateRepo, + branch: ctx.branch, + dir: ctx.dir, + cwd: ctx.cwd, + }); + if (templateInfo.config?.skills?.length) { + process.stdout.write('\n📦 Installing skills...\n\n'); + installSkills(templateInfo.config.skills, targetPath); + } + const relPath = path.relative(process.cwd(), targetPath); process.stdout.write(`\n✨ Enjoy!\n\ncd ./${relPath}\n`); @@ -620,6 +667,20 @@ async function handleModuleInit( process.stdout.write('\n'); } + // Install skills declared in .boilerplate.json + const moduleTemplateInfo = inspectTemplate({ + fromPath: ctx.fromPath, + templateRepo: ctx.templateRepo, + branch: ctx.branch, + dir: ctx.dir, + cwd: ctx.cwd, + }); + if (moduleTemplateInfo.config?.skills?.length) { + const skillsCwd = project.workspacePath || modulePath; + process.stdout.write('\n📦 Installing skills...\n\n'); + installSkills(moduleTemplateInfo.config.skills, skillsCwd); + } + const relPath = path.relative(process.cwd(), modulePath); process.stdout.write(`\n✨ Enjoy!\n\ncd ./${relPath}\n`); diff --git a/pgpm/core/src/core/template-scaffold.ts b/pgpm/core/src/core/template-scaffold.ts index 79a619512..e6fa208e9 100644 --- a/pgpm/core/src/core/template-scaffold.ts +++ b/pgpm/core/src/core/template-scaffold.ts @@ -14,9 +14,15 @@ import type { Inquirerer, Question } from 'inquirerer'; export type WorkspaceType = 'pgpm' | 'pnpm' | 'lerna' | 'npm' | false; /** - * Extended BoilerplateConfig that adds workspace requirement field. - * This field controls both workspace detection and whether pgpm-specific files are created. + * Declares a skill to install after scaffolding completes. */ +export interface BoilerplateSkill { + /** GitHub repository (org/repo format) */ + source: string; + /** Skill name(s) to install from the source */ + skills: string[]; +} + export interface BoilerplateConfig extends GenomicBoilerplateConfig { /** * Specifies what type of workspace this template requires. @@ -29,6 +35,13 @@ export interface BoilerplateConfig extends GenomicBoilerplateConfig { * Defaults to 'pgpm' for 'module' type (backward compatibility), false for others. */ requiresWorkspace?: WorkspaceType; + /** + * Skills to install after scaffolding completes. + * Each entry specifies a source repository and skill names to install. + * Runs `npx skills add --skill ` for each entry. + * Non-fatal: prints manual install commands on failure. + */ + skills?: BoilerplateSkill[]; } export interface InspectTemplateOptions {