import { basename, join, resolve } from 'node:path'; import { cpSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { spawnSync } from 'node:child_process'; import { packageRoot } from '../paths.js'; const INIT_USAGE = `Usage: harbor init [directory] [--workflows] [--config] Modes: harbor init Scaffold a new project into an empty directory harbor init . --workflows Replace scaffolded workflow files harbor init . --config Replace scaffolded config files harbor init . --workflows --config Replace both workflows and config files`; const templatesDir = resolve(packageRoot, 'templates/init'); const cliPackageJson = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf-8')) as { version: string; harborCollectorRange?: string; }; const packageRange = `>=${cliPackageJson.version} <1`; const collectorRange = cliPackageJson.harborCollectorRange; if (!collectorRange) { throw new Error('Missing harborCollectorRange in package.json'); } interface InitOptions { targetDir: string; workflows: boolean; config: boolean; } function parseArgs(argv: string[]): InitOptions { const flags = new Set(); let targetArg: string | undefined; for (const arg of argv) { if (arg === '--workflows' || arg === '--config') { flags.add(arg); continue; } if (arg.startsWith('-')) { throw new Error(`Unknown option: ${arg}`); } if (targetArg) { throw new Error('Too many positional arguments.'); } targetArg = arg; } return { targetDir: targetArg ? resolve(process.cwd(), targetArg) : process.cwd(), workflows: flags.has('--workflows'), config: flags.has('--config'), }; } function renderTemplate(content: string, projectName: string): string { return content .replace('{{PROJECT_NAME}}', projectName) .replaceAll('{{PACKAGE_RANGE}}', packageRange) .replaceAll('{{COLLECTOR_RANGE}}', collectorRange!); } function copyTemplateFile(source: string, destination: string, projectName: string): void { const content = renderTemplate(readFileSync(source, 'utf-8'), projectName); mkdirSync(resolve(destination, '..'), { recursive: true }); writeFileSync(destination, content); } function hasCategoryMode(options: InitOptions): boolean { return options.workflows || options.config; } function ensureEmptyDirectory(targetDir: string): void { if (!existsSync(targetDir)) { mkdirSync(targetDir, { recursive: true }); } const existing = readdirSync(targetDir).filter((entry) => entry !== '.git' && entry !== '.DS_Store'); if (existing.length > 0) { throw new Error(`Directory is not empty: ${targetDir}\n Please use an empty directory or specify a new one.`); } } function ensureCleanWorkingTree(targetDir: string, commandText: string): void { const gitRootResult = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd: targetDir, encoding: 'utf-8', }); if (gitRootResult.status !== 0) { console.warn('Git repository not detected; skipping clean working tree safety check.'); return; } const gitRoot = gitRootResult.stdout.trim(); const statusResult = spawnSync('git', ['status', '--porcelain'], { cwd: gitRoot, encoding: 'utf-8', }); if (statusResult.status !== 0) { throw new Error(statusResult.stderr.trim() || 'Failed to inspect git working tree.'); } if (statusResult.stdout.trim()) { throw new Error( `Error: \`${commandText}\` replaces existing scaffold files and requires a clean git working tree.\n` + ' Commit or stash your changes first, then run the command again.', ); } } function writeRootPackage(targetDir: string, projectName: string): void { copyTemplateFile(join(templatesDir, 'package.template.json'), join(targetDir, 'package.json'), projectName); console.log(' Created package.json'); } function writeReadme(targetDir: string, projectName: string): void { const readmeTemplate = readFileSync(join(templatesDir, 'README.md'), 'utf-8'); writeFileSync(join(targetDir, 'README.md'), readmeTemplate.replaceAll('{{PROJECT_NAME}}', projectName)); console.log(' Created README.md'); } function writeEnvFiles(targetDir: string): void { copyFileSync(join(templatesDir, '.env.example'), join(targetDir, '.env.example')); copyFileSync(join(templatesDir, '.env.example'), join(targetDir, '.env')); console.log(' Created .env.example'); console.log(' Created .env'); } function writeGitignore(targetDir: string): void { copyFileSync(join(templatesDir, 'gitignore'), join(targetDir, '.gitignore')); console.log(' Created .gitignore'); } function writeWorkflows(targetDir: string): void { cpSync(join(templatesDir, '.github', 'workflows'), join(targetDir, '.github', 'workflows'), { recursive: true }); console.log(' Created .github/workflows/collect-skills.yml'); console.log(' Created .github/workflows/deploy-cloudflare-pages.yml'); console.log(' Created .github/workflows/deploy-github-pages.yml'); console.log(' - workflow name: CollectSkills'); console.log(' - uses Harbor reusable workflow pinned to wf-v0'); console.log(' - workflow name: DeployCloudflarePages'); console.log(' - workflow name: DeployGitHubPages'); console.log(' - enable workflow_run in exactly one deploy workflow'); console.log(' - Cloudflare Pages uses CLOUDFLARE_PW_ secrets'); } function writeConfig(targetDir: string): void { cpSync(join(templatesDir, 'config'), join(targetDir, 'config'), { recursive: true }); console.log(' Created config/harbor.yaml'); console.log(' Created config/governance.yaml'); } function writeData(targetDir: string): void { cpSync(join(templatesDir, 'data'), join(targetDir, 'data'), { recursive: true }); console.log(' Created data/'); } function writeGuide(targetDir: string): void { cpSync(join(templatesDir, 'guide'), join(targetDir, 'guide'), { recursive: true }); console.log(' Created guide/'); } function writeCollectorPackage(targetDir: string, projectName: string): void { copyTemplateFile( join(templatesDir, 'collector', 'package.template.json'), join(targetDir, 'collector', 'package.json'), projectName, ); console.log(' Created collector/package.json'); } function replaceWorkflows(targetDir: string): void { rmSync(join(targetDir, '.github', 'workflows'), { recursive: true, force: true }); cpSync(join(templatesDir, '.github', 'workflows'), join(targetDir, '.github', 'workflows'), { recursive: true }); console.log(' Updated workflows'); } function replaceConfig(targetDir: string): void { rmSync(join(targetDir, 'config'), { recursive: true, force: true }); cpSync(join(templatesDir, 'config'), join(targetDir, 'config'), { recursive: true }); console.log(' Updated config'); } function printNextSteps(targetArg?: string): void { const changeDirectoryStep = targetArg && targetArg !== '.' ? ` 1. cd ${targetArg}\n` : ''; console.log(` Done! Next steps: ${changeDirectoryStep} 2. Edit .env: uncomment and set GH_ORG 3. Install dependencies: pnpm install 4. Install collector runtime dependencies: pnpm install --dir collector 5. Authenticate GitHub CLI and collect skills: gh auth login GH_TOKEN=$(gh auth token) pnpm collect 5'. Or edit .env and set GH_TOKEN, then run: pnpm collect 6. Run post-collect plugins: pnpm post-collect 7. Start development server: pnpm dev `); } export async function runCommand(argv = process.argv.slice(3)): Promise { let options: InitOptions | null = null; try { options = parseArgs(argv); } catch (error: unknown) { console.error(error instanceof Error ? error.message : String(error)); console.error(INIT_USAGE); process.exit(1); } if (!options) { process.exit(1); } const projectName = basename(options.targetDir); const commandText = `harbor init${argv.length ? ` ${argv.join(' ')}` : ''}`; console.log(`\nInitializing Agent Skill Harbor project: ${projectName}`); console.log(` Directory: ${options.targetDir}\n`); try { if (hasCategoryMode(options)) { if (!existsSync(options.targetDir)) { throw new Error(`Directory does not exist: ${options.targetDir}`); } ensureCleanWorkingTree(options.targetDir, commandText); if (options.workflows) { replaceWorkflows(options.targetDir); } if (options.config) { replaceConfig(options.targetDir); } return; } ensureEmptyDirectory(options.targetDir); writeRootPackage(options.targetDir, projectName); writeReadme(options.targetDir, projectName); writeEnvFiles(options.targetDir); writeGitignore(options.targetDir); writeWorkflows(options.targetDir); writeConfig(options.targetDir); writeData(options.targetDir); writeGuide(options.targetDir); writeCollectorPackage(options.targetDir, projectName); printNextSteps(argv[0]); } catch (error: unknown) { console.error(error instanceof Error ? error.message : String(error)); process.exit(1); } }