@yolo-labs/core-templates 1.0.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.
- package/dist/index.d.ts +100 -0
- package/dist/index.js +236 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { TemplateProvider, TemplateDefinition } from '@yolo-labs/core-types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Discovers and scaffolds templates from a local directory tree.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Recursively scans for `template.yaml` / `template.yml` manifest files.
|
|
8
|
+
* Each manifest defines a template's ID, name, variables, and tags.
|
|
9
|
+
* Call {@link load} before querying. Scaffolding replaces `{{var}}`
|
|
10
|
+
* placeholders in both file paths and file contents.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const provider = new FileSystemTemplateProvider('./templates');
|
|
15
|
+
* await provider.load();
|
|
16
|
+
* const files = await provider.scaffold('react-app', { name: 'MyApp' });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
declare class FileSystemTemplateProvider implements TemplateProvider {
|
|
20
|
+
private directory;
|
|
21
|
+
private templates;
|
|
22
|
+
private templateDirs;
|
|
23
|
+
constructor(directory: string);
|
|
24
|
+
/**
|
|
25
|
+
* Scans the directory tree for template manifests and loads all discovered templates.
|
|
26
|
+
*
|
|
27
|
+
* @remarks
|
|
28
|
+
* Must be called before `list`, `get`, or `scaffold`. Subsequent calls
|
|
29
|
+
* reload all templates from disk.
|
|
30
|
+
*/
|
|
31
|
+
load(): Promise<void>;
|
|
32
|
+
private scanDirectory;
|
|
33
|
+
private loadTemplate;
|
|
34
|
+
private readTemplateFiles;
|
|
35
|
+
private collectFiles;
|
|
36
|
+
/** Returns all loaded template definitions. */
|
|
37
|
+
list(): Promise<TemplateDefinition[]>;
|
|
38
|
+
/** Retrieves a template definition by its unique ID. */
|
|
39
|
+
get(id: string): Promise<TemplateDefinition | undefined>;
|
|
40
|
+
/**
|
|
41
|
+
* Produces a file map from a template, replacing `{{var}}` placeholders with values.
|
|
42
|
+
*
|
|
43
|
+
* @param id - The template ID to scaffold.
|
|
44
|
+
* @param variables - Key-value pairs for variable substitution.
|
|
45
|
+
* @returns A map of file paths to their rendered contents.
|
|
46
|
+
* @throws Error if the template ID is not found.
|
|
47
|
+
*/
|
|
48
|
+
scaffold(id: string, variables: Record<string, string>): Promise<Record<string, string>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Configuration options for the Git-based template provider. */
|
|
52
|
+
interface GitTemplateProviderOptions {
|
|
53
|
+
/** Directory used to cache cloned repositories (defaults to OS temp dir). */
|
|
54
|
+
cacheDir?: string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Clones git repositories on demand and provides templates from them.
|
|
58
|
+
*
|
|
59
|
+
* @remarks
|
|
60
|
+
* Uses shallow clones (`--depth 1`) for efficiency. Cloned repos are cached
|
|
61
|
+
* in memory by URL+ref. Delegates template discovery and scaffolding to
|
|
62
|
+
* {@link FileSystemTemplateProvider}. Call {@link loadFromRepo} to add
|
|
63
|
+
* repositories before querying.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* const provider = new GitTemplateProvider();
|
|
68
|
+
* await provider.loadFromRepo('https://github.com/org/templates.git', 'main');
|
|
69
|
+
* const templates = await provider.list();
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare class GitTemplateProvider implements TemplateProvider {
|
|
73
|
+
private cache;
|
|
74
|
+
private cacheDir;
|
|
75
|
+
constructor(options?: GitTemplateProviderOptions);
|
|
76
|
+
private getProvider;
|
|
77
|
+
/**
|
|
78
|
+
* Clones a git repository (or uses the cache) and loads its templates.
|
|
79
|
+
*
|
|
80
|
+
* @param repoUrl - The git repository URL to clone.
|
|
81
|
+
* @param ref - The branch or tag to checkout (defaults to `'main'`).
|
|
82
|
+
*/
|
|
83
|
+
loadFromRepo(repoUrl: string, ref?: string): Promise<void>;
|
|
84
|
+
/** Returns all template definitions from all cached repositories. */
|
|
85
|
+
list(): Promise<TemplateDefinition[]>;
|
|
86
|
+
/** Retrieves a template definition by ID, searching all cached repositories. */
|
|
87
|
+
get(id: string): Promise<TemplateDefinition | undefined>;
|
|
88
|
+
/** Scaffolds a template by ID, replacing placeholders with the given variables. */
|
|
89
|
+
scaffold(id: string, variables: Record<string, string>): Promise<Record<string, string>>;
|
|
90
|
+
/**
|
|
91
|
+
* Clears the in-memory cache of cloned repositories.
|
|
92
|
+
*
|
|
93
|
+
* @remarks
|
|
94
|
+
* Does not delete the cloned directories from disk. Use this when you
|
|
95
|
+
* want to force a fresh clone on the next {@link loadFromRepo} call.
|
|
96
|
+
*/
|
|
97
|
+
clearCache(): Promise<void>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { FileSystemTemplateProvider, GitTemplateProvider, type GitTemplateProviderOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// src/fs-template-provider.ts
|
|
2
|
+
import { readdir, readFile } from "fs/promises";
|
|
3
|
+
import { join, relative } from "path";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
var FileSystemTemplateProvider = class {
|
|
6
|
+
directory;
|
|
7
|
+
templates = /* @__PURE__ */ new Map();
|
|
8
|
+
templateDirs = /* @__PURE__ */ new Map();
|
|
9
|
+
constructor(directory) {
|
|
10
|
+
this.directory = directory;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Scans the directory tree for template manifests and loads all discovered templates.
|
|
14
|
+
*
|
|
15
|
+
* @remarks
|
|
16
|
+
* Must be called before `list`, `get`, or `scaffold`. Subsequent calls
|
|
17
|
+
* reload all templates from disk.
|
|
18
|
+
*/
|
|
19
|
+
async load() {
|
|
20
|
+
this.templates.clear();
|
|
21
|
+
this.templateDirs.clear();
|
|
22
|
+
await this.scanDirectory(this.directory);
|
|
23
|
+
}
|
|
24
|
+
// 44a: Template discovery — scan for template.yaml manifest files
|
|
25
|
+
async scanDirectory(dir) {
|
|
26
|
+
let entries;
|
|
27
|
+
try {
|
|
28
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
29
|
+
} catch {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const hasManifest = entries.some(
|
|
33
|
+
(e) => e.isFile() && (e.name === "template.yaml" || e.name === "template.yml")
|
|
34
|
+
);
|
|
35
|
+
if (hasManifest) {
|
|
36
|
+
await this.loadTemplate(dir);
|
|
37
|
+
}
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".git") {
|
|
40
|
+
await this.scanDirectory(join(dir, entry.name));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async loadTemplate(dir) {
|
|
45
|
+
const manifestPath = join(dir, "template.yaml");
|
|
46
|
+
let manifestContent;
|
|
47
|
+
try {
|
|
48
|
+
manifestContent = await readFile(manifestPath, "utf-8");
|
|
49
|
+
} catch {
|
|
50
|
+
try {
|
|
51
|
+
manifestContent = await readFile(join(dir, "template.yml"), "utf-8");
|
|
52
|
+
} catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const manifest = parseYaml(manifestContent);
|
|
58
|
+
if (!manifest.id || !manifest.name) return;
|
|
59
|
+
const variables = (manifest.variables ?? []).map((v) => ({
|
|
60
|
+
name: v.name,
|
|
61
|
+
description: v.description,
|
|
62
|
+
default: v.default,
|
|
63
|
+
required: v.required ?? false
|
|
64
|
+
}));
|
|
65
|
+
const files = await this.readTemplateFiles(dir);
|
|
66
|
+
const definition = {
|
|
67
|
+
id: manifest.id,
|
|
68
|
+
name: manifest.name,
|
|
69
|
+
description: manifest.description ?? "",
|
|
70
|
+
files,
|
|
71
|
+
variables,
|
|
72
|
+
tags: manifest.tags,
|
|
73
|
+
metadata: manifest.metadata
|
|
74
|
+
};
|
|
75
|
+
this.templates.set(manifest.id, definition);
|
|
76
|
+
this.templateDirs.set(manifest.id, dir);
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async readTemplateFiles(dir) {
|
|
81
|
+
const files = {};
|
|
82
|
+
await this.collectFiles(dir, dir, files);
|
|
83
|
+
return files;
|
|
84
|
+
}
|
|
85
|
+
async collectFiles(baseDir, currentDir, files) {
|
|
86
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const fullPath = join(currentDir, entry.name);
|
|
89
|
+
const relativePath = relative(baseDir, fullPath);
|
|
90
|
+
if (entry.isFile() && entry.name !== "template.yaml" && entry.name !== "template.yml") {
|
|
91
|
+
try {
|
|
92
|
+
files[relativePath] = await readFile(fullPath, "utf-8");
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
} else if (entry.isDirectory() && entry.name !== "node_modules") {
|
|
96
|
+
await this.collectFiles(baseDir, fullPath, files);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Returns all loaded template definitions. */
|
|
101
|
+
async list() {
|
|
102
|
+
return Array.from(this.templates.values());
|
|
103
|
+
}
|
|
104
|
+
/** Retrieves a template definition by its unique ID. */
|
|
105
|
+
async get(id) {
|
|
106
|
+
return this.templates.get(id);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Produces a file map from a template, replacing `{{var}}` placeholders with values.
|
|
110
|
+
*
|
|
111
|
+
* @param id - The template ID to scaffold.
|
|
112
|
+
* @param variables - Key-value pairs for variable substitution.
|
|
113
|
+
* @returns A map of file paths to their rendered contents.
|
|
114
|
+
* @throws Error if the template ID is not found.
|
|
115
|
+
*/
|
|
116
|
+
async scaffold(id, variables) {
|
|
117
|
+
const template = this.templates.get(id);
|
|
118
|
+
if (!template) {
|
|
119
|
+
throw new Error(`Template not found: ${id}`);
|
|
120
|
+
}
|
|
121
|
+
const result = {};
|
|
122
|
+
for (const [path, content] of Object.entries(template.files)) {
|
|
123
|
+
let resolvedPath = path;
|
|
124
|
+
let resolvedContent = content;
|
|
125
|
+
for (const [varName, varValue] of Object.entries(variables)) {
|
|
126
|
+
const pattern = new RegExp(`\\{\\{${varName}\\}\\}`, "g");
|
|
127
|
+
resolvedPath = resolvedPath.replace(pattern, varValue);
|
|
128
|
+
resolvedContent = resolvedContent.replace(pattern, varValue);
|
|
129
|
+
}
|
|
130
|
+
for (const v of template.variables) {
|
|
131
|
+
if (!(v.name in variables) && v.default !== void 0) {
|
|
132
|
+
const pattern = new RegExp(`\\{\\{${v.name}\\}\\}`, "g");
|
|
133
|
+
resolvedPath = resolvedPath.replace(pattern, v.default);
|
|
134
|
+
resolvedContent = resolvedContent.replace(pattern, v.default);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
result[resolvedPath] = resolvedContent;
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// src/git-template-provider.ts
|
|
144
|
+
import { execFile } from "child_process";
|
|
145
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
146
|
+
import { join as join2 } from "path";
|
|
147
|
+
import { tmpdir } from "os";
|
|
148
|
+
import { promisify } from "util";
|
|
149
|
+
var execFileAsync = promisify(execFile);
|
|
150
|
+
var GitTemplateProvider = class {
|
|
151
|
+
// 45b: Cache cloned repos by URL + ref
|
|
152
|
+
cache = /* @__PURE__ */ new Map();
|
|
153
|
+
cacheDir;
|
|
154
|
+
constructor(options = {}) {
|
|
155
|
+
this.cacheDir = options.cacheDir ?? join2(tmpdir(), "yolo-git-templates");
|
|
156
|
+
}
|
|
157
|
+
// 45a: Clone to temp directory, read manifest, scaffold
|
|
158
|
+
async getProvider(repoUrl, ref = "main") {
|
|
159
|
+
const cacheKey = `${repoUrl}@${ref}`;
|
|
160
|
+
const cached = this.cache.get(cacheKey);
|
|
161
|
+
if (cached) {
|
|
162
|
+
return cached.provider;
|
|
163
|
+
}
|
|
164
|
+
const tempDir = await mkdtemp(join2(this.cacheDir, "repo-"));
|
|
165
|
+
try {
|
|
166
|
+
await execFileAsync("git", ["clone", "--depth", "1", "--branch", ref, repoUrl, tempDir], {
|
|
167
|
+
timeout: 3e4
|
|
168
|
+
});
|
|
169
|
+
} catch {
|
|
170
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
171
|
+
const retryDir = await mkdtemp(join2(this.cacheDir, "repo-"));
|
|
172
|
+
await execFileAsync("git", ["clone", "--depth", "1", repoUrl, retryDir], {
|
|
173
|
+
timeout: 3e4
|
|
174
|
+
});
|
|
175
|
+
const provider2 = new FileSystemTemplateProvider(retryDir);
|
|
176
|
+
await provider2.load();
|
|
177
|
+
this.cache.set(cacheKey, { provider: provider2, clonedAt: Date.now(), ref });
|
|
178
|
+
return provider2;
|
|
179
|
+
}
|
|
180
|
+
const provider = new FileSystemTemplateProvider(tempDir);
|
|
181
|
+
await provider.load();
|
|
182
|
+
this.cache.set(cacheKey, { provider, clonedAt: Date.now(), ref });
|
|
183
|
+
return provider;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Clones a git repository (or uses the cache) and loads its templates.
|
|
187
|
+
*
|
|
188
|
+
* @param repoUrl - The git repository URL to clone.
|
|
189
|
+
* @param ref - The branch or tag to checkout (defaults to `'main'`).
|
|
190
|
+
*/
|
|
191
|
+
async loadFromRepo(repoUrl, ref) {
|
|
192
|
+
await this.getProvider(repoUrl, ref);
|
|
193
|
+
}
|
|
194
|
+
/** Returns all template definitions from all cached repositories. */
|
|
195
|
+
async list() {
|
|
196
|
+
const allTemplates = [];
|
|
197
|
+
for (const entry of this.cache.values()) {
|
|
198
|
+
const templates = await entry.provider.list();
|
|
199
|
+
allTemplates.push(...templates);
|
|
200
|
+
}
|
|
201
|
+
return allTemplates;
|
|
202
|
+
}
|
|
203
|
+
/** Retrieves a template definition by ID, searching all cached repositories. */
|
|
204
|
+
async get(id) {
|
|
205
|
+
for (const entry of this.cache.values()) {
|
|
206
|
+
const template = await entry.provider.get(id);
|
|
207
|
+
if (template) return template;
|
|
208
|
+
}
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
/** Scaffolds a template by ID, replacing placeholders with the given variables. */
|
|
212
|
+
async scaffold(id, variables) {
|
|
213
|
+
for (const entry of this.cache.values()) {
|
|
214
|
+
const template = await entry.provider.get(id);
|
|
215
|
+
if (template) {
|
|
216
|
+
return entry.provider.scaffold(id, variables);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
throw new Error(`Template not found: ${id}`);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Clears the in-memory cache of cloned repositories.
|
|
223
|
+
*
|
|
224
|
+
* @remarks
|
|
225
|
+
* Does not delete the cloned directories from disk. Use this when you
|
|
226
|
+
* want to force a fresh clone on the next {@link loadFromRepo} call.
|
|
227
|
+
*/
|
|
228
|
+
async clearCache() {
|
|
229
|
+
this.cache.clear();
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
export {
|
|
233
|
+
FileSystemTemplateProvider,
|
|
234
|
+
GitTemplateProvider
|
|
235
|
+
};
|
|
236
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fs-template-provider.ts","../src/git-template-provider.ts"],"sourcesContent":["// Task 44: FileSystemTemplateProvider — load templates from a local directory\n\nimport { readdir, readFile } from 'node:fs/promises';\nimport { join, relative } from 'node:path';\nimport { parse as parseYaml } from 'yaml';\nimport type { TemplateDefinition, TemplateProvider, TemplateVariable } from '@yolo-labs/core-types';\n\ninterface TemplateManifest {\n id: string;\n name: string;\n description: string;\n variables?: Array<{\n name: string;\n description?: string;\n default?: string;\n required?: boolean;\n }>;\n tags?: string[];\n metadata?: Record<string, unknown>;\n}\n\n/**\n * Discovers and scaffolds templates from a local directory tree.\n *\n * @remarks\n * Recursively scans for `template.yaml` / `template.yml` manifest files.\n * Each manifest defines a template's ID, name, variables, and tags.\n * Call {@link load} before querying. Scaffolding replaces `{{var}}`\n * placeholders in both file paths and file contents.\n *\n * @example\n * ```ts\n * const provider = new FileSystemTemplateProvider('./templates');\n * await provider.load();\n * const files = await provider.scaffold('react-app', { name: 'MyApp' });\n * ```\n */\nexport class FileSystemTemplateProvider implements TemplateProvider {\n private directory: string;\n private templates = new Map<string, TemplateDefinition>();\n private templateDirs = new Map<string, string>();\n\n constructor(directory: string) {\n this.directory = directory;\n }\n\n /**\n * Scans the directory tree for template manifests and loads all discovered templates.\n *\n * @remarks\n * Must be called before `list`, `get`, or `scaffold`. Subsequent calls\n * reload all templates from disk.\n */\n async load(): Promise<void> {\n this.templates.clear();\n this.templateDirs.clear();\n await this.scanDirectory(this.directory);\n }\n\n // 44a: Template discovery — scan for template.yaml manifest files\n private async scanDirectory(dir: string): Promise<void> {\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return;\n }\n\n // Check if this directory has a template.yaml manifest\n const hasManifest = entries.some(\n (e) => e.isFile() && (e.name === 'template.yaml' || e.name === 'template.yml'),\n );\n\n if (hasManifest) {\n await this.loadTemplate(dir);\n }\n\n // Recursively scan subdirectories\n for (const entry of entries) {\n if (entry.isDirectory() && entry.name !== 'node_modules' && entry.name !== '.git') {\n await this.scanDirectory(join(dir, entry.name));\n }\n }\n }\n\n private async loadTemplate(dir: string): Promise<void> {\n const manifestPath = join(dir, 'template.yaml');\n let manifestContent: string;\n\n try {\n manifestContent = await readFile(manifestPath, 'utf-8');\n } catch {\n try {\n manifestContent = await readFile(join(dir, 'template.yml'), 'utf-8');\n } catch {\n return;\n }\n }\n\n try {\n // 44b: Parse manifest\n const manifest = parseYaml(manifestContent) as TemplateManifest;\n if (!manifest.id || !manifest.name) return;\n\n const variables: TemplateVariable[] = (manifest.variables ?? []).map((v) => ({\n name: v.name,\n description: v.description,\n default: v.default,\n required: v.required ?? false,\n }));\n\n // Read all files in the template directory (except manifest)\n const files = await this.readTemplateFiles(dir);\n\n const definition: TemplateDefinition = {\n id: manifest.id,\n name: manifest.name,\n description: manifest.description ?? '',\n files,\n variables,\n tags: manifest.tags,\n metadata: manifest.metadata,\n };\n\n this.templates.set(manifest.id, definition);\n this.templateDirs.set(manifest.id, dir);\n } catch {\n // Skip templates with invalid manifests\n }\n }\n\n private async readTemplateFiles(dir: string): Promise<Record<string, string>> {\n const files: Record<string, string> = {};\n await this.collectFiles(dir, dir, files);\n return files;\n }\n\n private async collectFiles(\n baseDir: string,\n currentDir: string,\n files: Record<string, string>,\n ): Promise<void> {\n const entries = await readdir(currentDir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(currentDir, entry.name);\n const relativePath = relative(baseDir, fullPath);\n\n if (entry.isFile() && entry.name !== 'template.yaml' && entry.name !== 'template.yml') {\n try {\n files[relativePath] = await readFile(fullPath, 'utf-8');\n } catch {\n // Skip unreadable files\n }\n } else if (entry.isDirectory() && entry.name !== 'node_modules') {\n await this.collectFiles(baseDir, fullPath, files);\n }\n }\n }\n\n /** Returns all loaded template definitions. */\n async list(): Promise<TemplateDefinition[]> {\n return Array.from(this.templates.values());\n }\n\n /** Retrieves a template definition by its unique ID. */\n async get(id: string): Promise<TemplateDefinition | undefined> {\n return this.templates.get(id);\n }\n\n /**\n * Produces a file map from a template, replacing `{{var}}` placeholders with values.\n *\n * @param id - The template ID to scaffold.\n * @param variables - Key-value pairs for variable substitution.\n * @returns A map of file paths to their rendered contents.\n * @throws Error if the template ID is not found.\n */\n async scaffold(\n id: string,\n variables: Record<string, string>,\n ): Promise<Record<string, string>> {\n const template = this.templates.get(id);\n if (!template) {\n throw new Error(`Template not found: ${id}`);\n }\n\n const result: Record<string, string> = {};\n\n for (const [path, content] of Object.entries(template.files)) {\n // Replace variables in both file path and content\n let resolvedPath = path;\n let resolvedContent = content;\n\n for (const [varName, varValue] of Object.entries(variables)) {\n const pattern = new RegExp(`\\\\{\\\\{${varName}\\\\}\\\\}`, 'g');\n resolvedPath = resolvedPath.replace(pattern, varValue);\n resolvedContent = resolvedContent.replace(pattern, varValue);\n }\n\n // Apply defaults for missing variables\n for (const v of template.variables) {\n if (!(v.name in variables) && v.default !== undefined) {\n const pattern = new RegExp(`\\\\{\\\\{${v.name}\\\\}\\\\}`, 'g');\n resolvedPath = resolvedPath.replace(pattern, v.default);\n resolvedContent = resolvedContent.replace(pattern, v.default);\n }\n }\n\n result[resolvedPath] = resolvedContent;\n }\n\n return result;\n }\n}\n","// Task 45: GitTemplateProvider — clone template from a git repo on demand\n\nimport { execFile } from 'node:child_process';\nimport { mkdtemp, rm } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { tmpdir } from 'node:os';\nimport { promisify } from 'node:util';\nimport type { TemplateDefinition, TemplateProvider } from '@yolo-labs/core-types';\nimport { FileSystemTemplateProvider } from './fs-template-provider.js';\n\nconst execFileAsync = promisify(execFile);\n\n/** Configuration options for the Git-based template provider. */\nexport interface GitTemplateProviderOptions {\n /** Directory used to cache cloned repositories (defaults to OS temp dir). */\n cacheDir?: string;\n}\n\ninterface CacheEntry {\n provider: FileSystemTemplateProvider;\n clonedAt: number;\n ref: string;\n}\n\n/**\n * Clones git repositories on demand and provides templates from them.\n *\n * @remarks\n * Uses shallow clones (`--depth 1`) for efficiency. Cloned repos are cached\n * in memory by URL+ref. Delegates template discovery and scaffolding to\n * {@link FileSystemTemplateProvider}. Call {@link loadFromRepo} to add\n * repositories before querying.\n *\n * @example\n * ```ts\n * const provider = new GitTemplateProvider();\n * await provider.loadFromRepo('https://github.com/org/templates.git', 'main');\n * const templates = await provider.list();\n * ```\n */\nexport class GitTemplateProvider implements TemplateProvider {\n // 45b: Cache cloned repos by URL + ref\n private cache = new Map<string, CacheEntry>();\n private cacheDir: string;\n\n constructor(options: GitTemplateProviderOptions = {}) {\n this.cacheDir = options.cacheDir ?? join(tmpdir(), 'yolo-git-templates');\n }\n\n // 45a: Clone to temp directory, read manifest, scaffold\n private async getProvider(\n repoUrl: string,\n ref = 'main',\n ): Promise<FileSystemTemplateProvider> {\n const cacheKey = `${repoUrl}@${ref}`;\n const cached = this.cache.get(cacheKey);\n if (cached) {\n return cached.provider;\n }\n\n const tempDir = await mkdtemp(join(this.cacheDir, 'repo-'));\n\n try {\n await execFileAsync('git', ['clone', '--depth', '1', '--branch', ref, repoUrl, tempDir], {\n timeout: 30000,\n });\n } catch {\n // Try without --branch for default branch\n await rm(tempDir, { recursive: true, force: true });\n const retryDir = await mkdtemp(join(this.cacheDir, 'repo-'));\n await execFileAsync('git', ['clone', '--depth', '1', repoUrl, retryDir], {\n timeout: 30000,\n });\n const provider = new FileSystemTemplateProvider(retryDir);\n await provider.load();\n this.cache.set(cacheKey, { provider, clonedAt: Date.now(), ref });\n return provider;\n }\n\n const provider = new FileSystemTemplateProvider(tempDir);\n await provider.load();\n this.cache.set(cacheKey, { provider, clonedAt: Date.now(), ref });\n return provider;\n }\n\n /**\n * Clones a git repository (or uses the cache) and loads its templates.\n *\n * @param repoUrl - The git repository URL to clone.\n * @param ref - The branch or tag to checkout (defaults to `'main'`).\n */\n async loadFromRepo(repoUrl: string, ref?: string): Promise<void> {\n await this.getProvider(repoUrl, ref);\n }\n\n /** Returns all template definitions from all cached repositories. */\n async list(): Promise<TemplateDefinition[]> {\n const allTemplates: TemplateDefinition[] = [];\n for (const entry of this.cache.values()) {\n const templates = await entry.provider.list();\n allTemplates.push(...templates);\n }\n return allTemplates;\n }\n\n /** Retrieves a template definition by ID, searching all cached repositories. */\n async get(id: string): Promise<TemplateDefinition | undefined> {\n for (const entry of this.cache.values()) {\n const template = await entry.provider.get(id);\n if (template) return template;\n }\n return undefined;\n }\n\n /** Scaffolds a template by ID, replacing placeholders with the given variables. */\n async scaffold(\n id: string,\n variables: Record<string, string>,\n ): Promise<Record<string, string>> {\n for (const entry of this.cache.values()) {\n const template = await entry.provider.get(id);\n if (template) {\n return entry.provider.scaffold(id, variables);\n }\n }\n throw new Error(`Template not found: ${id}`);\n }\n\n /**\n * Clears the in-memory cache of cloned repositories.\n *\n * @remarks\n * Does not delete the cloned directories from disk. Use this when you\n * want to force a fresh clone on the next {@link loadFromRepo} call.\n */\n async clearCache(): Promise<void> {\n this.cache.clear();\n }\n}\n"],"mappings":"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","names":["join","join","provider"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yolo-labs/core-templates",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"import": "./dist/index.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"yaml": "^2.8.2",
|
|
18
|
+
"@yolo-labs/core-types": "1.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"tsup": "^8.0.0",
|
|
22
|
+
"typescript": "^5.5.0"
|
|
23
|
+
},
|
|
24
|
+
"description": "Template providers (filesystem, git) and scaffolding",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/yolo-labs-hq/monorepo.git",
|
|
29
|
+
"directory": "yolo-core/packages/templates"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public",
|
|
33
|
+
"registry": "https://registry.npmjs.org/"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/yolo-labs-hq/monorepo/tree/main/yolo-core#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/yolo-labs-hq/monorepo/issues"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"clean": "rm -rf dist *.tsbuildinfo"
|
|
42
|
+
}
|
|
43
|
+
}
|