All files / core scaffold.ts

100% Statements 42/42
100% Branches 10/10
100% Functions 2/2
100% Lines 40/40

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91      2x 2x   2x 2x 2x 2x   2x 18x     18x 18x   2x                 16x   16x 16x 16x   2x                   14x 4x 4x 3x 1x 1x   2x 2x 2x   1x         14x   14x 14x   14x                   14x 14x   11x 11x   1x 1x   1x 1x   1x   14x    
// Copyright 2026 ForgeKit Contributors
// SPDX-License-Identifier: Apache-2.0
// https://github.com/SubhanshuMG/ForgeKit
import { spawnSync } from 'child_process';
import * as path from 'path';
import { ScaffoldOptions, ScaffoldResult, Template } from '../types';
import { getTemplate } from './template-resolver';
import { writeTemplateFiles } from './file-writer';
import { validateHookCommand } from './security';
import { trackEvent } from './telemetry';
 
export async function scaffold(options: ScaffoldOptions): Promise<ScaffoldResult> {
  const errors: string[] = [];
  let template: Template;
 
  try {
    template = await getTemplate(options.templateId);
  } catch (err) {
    return {
      success: false,
      projectPath: '',
      filesCreated: [],
      errors: [(err as Error).message],
      nextSteps: [],
    };
  }
 
  const projectPath = path.resolve(options.outputDir, options.projectName);
 
  let filesCreated: string[] = [];
  try {
    filesCreated = await writeTemplateFiles(template, options);
  } catch (err) {
    return {
      success: false,
      projectPath,
      filesCreated,
      errors: [(err as Error).message],
      nextSteps: [],
    };
  }
 
  // Run post-scaffold hooks (e.g. npm install)
  if (!options.skipInstall && !options.dryRun) {
    for (const hook of template.hooks) {
      if (hook.type !== 'post-scaffold') continue;
      if (!validateHookCommand(hook.command)) {
        errors.push(`Skipped unsafe hook command: ${hook.command}`);
        continue;
      }
      try {
        const result = spawnSync(hook.command, hook.args, { cwd: projectPath, stdio: 'inherit', shell: false });
        if (result.status !== 0) throw new Error(`exited with code ${result.status}`);
      } catch (err) {
        errors.push(`Hook "${hook.command} ${hook.args.join(' ')}" failed: ${(err as Error).message}`);
      }
    }
  }
 
  const nextSteps = buildNextSteps(template, options.projectName);
 
  const success = errors.length === 0;
  trackEvent('scaffold', { template: options.templateId, success });
 
  return {
    success,
    projectPath,
    filesCreated,
    errors,
    nextSteps,
  };
}
 
function buildNextSteps(template: Template, projectName: string): string[] {
  const steps: string[] = [`cd ${projectName}`];
  switch (template.id) {
    case 'web-app':
      steps.push('npm run dev', 'Open http://localhost:3000');
      break;
    case 'api-service':
      steps.push('pip install -r requirements.txt', 'uvicorn main:app --reload', 'Open http://localhost:8000/docs');
      break;
    case 'ml-pipeline':
      steps.push('pip install -r requirements.txt', 'jupyter lab', 'Open notebooks/01_explore.ipynb');
      break;
    default:
      steps.push('Follow the README in your new project');
  }
  return steps;
}