diff --git a/packages/create-gen-app/src/scaffolder/template-scaffolder.ts b/packages/create-gen-app/src/scaffolder/template-scaffolder.ts index 4ce353e..376c491 100644 --- a/packages/create-gen-app/src/scaffolder/template-scaffolder.ts +++ b/packages/create-gen-app/src/scaffolder/template-scaffolder.ts @@ -10,6 +10,8 @@ import { ScaffoldResult, BoilerplatesConfig, BoilerplateConfig, + InspectOptions, + InspectResult, } from './types'; /** @@ -80,6 +82,36 @@ export class TemplateScaffolder { return this.scaffoldFromRemote(resolvedTemplate, branch, options); } + /** + * Inspect a template without scaffolding. + * Clones/caches the template and reads its .boilerplate.json configuration + * without copying any files to an output directory. + * + * This is useful for metadata-driven workflows where you need to know + * the template's type or other configuration before deciding how to handle it. + * + * @param options - Inspect options + * @returns Inspect result with template metadata + */ + inspect(options: InspectOptions): InspectResult { + const template = options.template ?? this.config.defaultRepo; + if (!template) { + throw new Error( + 'No template specified and no defaultRepo configured. ' + + 'Either pass template in options or set defaultRepo in config.' + ); + } + + const branch = options.branch ?? this.config.defaultBranch; + const resolvedTemplate = this.resolveTemplatePath(template); + + if (this.isLocalPath(resolvedTemplate) && fs.existsSync(resolvedTemplate)) { + return this.inspectLocal(resolvedTemplate, options.fromPath); + } + + return this.inspectRemote(resolvedTemplate, branch, options.fromPath); + } + /** * Read the .boilerplates.json configuration from a template repository root. */ @@ -133,6 +165,75 @@ export class TemplateScaffolder { return this.templatizer; } + private inspectLocal( + templateDir: string, + fromPath?: string + ): InspectResult { + const { fromPath: resolvedFromPath, resolvedTemplatePath } = this.resolveFromPath( + templateDir, + fromPath + ); + + const config = this.readBoilerplateConfig(resolvedTemplatePath); + + return { + templateDir, + resolvedFromPath, + resolvedTemplatePath, + cacheUsed: false, + cacheExpired: false, + config, + }; + } + + private inspectRemote( + templateUrl: string, + branch: string | undefined, + fromPath?: string + ): InspectResult { + const normalizedUrl = this.gitCloner.normalizeUrl(templateUrl); + const cacheKey = this.cacheManager.createKey(normalizedUrl, branch); + + const expiredMetadata = this.cacheManager.checkExpiration(cacheKey); + if (expiredMetadata) { + this.cacheManager.clear(cacheKey); + } + + let templateDir: string; + let cacheUsed = false; + + const cachedPath = this.cacheManager.get(cacheKey); + if (cachedPath && !expiredMetadata) { + templateDir = cachedPath; + cacheUsed = true; + } else { + const tempDest = path.join(this.cacheManager.getReposDir(), cacheKey); + this.gitCloner.clone(normalizedUrl, tempDest, { + branch, + depth: 1, + singleBranch: true, + }); + this.cacheManager.set(cacheKey, tempDest); + templateDir = tempDest; + } + + const { fromPath: resolvedFromPath, resolvedTemplatePath } = this.resolveFromPath( + templateDir, + fromPath + ); + + const config = this.readBoilerplateConfig(resolvedTemplatePath); + + return { + templateDir, + resolvedFromPath, + resolvedTemplatePath, + cacheUsed, + cacheExpired: Boolean(expiredMetadata), + config, + }; + } + private async scaffoldFromLocal( templateDir: string, options: ScaffoldOptions diff --git a/packages/create-gen-app/src/scaffolder/types.ts b/packages/create-gen-app/src/scaffolder/types.ts index 9be0118..fa77f7a 100644 --- a/packages/create-gen-app/src/scaffolder/types.ts +++ b/packages/create-gen-app/src/scaffolder/types.ts @@ -145,3 +145,62 @@ export interface BoilerplateConfig { */ questions?: Question[]; } + +/** + * Options for inspecting a template without scaffolding. + * Used to read template metadata before deciding how to handle it. + */ +export interface InspectOptions { + /** + * Template repository URL, local path, or org/repo shorthand. + * If not provided, uses the defaultRepo from config. + */ + template?: string; + + /** + * Branch to clone (for remote repositories) + */ + branch?: string; + + /** + * Subdirectory within the template repository to inspect. + * Can be a direct path or a variant name that gets resolved via .boilerplates.json + */ + fromPath?: string; +} + +/** + * Result of inspecting a template. + * Contains metadata about the template without copying any files. + */ +export interface InspectResult { + /** + * Path to the cached/cloned template directory + */ + templateDir: string; + + /** + * The resolved fromPath after .boilerplates.json resolution + */ + resolvedFromPath?: string; + + /** + * Full path to the resolved template directory + */ + resolvedTemplatePath: string; + + /** + * Whether a cached template was used + */ + cacheUsed: boolean; + + /** + * Whether the cache was expired and refreshed + */ + cacheExpired: boolean; + + /** + * The .boilerplate.json configuration from the template, if present + */ + config: BoilerplateConfig | null; +}