genomic 5.0.3 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,6 +2,7 @@ import { execSync } from 'child_process';
2
2
  import * as fs from 'fs';
3
3
  import * as os from 'os';
4
4
  import * as path from 'path';
5
+ import { createSpinner } from 'inquirerer';
5
6
  export class GitCloner {
6
7
  /**
7
8
  * Clone a git repository to a destination
@@ -73,14 +74,28 @@ export class GitCloner {
73
74
  const branch = options?.branch;
74
75
  const depth = options?.depth ?? 1;
75
76
  const singleBranch = options?.singleBranch ?? true;
77
+ const silent = options?.silent ?? true;
76
78
  const branchArgs = branch ? ` --branch ${branch}` : '';
77
79
  const singleBranchArgs = singleBranch ? ' --single-branch' : '';
78
80
  const depthArgs = ` --depth ${depth}`;
79
81
  const command = `git clone${branchArgs}${singleBranchArgs}${depthArgs} ${url} ${destination}`;
82
+ const spinner = silent ? createSpinner(`Cloning ${url}...`) : null;
80
83
  try {
81
- execSync(command, { stdio: 'inherit' });
84
+ if (spinner) {
85
+ spinner.start();
86
+ }
87
+ execSync(command, {
88
+ stdio: silent ? 'pipe' : 'inherit',
89
+ encoding: 'utf-8'
90
+ });
91
+ if (spinner) {
92
+ spinner.succeed('Repository cloned');
93
+ }
82
94
  }
83
95
  catch (error) {
96
+ if (spinner) {
97
+ spinner.fail('Failed to clone repository');
98
+ }
84
99
  // Clean up on failure
85
100
  if (fs.existsSync(destination)) {
86
101
  fs.rmSync(destination, { recursive: true, force: true });
@@ -1,2 +1,3 @@
1
1
  export * from './template-scaffolder';
2
2
  export * from './types';
3
+ export * from './scan-boilerplates';
@@ -0,0 +1,187 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ /**
4
+ * Directories to skip during recursive scanning.
5
+ * These are common directories that should never contain boilerplates.
6
+ */
7
+ const SKIP_DIRECTORIES = new Set([
8
+ '.git',
9
+ 'node_modules',
10
+ '.pnpm',
11
+ 'dist',
12
+ 'build',
13
+ 'coverage',
14
+ '.next',
15
+ '.nuxt',
16
+ '.cache',
17
+ '__pycache__',
18
+ '.venv',
19
+ 'venv',
20
+ ]);
21
+ /**
22
+ * Read the .boilerplate.json configuration from a directory.
23
+ *
24
+ * @param dirPath - The directory path to check
25
+ * @returns The boilerplate config or null if not found
26
+ */
27
+ export function readBoilerplateConfig(dirPath) {
28
+ const configPath = path.join(dirPath, '.boilerplate.json');
29
+ if (fs.existsSync(configPath)) {
30
+ try {
31
+ const content = fs.readFileSync(configPath, 'utf-8');
32
+ return JSON.parse(content);
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ return null;
39
+ }
40
+ /**
41
+ * Recursively scan a directory for boilerplate templates.
42
+ *
43
+ * A boilerplate is any directory containing a `.boilerplate.json` file.
44
+ * This function recursively searches the entire directory tree (with sensible
45
+ * pruning of common non-template directories like node_modules, .git, etc.)
46
+ * and returns all discovered boilerplates with their relative paths.
47
+ *
48
+ * This is useful when:
49
+ * - The user specifies `--dir .` to bypass `.boilerplates.json`
50
+ * - You want to discover all available boilerplates regardless of nesting
51
+ * - You need to match a `fromPath` against available boilerplates
52
+ *
53
+ * @param baseDir - The root directory to start scanning from
54
+ * @param options - Scanning options
55
+ * @returns Array of discovered boilerplates with relative paths
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * // Given structure:
60
+ * // repo/
61
+ * // default/
62
+ * // module/.boilerplate.json
63
+ * // workspace/.boilerplate.json
64
+ * // scripts/ (no .boilerplate.json)
65
+ *
66
+ * const boilerplates = scanBoilerplatesRecursive('/path/to/repo');
67
+ * // Returns:
68
+ * // [
69
+ * // { relativePath: 'default/module', absolutePath: '...', config: {...} },
70
+ * // { relativePath: 'default/workspace', absolutePath: '...', config: {...} }
71
+ * // ]
72
+ * // Note: 'scripts' is not included because it has no .boilerplate.json
73
+ * ```
74
+ */
75
+ export function scanBoilerplatesRecursive(baseDir, options = {}) {
76
+ const { maxDepth = 10, skipDirectories = [] } = options;
77
+ const boilerplates = [];
78
+ const skipSet = new Set([...SKIP_DIRECTORIES, ...skipDirectories]);
79
+ function scan(currentDir, relativePath, depth) {
80
+ if (depth > maxDepth) {
81
+ return;
82
+ }
83
+ if (!fs.existsSync(currentDir)) {
84
+ return;
85
+ }
86
+ let entries;
87
+ try {
88
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
89
+ }
90
+ catch {
91
+ return;
92
+ }
93
+ for (const entry of entries) {
94
+ if (!entry.isDirectory()) {
95
+ continue;
96
+ }
97
+ if (skipSet.has(entry.name)) {
98
+ continue;
99
+ }
100
+ const entryPath = path.join(currentDir, entry.name);
101
+ const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
102
+ const config = readBoilerplateConfig(entryPath);
103
+ if (config) {
104
+ boilerplates.push({
105
+ relativePath: entryRelativePath,
106
+ absolutePath: entryPath,
107
+ config,
108
+ });
109
+ }
110
+ // Continue scanning subdirectories even if this directory is a boilerplate
111
+ // (in case there are nested boilerplates, though uncommon)
112
+ scan(entryPath, entryRelativePath, depth + 1);
113
+ }
114
+ }
115
+ scan(baseDir, '', 0);
116
+ // Sort by relative path for consistent ordering
117
+ boilerplates.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
118
+ return boilerplates;
119
+ }
120
+ /**
121
+ * Find a boilerplate by matching against a fromPath.
122
+ *
123
+ * This function attempts to match a user-provided `fromPath` against
124
+ * discovered boilerplates. It supports:
125
+ * 1. Exact match: `fromPath` matches a relative path exactly
126
+ * 2. Basename match: `fromPath` matches the last segment of a relative path
127
+ * (only if unambiguous - i.e., exactly one match)
128
+ *
129
+ * @param boilerplates - Array of scanned boilerplates
130
+ * @param fromPath - The path to match against
131
+ * @returns The matching boilerplate, or null if no match or ambiguous
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const boilerplates = scanBoilerplatesRecursive('/path/to/repo');
136
+ *
137
+ * // Exact match
138
+ * findBoilerplateByPath(boilerplates, 'default/module');
139
+ * // Returns the 'default/module' boilerplate
140
+ *
141
+ * // Basename match (unambiguous)
142
+ * findBoilerplateByPath(boilerplates, 'module');
143
+ * // Returns the 'default/module' boilerplate if it's the only one ending in 'module'
144
+ *
145
+ * // Ambiguous basename match
146
+ * // If both 'default/module' and 'supabase/module' exist:
147
+ * findBoilerplateByPath(boilerplates, 'module');
148
+ * // Returns null (ambiguous)
149
+ * ```
150
+ */
151
+ export function findBoilerplateByPath(boilerplates, fromPath) {
152
+ // Normalize the fromPath (remove leading/trailing slashes)
153
+ const normalizedPath = fromPath.replace(/^\/+|\/+$/g, '');
154
+ // Try exact match first
155
+ const exactMatch = boilerplates.find((bp) => bp.relativePath === normalizedPath);
156
+ if (exactMatch) {
157
+ return exactMatch;
158
+ }
159
+ // Try basename match (last segment of path)
160
+ const basename = path.basename(normalizedPath);
161
+ const basenameMatches = boilerplates.filter((bp) => path.basename(bp.relativePath) === basename);
162
+ // Only return if unambiguous (exactly one match)
163
+ if (basenameMatches.length === 1) {
164
+ return basenameMatches[0];
165
+ }
166
+ return null;
167
+ }
168
+ /**
169
+ * Find a boilerplate by type within a scanned list.
170
+ *
171
+ * @param boilerplates - Array of scanned boilerplates
172
+ * @param type - The type to find (e.g., 'workspace', 'module')
173
+ * @returns The matching boilerplate or undefined
174
+ */
175
+ export function findBoilerplateByType(boilerplates, type) {
176
+ return boilerplates.find((bp) => bp.config.type === type);
177
+ }
178
+ /**
179
+ * Get all boilerplates of a specific type.
180
+ *
181
+ * @param boilerplates - Array of scanned boilerplates
182
+ * @param type - The type to filter by
183
+ * @returns Array of matching boilerplates
184
+ */
185
+ export function filterBoilerplatesByType(boilerplates, type) {
186
+ return boilerplates.filter((bp) => bp.config.type === type);
187
+ }
@@ -3,6 +3,7 @@ import * as path from 'path';
3
3
  import { CacheManager } from '../cache/cache-manager';
4
4
  import { GitCloner } from '../git/git-cloner';
5
5
  import { Templatizer } from '../template/templatizer';
6
+ import { scanBoilerplatesRecursive, findBoilerplateByPath, } from './scan-boilerplates';
6
7
  /**
7
8
  * High-level orchestrator for template scaffolding operations.
8
9
  * Combines CacheManager, GitCloner, and Templatizer into a single, easy-to-use API.
@@ -137,6 +138,33 @@ export class TemplateScaffolder {
137
138
  getTemplatizer() {
138
139
  return this.templatizer;
139
140
  }
141
+ /**
142
+ * Scan a template directory recursively for all boilerplates.
143
+ *
144
+ * A boilerplate is any directory containing a `.boilerplate.json` file.
145
+ * This method recursively searches the entire directory tree and returns
146
+ * all discovered boilerplates with their relative paths.
147
+ *
148
+ * This is useful when:
149
+ * - The user specifies `--dir .` to bypass `.boilerplates.json`
150
+ * - You want to discover all available boilerplates regardless of nesting
151
+ * - You need to present a list of available boilerplates to the user
152
+ *
153
+ * @param templateDir - The root directory to scan
154
+ * @param options - Scanning options (maxDepth, skipDirectories)
155
+ * @returns Array of discovered boilerplates with relative paths
156
+ *
157
+ * @example
158
+ * ```typescript
159
+ * const scaffolder = new TemplateScaffolder({ toolName: 'my-cli' });
160
+ * const inspection = scaffolder.inspect({ template: 'org/repo' });
161
+ * const boilerplates = scaffolder.scanBoilerplates(inspection.templateDir);
162
+ * // Returns: [{ relativePath: 'default/module', ... }, { relativePath: 'default/workspace', ... }]
163
+ * ```
164
+ */
165
+ scanBoilerplates(templateDir, options) {
166
+ return scanBoilerplatesRecursive(templateDir, options);
167
+ }
140
168
  inspectLocal(templateDir, fromPath, useBoilerplatesConfig = true) {
141
169
  const { fromPath: resolvedFromPath, resolvedTemplatePath } = this.resolveFromPath(templateDir, fromPath, useBoilerplatesConfig);
142
170
  const config = this.readBoilerplateConfig(resolvedTemplatePath);
@@ -246,12 +274,13 @@ export class TemplateScaffolder {
246
274
  };
247
275
  }
248
276
  /**
249
- * Resolve the fromPath using .boilerplates.json convention.
277
+ * Resolve the fromPath using .boilerplates.json convention and recursive scanning.
250
278
  *
251
279
  * Resolution order:
252
280
  * 1. If explicit fromPath is provided and exists, use it directly
253
281
  * 2. If useBoilerplatesConfig is true and .boilerplates.json exists with a dir field, prepend it to fromPath
254
- * 3. Return the fromPath as-is
282
+ * 3. Recursively scan for boilerplates and try to match fromPath (exact match, then basename match if unambiguous)
283
+ * 4. Return the fromPath as-is (will likely fail later if path doesn't exist)
255
284
  *
256
285
  * @param templateDir - The template repository root directory
257
286
  * @param fromPath - The subdirectory path to resolve
@@ -286,6 +315,17 @@ export class TemplateScaffolder {
286
315
  }
287
316
  }
288
317
  }
318
+ // Try recursive scan to find a matching boilerplate
319
+ // This handles cases like `--dir .` where the user wants to match against
320
+ // discovered boilerplates (e.g., "module" matching "default/module")
321
+ const boilerplates = scanBoilerplatesRecursive(templateDir);
322
+ const match = findBoilerplateByPath(boilerplates, fromPath);
323
+ if (match) {
324
+ return {
325
+ fromPath: match.relativePath,
326
+ resolvedTemplatePath: match.absolutePath,
327
+ };
328
+ }
289
329
  return {
290
330
  fromPath,
291
331
  resolvedTemplatePath: path.join(templateDir, fromPath),
package/git/git-cloner.js CHANGED
@@ -38,6 +38,7 @@ const child_process_1 = require("child_process");
38
38
  const fs = __importStar(require("fs"));
39
39
  const os = __importStar(require("os"));
40
40
  const path = __importStar(require("path"));
41
+ const inquirerer_1 = require("inquirerer");
41
42
  class GitCloner {
42
43
  /**
43
44
  * Clone a git repository to a destination
@@ -109,14 +110,28 @@ class GitCloner {
109
110
  const branch = options?.branch;
110
111
  const depth = options?.depth ?? 1;
111
112
  const singleBranch = options?.singleBranch ?? true;
113
+ const silent = options?.silent ?? true;
112
114
  const branchArgs = branch ? ` --branch ${branch}` : '';
113
115
  const singleBranchArgs = singleBranch ? ' --single-branch' : '';
114
116
  const depthArgs = ` --depth ${depth}`;
115
117
  const command = `git clone${branchArgs}${singleBranchArgs}${depthArgs} ${url} ${destination}`;
118
+ const spinner = silent ? (0, inquirerer_1.createSpinner)(`Cloning ${url}...`) : null;
116
119
  try {
117
- (0, child_process_1.execSync)(command, { stdio: 'inherit' });
120
+ if (spinner) {
121
+ spinner.start();
122
+ }
123
+ (0, child_process_1.execSync)(command, {
124
+ stdio: silent ? 'pipe' : 'inherit',
125
+ encoding: 'utf-8'
126
+ });
127
+ if (spinner) {
128
+ spinner.succeed('Repository cloned');
129
+ }
118
130
  }
119
131
  catch (error) {
132
+ if (spinner) {
133
+ spinner.fail('Failed to clone repository');
134
+ }
120
135
  // Clean up on failure
121
136
  if (fs.existsSync(destination)) {
122
137
  fs.rmSync(destination, { recursive: true, force: true });
package/git/types.d.ts CHANGED
@@ -2,6 +2,8 @@ export interface GitCloneOptions {
2
2
  branch?: string;
3
3
  depth?: number;
4
4
  singleBranch?: boolean;
5
+ /** If true (default), show spinner and silence git output. If false, show raw git output. */
6
+ silent?: boolean;
5
7
  }
6
8
  export interface GitCloneResult {
7
9
  destination: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genomic",
3
- "version": "5.0.3",
3
+ "version": "5.2.0",
4
4
  "author": "Constructive <developers@constructive.io>",
5
5
  "description": "Clone and customize template repositories with variable replacement",
6
6
  "main": "index.js",
@@ -29,12 +29,12 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "appstash": "0.2.7",
32
- "inquirerer": "4.1.2"
32
+ "inquirerer": "4.2.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "copyfiles": "^2.4.1",
36
36
  "makage": "0.1.8"
37
37
  },
38
38
  "keywords": [],
39
- "gitHead": "8f2e433e1feb28d23095ce761388a61a0e827bd0"
39
+ "gitHead": "89afff9da8425e38d1c8b75b2021833581a90307"
40
40
  }
@@ -1,2 +1,3 @@
1
1
  export * from './template-scaffolder';
2
2
  export * from './types';
3
+ export * from './scan-boilerplates';
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./template-scaffolder"), exports);
18
18
  __exportStar(require("./types"), exports);
19
+ __exportStar(require("./scan-boilerplates"), exports);
@@ -0,0 +1,124 @@
1
+ import { BoilerplateConfig } from './types';
2
+ /**
3
+ * Result of scanning for boilerplates.
4
+ */
5
+ export interface ScannedBoilerplate {
6
+ /**
7
+ * The relative path from the scan root to the boilerplate directory.
8
+ * For example: "default/module", "default/workspace"
9
+ */
10
+ relativePath: string;
11
+ /**
12
+ * The absolute path to the boilerplate directory.
13
+ */
14
+ absolutePath: string;
15
+ /**
16
+ * The boilerplate configuration from .boilerplate.json
17
+ */
18
+ config: BoilerplateConfig;
19
+ }
20
+ /**
21
+ * Options for scanning boilerplates.
22
+ */
23
+ export interface ScanBoilerplatesOptions {
24
+ /**
25
+ * Maximum depth to recurse into directories.
26
+ * Default: 10 (should be enough for any reasonable structure)
27
+ */
28
+ maxDepth?: number;
29
+ /**
30
+ * Additional directory names to skip during scanning.
31
+ */
32
+ skipDirectories?: string[];
33
+ }
34
+ /**
35
+ * Read the .boilerplate.json configuration from a directory.
36
+ *
37
+ * @param dirPath - The directory path to check
38
+ * @returns The boilerplate config or null if not found
39
+ */
40
+ export declare function readBoilerplateConfig(dirPath: string): BoilerplateConfig | null;
41
+ /**
42
+ * Recursively scan a directory for boilerplate templates.
43
+ *
44
+ * A boilerplate is any directory containing a `.boilerplate.json` file.
45
+ * This function recursively searches the entire directory tree (with sensible
46
+ * pruning of common non-template directories like node_modules, .git, etc.)
47
+ * and returns all discovered boilerplates with their relative paths.
48
+ *
49
+ * This is useful when:
50
+ * - The user specifies `--dir .` to bypass `.boilerplates.json`
51
+ * - You want to discover all available boilerplates regardless of nesting
52
+ * - You need to match a `fromPath` against available boilerplates
53
+ *
54
+ * @param baseDir - The root directory to start scanning from
55
+ * @param options - Scanning options
56
+ * @returns Array of discovered boilerplates with relative paths
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * // Given structure:
61
+ * // repo/
62
+ * // default/
63
+ * // module/.boilerplate.json
64
+ * // workspace/.boilerplate.json
65
+ * // scripts/ (no .boilerplate.json)
66
+ *
67
+ * const boilerplates = scanBoilerplatesRecursive('/path/to/repo');
68
+ * // Returns:
69
+ * // [
70
+ * // { relativePath: 'default/module', absolutePath: '...', config: {...} },
71
+ * // { relativePath: 'default/workspace', absolutePath: '...', config: {...} }
72
+ * // ]
73
+ * // Note: 'scripts' is not included because it has no .boilerplate.json
74
+ * ```
75
+ */
76
+ export declare function scanBoilerplatesRecursive(baseDir: string, options?: ScanBoilerplatesOptions): ScannedBoilerplate[];
77
+ /**
78
+ * Find a boilerplate by matching against a fromPath.
79
+ *
80
+ * This function attempts to match a user-provided `fromPath` against
81
+ * discovered boilerplates. It supports:
82
+ * 1. Exact match: `fromPath` matches a relative path exactly
83
+ * 2. Basename match: `fromPath` matches the last segment of a relative path
84
+ * (only if unambiguous - i.e., exactly one match)
85
+ *
86
+ * @param boilerplates - Array of scanned boilerplates
87
+ * @param fromPath - The path to match against
88
+ * @returns The matching boilerplate, or null if no match or ambiguous
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * const boilerplates = scanBoilerplatesRecursive('/path/to/repo');
93
+ *
94
+ * // Exact match
95
+ * findBoilerplateByPath(boilerplates, 'default/module');
96
+ * // Returns the 'default/module' boilerplate
97
+ *
98
+ * // Basename match (unambiguous)
99
+ * findBoilerplateByPath(boilerplates, 'module');
100
+ * // Returns the 'default/module' boilerplate if it's the only one ending in 'module'
101
+ *
102
+ * // Ambiguous basename match
103
+ * // If both 'default/module' and 'supabase/module' exist:
104
+ * findBoilerplateByPath(boilerplates, 'module');
105
+ * // Returns null (ambiguous)
106
+ * ```
107
+ */
108
+ export declare function findBoilerplateByPath(boilerplates: ScannedBoilerplate[], fromPath: string): ScannedBoilerplate | null;
109
+ /**
110
+ * Find a boilerplate by type within a scanned list.
111
+ *
112
+ * @param boilerplates - Array of scanned boilerplates
113
+ * @param type - The type to find (e.g., 'workspace', 'module')
114
+ * @returns The matching boilerplate or undefined
115
+ */
116
+ export declare function findBoilerplateByType(boilerplates: ScannedBoilerplate[], type: string): ScannedBoilerplate | undefined;
117
+ /**
118
+ * Get all boilerplates of a specific type.
119
+ *
120
+ * @param boilerplates - Array of scanned boilerplates
121
+ * @param type - The type to filter by
122
+ * @returns Array of matching boilerplates
123
+ */
124
+ export declare function filterBoilerplatesByType(boilerplates: ScannedBoilerplate[], type: string): ScannedBoilerplate[];
@@ -0,0 +1,227 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.readBoilerplateConfig = readBoilerplateConfig;
37
+ exports.scanBoilerplatesRecursive = scanBoilerplatesRecursive;
38
+ exports.findBoilerplateByPath = findBoilerplateByPath;
39
+ exports.findBoilerplateByType = findBoilerplateByType;
40
+ exports.filterBoilerplatesByType = filterBoilerplatesByType;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ /**
44
+ * Directories to skip during recursive scanning.
45
+ * These are common directories that should never contain boilerplates.
46
+ */
47
+ const SKIP_DIRECTORIES = new Set([
48
+ '.git',
49
+ 'node_modules',
50
+ '.pnpm',
51
+ 'dist',
52
+ 'build',
53
+ 'coverage',
54
+ '.next',
55
+ '.nuxt',
56
+ '.cache',
57
+ '__pycache__',
58
+ '.venv',
59
+ 'venv',
60
+ ]);
61
+ /**
62
+ * Read the .boilerplate.json configuration from a directory.
63
+ *
64
+ * @param dirPath - The directory path to check
65
+ * @returns The boilerplate config or null if not found
66
+ */
67
+ function readBoilerplateConfig(dirPath) {
68
+ const configPath = path.join(dirPath, '.boilerplate.json');
69
+ if (fs.existsSync(configPath)) {
70
+ try {
71
+ const content = fs.readFileSync(configPath, 'utf-8');
72
+ return JSON.parse(content);
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+ /**
81
+ * Recursively scan a directory for boilerplate templates.
82
+ *
83
+ * A boilerplate is any directory containing a `.boilerplate.json` file.
84
+ * This function recursively searches the entire directory tree (with sensible
85
+ * pruning of common non-template directories like node_modules, .git, etc.)
86
+ * and returns all discovered boilerplates with their relative paths.
87
+ *
88
+ * This is useful when:
89
+ * - The user specifies `--dir .` to bypass `.boilerplates.json`
90
+ * - You want to discover all available boilerplates regardless of nesting
91
+ * - You need to match a `fromPath` against available boilerplates
92
+ *
93
+ * @param baseDir - The root directory to start scanning from
94
+ * @param options - Scanning options
95
+ * @returns Array of discovered boilerplates with relative paths
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * // Given structure:
100
+ * // repo/
101
+ * // default/
102
+ * // module/.boilerplate.json
103
+ * // workspace/.boilerplate.json
104
+ * // scripts/ (no .boilerplate.json)
105
+ *
106
+ * const boilerplates = scanBoilerplatesRecursive('/path/to/repo');
107
+ * // Returns:
108
+ * // [
109
+ * // { relativePath: 'default/module', absolutePath: '...', config: {...} },
110
+ * // { relativePath: 'default/workspace', absolutePath: '...', config: {...} }
111
+ * // ]
112
+ * // Note: 'scripts' is not included because it has no .boilerplate.json
113
+ * ```
114
+ */
115
+ function scanBoilerplatesRecursive(baseDir, options = {}) {
116
+ const { maxDepth = 10, skipDirectories = [] } = options;
117
+ const boilerplates = [];
118
+ const skipSet = new Set([...SKIP_DIRECTORIES, ...skipDirectories]);
119
+ function scan(currentDir, relativePath, depth) {
120
+ if (depth > maxDepth) {
121
+ return;
122
+ }
123
+ if (!fs.existsSync(currentDir)) {
124
+ return;
125
+ }
126
+ let entries;
127
+ try {
128
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
129
+ }
130
+ catch {
131
+ return;
132
+ }
133
+ for (const entry of entries) {
134
+ if (!entry.isDirectory()) {
135
+ continue;
136
+ }
137
+ if (skipSet.has(entry.name)) {
138
+ continue;
139
+ }
140
+ const entryPath = path.join(currentDir, entry.name);
141
+ const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
142
+ const config = readBoilerplateConfig(entryPath);
143
+ if (config) {
144
+ boilerplates.push({
145
+ relativePath: entryRelativePath,
146
+ absolutePath: entryPath,
147
+ config,
148
+ });
149
+ }
150
+ // Continue scanning subdirectories even if this directory is a boilerplate
151
+ // (in case there are nested boilerplates, though uncommon)
152
+ scan(entryPath, entryRelativePath, depth + 1);
153
+ }
154
+ }
155
+ scan(baseDir, '', 0);
156
+ // Sort by relative path for consistent ordering
157
+ boilerplates.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
158
+ return boilerplates;
159
+ }
160
+ /**
161
+ * Find a boilerplate by matching against a fromPath.
162
+ *
163
+ * This function attempts to match a user-provided `fromPath` against
164
+ * discovered boilerplates. It supports:
165
+ * 1. Exact match: `fromPath` matches a relative path exactly
166
+ * 2. Basename match: `fromPath` matches the last segment of a relative path
167
+ * (only if unambiguous - i.e., exactly one match)
168
+ *
169
+ * @param boilerplates - Array of scanned boilerplates
170
+ * @param fromPath - The path to match against
171
+ * @returns The matching boilerplate, or null if no match or ambiguous
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * const boilerplates = scanBoilerplatesRecursive('/path/to/repo');
176
+ *
177
+ * // Exact match
178
+ * findBoilerplateByPath(boilerplates, 'default/module');
179
+ * // Returns the 'default/module' boilerplate
180
+ *
181
+ * // Basename match (unambiguous)
182
+ * findBoilerplateByPath(boilerplates, 'module');
183
+ * // Returns the 'default/module' boilerplate if it's the only one ending in 'module'
184
+ *
185
+ * // Ambiguous basename match
186
+ * // If both 'default/module' and 'supabase/module' exist:
187
+ * findBoilerplateByPath(boilerplates, 'module');
188
+ * // Returns null (ambiguous)
189
+ * ```
190
+ */
191
+ function findBoilerplateByPath(boilerplates, fromPath) {
192
+ // Normalize the fromPath (remove leading/trailing slashes)
193
+ const normalizedPath = fromPath.replace(/^\/+|\/+$/g, '');
194
+ // Try exact match first
195
+ const exactMatch = boilerplates.find((bp) => bp.relativePath === normalizedPath);
196
+ if (exactMatch) {
197
+ return exactMatch;
198
+ }
199
+ // Try basename match (last segment of path)
200
+ const basename = path.basename(normalizedPath);
201
+ const basenameMatches = boilerplates.filter((bp) => path.basename(bp.relativePath) === basename);
202
+ // Only return if unambiguous (exactly one match)
203
+ if (basenameMatches.length === 1) {
204
+ return basenameMatches[0];
205
+ }
206
+ return null;
207
+ }
208
+ /**
209
+ * Find a boilerplate by type within a scanned list.
210
+ *
211
+ * @param boilerplates - Array of scanned boilerplates
212
+ * @param type - The type to find (e.g., 'workspace', 'module')
213
+ * @returns The matching boilerplate or undefined
214
+ */
215
+ function findBoilerplateByType(boilerplates, type) {
216
+ return boilerplates.find((bp) => bp.config.type === type);
217
+ }
218
+ /**
219
+ * Get all boilerplates of a specific type.
220
+ *
221
+ * @param boilerplates - Array of scanned boilerplates
222
+ * @param type - The type to filter by
223
+ * @returns Array of matching boilerplates
224
+ */
225
+ function filterBoilerplatesByType(boilerplates, type) {
226
+ return boilerplates.filter((bp) => bp.config.type === type);
227
+ }
@@ -2,6 +2,7 @@ import { CacheManager } from '../cache/cache-manager';
2
2
  import { GitCloner } from '../git/git-cloner';
3
3
  import { Templatizer } from '../template/templatizer';
4
4
  import { TemplateScaffolderConfig, ScaffoldOptions, ScaffoldResult, BoilerplatesConfig, BoilerplateConfig, InspectOptions, InspectResult } from './types';
5
+ import { ScannedBoilerplate, ScanBoilerplatesOptions } from './scan-boilerplates';
5
6
  /**
6
7
  * High-level orchestrator for template scaffolding operations.
7
8
  * Combines CacheManager, GitCloner, and Templatizer into a single, easy-to-use API.
@@ -69,17 +70,43 @@ export declare class TemplateScaffolder {
69
70
  * Get the underlying Templatizer instance for advanced template operations.
70
71
  */
71
72
  getTemplatizer(): Templatizer;
73
+ /**
74
+ * Scan a template directory recursively for all boilerplates.
75
+ *
76
+ * A boilerplate is any directory containing a `.boilerplate.json` file.
77
+ * This method recursively searches the entire directory tree and returns
78
+ * all discovered boilerplates with their relative paths.
79
+ *
80
+ * This is useful when:
81
+ * - The user specifies `--dir .` to bypass `.boilerplates.json`
82
+ * - You want to discover all available boilerplates regardless of nesting
83
+ * - You need to present a list of available boilerplates to the user
84
+ *
85
+ * @param templateDir - The root directory to scan
86
+ * @param options - Scanning options (maxDepth, skipDirectories)
87
+ * @returns Array of discovered boilerplates with relative paths
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * const scaffolder = new TemplateScaffolder({ toolName: 'my-cli' });
92
+ * const inspection = scaffolder.inspect({ template: 'org/repo' });
93
+ * const boilerplates = scaffolder.scanBoilerplates(inspection.templateDir);
94
+ * // Returns: [{ relativePath: 'default/module', ... }, { relativePath: 'default/workspace', ... }]
95
+ * ```
96
+ */
97
+ scanBoilerplates(templateDir: string, options?: ScanBoilerplatesOptions): ScannedBoilerplate[];
72
98
  private inspectLocal;
73
99
  private inspectRemote;
74
100
  private scaffoldFromLocal;
75
101
  private scaffoldFromRemote;
76
102
  /**
77
- * Resolve the fromPath using .boilerplates.json convention.
103
+ * Resolve the fromPath using .boilerplates.json convention and recursive scanning.
78
104
  *
79
105
  * Resolution order:
80
106
  * 1. If explicit fromPath is provided and exists, use it directly
81
107
  * 2. If useBoilerplatesConfig is true and .boilerplates.json exists with a dir field, prepend it to fromPath
82
- * 3. Return the fromPath as-is
108
+ * 3. Recursively scan for boilerplates and try to match fromPath (exact match, then basename match if unambiguous)
109
+ * 4. Return the fromPath as-is (will likely fail later if path doesn't exist)
83
110
  *
84
111
  * @param templateDir - The template repository root directory
85
112
  * @param fromPath - The subdirectory path to resolve
@@ -39,6 +39,7 @@ const path = __importStar(require("path"));
39
39
  const cache_manager_1 = require("../cache/cache-manager");
40
40
  const git_cloner_1 = require("../git/git-cloner");
41
41
  const templatizer_1 = require("../template/templatizer");
42
+ const scan_boilerplates_1 = require("./scan-boilerplates");
42
43
  /**
43
44
  * High-level orchestrator for template scaffolding operations.
44
45
  * Combines CacheManager, GitCloner, and Templatizer into a single, easy-to-use API.
@@ -173,6 +174,33 @@ class TemplateScaffolder {
173
174
  getTemplatizer() {
174
175
  return this.templatizer;
175
176
  }
177
+ /**
178
+ * Scan a template directory recursively for all boilerplates.
179
+ *
180
+ * A boilerplate is any directory containing a `.boilerplate.json` file.
181
+ * This method recursively searches the entire directory tree and returns
182
+ * all discovered boilerplates with their relative paths.
183
+ *
184
+ * This is useful when:
185
+ * - The user specifies `--dir .` to bypass `.boilerplates.json`
186
+ * - You want to discover all available boilerplates regardless of nesting
187
+ * - You need to present a list of available boilerplates to the user
188
+ *
189
+ * @param templateDir - The root directory to scan
190
+ * @param options - Scanning options (maxDepth, skipDirectories)
191
+ * @returns Array of discovered boilerplates with relative paths
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * const scaffolder = new TemplateScaffolder({ toolName: 'my-cli' });
196
+ * const inspection = scaffolder.inspect({ template: 'org/repo' });
197
+ * const boilerplates = scaffolder.scanBoilerplates(inspection.templateDir);
198
+ * // Returns: [{ relativePath: 'default/module', ... }, { relativePath: 'default/workspace', ... }]
199
+ * ```
200
+ */
201
+ scanBoilerplates(templateDir, options) {
202
+ return (0, scan_boilerplates_1.scanBoilerplatesRecursive)(templateDir, options);
203
+ }
176
204
  inspectLocal(templateDir, fromPath, useBoilerplatesConfig = true) {
177
205
  const { fromPath: resolvedFromPath, resolvedTemplatePath } = this.resolveFromPath(templateDir, fromPath, useBoilerplatesConfig);
178
206
  const config = this.readBoilerplateConfig(resolvedTemplatePath);
@@ -282,12 +310,13 @@ class TemplateScaffolder {
282
310
  };
283
311
  }
284
312
  /**
285
- * Resolve the fromPath using .boilerplates.json convention.
313
+ * Resolve the fromPath using .boilerplates.json convention and recursive scanning.
286
314
  *
287
315
  * Resolution order:
288
316
  * 1. If explicit fromPath is provided and exists, use it directly
289
317
  * 2. If useBoilerplatesConfig is true and .boilerplates.json exists with a dir field, prepend it to fromPath
290
- * 3. Return the fromPath as-is
318
+ * 3. Recursively scan for boilerplates and try to match fromPath (exact match, then basename match if unambiguous)
319
+ * 4. Return the fromPath as-is (will likely fail later if path doesn't exist)
291
320
  *
292
321
  * @param templateDir - The template repository root directory
293
322
  * @param fromPath - The subdirectory path to resolve
@@ -322,6 +351,17 @@ class TemplateScaffolder {
322
351
  }
323
352
  }
324
353
  }
354
+ // Try recursive scan to find a matching boilerplate
355
+ // This handles cases like `--dir .` where the user wants to match against
356
+ // discovered boilerplates (e.g., "module" matching "default/module")
357
+ const boilerplates = (0, scan_boilerplates_1.scanBoilerplatesRecursive)(templateDir);
358
+ const match = (0, scan_boilerplates_1.findBoilerplateByPath)(boilerplates, fromPath);
359
+ if (match) {
360
+ return {
361
+ fromPath: match.relativePath,
362
+ resolvedTemplatePath: match.absolutePath,
363
+ };
364
+ }
325
365
  return {
326
366
  fromPath,
327
367
  resolvedTemplatePath: path.join(templateDir, fromPath),