create-gen-app 0.2.1 → 0.3.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.
Files changed (51) hide show
  1. package/README.md +73 -49
  2. package/cache/cache-manager.d.ts +60 -0
  3. package/cache/cache-manager.js +228 -0
  4. package/cache/types.d.ts +22 -0
  5. package/cache/types.js +2 -0
  6. package/esm/cache/cache-manager.js +191 -0
  7. package/esm/cache/types.js +1 -0
  8. package/esm/git/git-cloner.js +92 -0
  9. package/esm/git/types.js +1 -0
  10. package/esm/index.js +26 -53
  11. package/esm/licenses.js +30 -0
  12. package/esm/{extract.js → template/extract.js} +4 -80
  13. package/esm/{prompt.js → template/prompt.js} +4 -88
  14. package/esm/{replace.js → template/replace.js} +5 -23
  15. package/esm/template/templatizer.js +69 -0
  16. package/esm/template/types.js +1 -0
  17. package/esm/utils/npm-version-check.js +52 -0
  18. package/esm/utils/types.js +1 -0
  19. package/git/git-cloner.d.ts +32 -0
  20. package/git/git-cloner.js +129 -0
  21. package/git/types.d.ts +15 -0
  22. package/git/types.js +2 -0
  23. package/index.d.ts +19 -6
  24. package/index.js +27 -75
  25. package/licenses.d.ts +6 -0
  26. package/licenses.js +34 -0
  27. package/package.json +5 -5
  28. package/{extract.d.ts → template/extract.d.ts} +1 -1
  29. package/{extract.js → template/extract.js} +4 -80
  30. package/{prompt.d.ts → template/prompt.d.ts} +1 -1
  31. package/{prompt.js → template/prompt.js} +4 -88
  32. package/{replace.d.ts → template/replace.d.ts} +1 -1
  33. package/{replace.js → template/replace.js} +5 -23
  34. package/template/templatizer.d.ts +29 -0
  35. package/template/templatizer.js +106 -0
  36. package/template/types.d.ts +11 -0
  37. package/template/types.js +2 -0
  38. package/types.d.ts +0 -1
  39. package/utils/npm-version-check.d.ts +17 -0
  40. package/utils/npm-version-check.js +57 -0
  41. package/utils/types.d.ts +6 -0
  42. package/utils/types.js +2 -0
  43. package/cache.d.ts +0 -13
  44. package/cache.js +0 -76
  45. package/clone.d.ts +0 -15
  46. package/clone.js +0 -86
  47. package/esm/cache.js +0 -38
  48. package/esm/clone.js +0 -49
  49. package/esm/template-cache.js +0 -223
  50. package/template-cache.d.ts +0 -59
  51. package/template-cache.js +0 -260
package/README.md CHANGED
@@ -22,10 +22,10 @@ A TypeScript-first library for cloning template repositories, asking the user fo
22
22
 
23
23
  - Clone any Git repo (or GitHub `org/repo` shorthand) and optionally select a branch + subdirectory
24
24
  - Extract template variables from filenames and file contents using the safer `____variable____` convention
25
- - Merge auto-discovered variables with `.questions.{json,js}` (questions win, including `ignore` patterns)
25
+ - Merge auto-discovered variables with `.questions.{json,js}` (questions win)
26
26
  - Interactive prompts powered by `inquirerer`, with flexible override mapping (`argv` support) and non-TTY mode for CI
27
27
  - License scaffolding: choose from MIT, Apache-2.0, ISC, GPL-3.0, BSD-3-Clause, Unlicense, or MPL-2.0 and generate a populated `LICENSE`
28
- - Built-in template caching powered by `appstash`, so repeat runs skip `git clone` (configurable via `cache` options)
28
+ - Built-in template caching powered by `appstash`, so repeat runs skip `git clone` (configurable via `cache` options; TTL is opt-in)
29
29
 
30
30
  ## Installation
31
31
 
@@ -37,51 +37,66 @@ npm install create-gen-app
37
37
 
38
38
  ## Library Usage
39
39
 
40
- ```typescript
41
- import * as os from "os";
42
- import * as path from "path";
43
-
44
- import { createGen } from "create-gen-app";
45
-
46
- await createGen({
47
- templateUrl: "https://github.com/user/template-repo",
48
- fromBranch: "main",
49
- fromPath: "templates/module",
50
- outputDir: "./my-new-project",
51
- argv: {
52
- USERFULLNAME: "Jane Dev",
53
- USEREMAIL: "jane@example.com",
54
- MODULENAME: "awesome-module",
55
- LICENSE: "MIT",
56
- },
57
- noTty: true,
58
- cache: {
59
- // optional: override tool/baseDir (defaults to pgpm + ~/.pgpm)
60
- toolName: "pgpm",
61
- baseDir: path.join(os.tmpdir(), "create-gen-cache"),
62
- },
63
- });
64
- ```
40
+ `create-gen-app` provides a modular set of classes to handle template cloning, caching, and processing.
65
41
 
66
- ### Template Caching
42
+ ### Core Components
67
43
 
68
- `create-gen-app` caches repositories under `~/.pgpm/cache/repos/<hash>` by default (using [`appstash`](https://github.com/hyperweb-io/dev-utils/tree/main/packages/appstash)). The first run clones & stores the repo, subsequent runs re-use the cached directory.
44
+ - **CacheManager**: Handles local caching of git repositories with TTL (Time-To-Live) support.
45
+ - **GitCloner**: Handles cloning git repositories.
46
+ - **Templatizer**: Handles variable extraction, user prompting, and template generation.
69
47
 
70
- - Disable caching with `cache: false` or `cache: { enabled: false }`
71
- - Override the tool name or base directory with `cache: { toolName, baseDir }`
72
- - For tests/CI, point `baseDir` to a temporary folder so the suite does not touch the developer’s real home directory:
48
+ ### Example: Orchestration
73
49
 
74
- ```ts
75
- const tempBase = fs.mkdtempSync(path.join(os.tmpdir(), "create-gen-cache-"));
50
+ Here is how you can combine these components to create a full CLI pipeline (similar to `create-gen-app-test`):
76
51
 
77
- await createGen({
78
- ...options,
79
- cache: { baseDir: tempBase, toolName: "pgpm-test-suite" },
80
- });
52
+ ```typescript
53
+ import * as path from "path";
54
+ import { CacheManager, GitCloner, Templatizer } from "create-gen-app";
55
+
56
+ async function main() {
57
+ const repoUrl = "https://github.com/user/template-repo";
58
+ const outputDir = "./my-new-project";
59
+
60
+ // 1. Initialize components
61
+ const cacheManager = new CacheManager({
62
+ toolName: "my-cli", // ~/.my-cli/cache
63
+ // ttl is optional; omit to keep cache forever, or set (e.g., 1 week) to enable expiration
64
+ // ttl: 604800000,
65
+ });
66
+ const gitCloner = new GitCloner();
67
+ const templatizer = new Templatizer();
68
+
69
+ // 2. Resolve template path (Cache or Clone)
70
+ const normalizedUrl = gitCloner.normalizeUrl(repoUrl);
71
+ const cacheKey = cacheManager.createKey(normalizedUrl);
72
+
73
+ // Check cache
74
+ let templateDir = cacheManager.get(cacheKey);
75
+ const isExpired = cacheManager.checkExpiration(cacheKey);
76
+
77
+ if (!templateDir || isExpired) {
78
+ console.log("Cloning template...");
79
+ if (isExpired) cacheManager.clear(cacheKey);
80
+
81
+ // Clone to a temporary location managed by CacheManager
82
+ const tempDest = path.join(cacheManager.getReposDir(), cacheKey);
83
+ await gitCloner.clone(normalizedUrl, tempDest, { depth: 1 });
84
+
85
+ // Register and update cache
86
+ cacheManager.set(cacheKey, tempDest);
87
+ templateDir = tempDest;
88
+ }
89
+
90
+ // 3. Process Template
91
+ await templatizer.process(templateDir, outputDir, {
92
+ argv: {
93
+ PROJECT_NAME: "my-app",
94
+ LICENSE: "MIT"
95
+ }
96
+ });
97
+ }
81
98
  ```
82
99
 
83
- The cache directory never mutates the template, so reusing the same cached repo across many runs is safe.
84
-
85
100
  ### Template Variables
86
101
 
87
102
  Variables should be wrapped in four underscores on each side:
@@ -97,13 +112,12 @@ export const projectName = "____projectName____";
97
112
  export const author = "____fullName____";
98
113
  ```
99
114
 
100
- ### Custom Questions & Ignore Rules
115
+ ### Custom Questions
101
116
 
102
117
  Create a `.questions.json`:
103
118
 
104
119
  ```json
105
120
  {
106
- "ignore": ["__tests__", "docs/drafts"],
107
121
  "questions": [
108
122
  {
109
123
  "name": "____fullName____",
@@ -133,11 +147,21 @@ No code changes are needed; the generator discovers templates at runtime and wil
133
147
 
134
148
  ## API Overview
135
149
 
136
- - `createGen(options)` – full pipeline (clone → extract → prompt → replace)
137
- - `cloneRepo(url, { branch })` clone to a temp dir
138
- - `normalizeCacheOptions(cache)` / `prepareTemplateDirectory(...)` inspect or reuse cached template repos
139
- - `extractVariables(dir)` parse file/folder names + content for variables, load `.questions`
140
- - `promptUser(extracted, argv, noTty)` run interactive questions with override alias deduping
141
- - `replaceVariables(templateDir, outputDir, extracted, answers)` copy files, rename paths, render licenses
150
+ ### CacheManager
151
+ - `new CacheManager(config)`: Initialize with `toolName` and optional `ttl`.
152
+ - `get(key)`: Get path to cached repo if exists.
153
+ - `set(key, path)`: Register a path in the cache.
154
+ - `checkExpiration(key)`: Check if a cache entry is expired.
155
+ - `clear(key)`: Remove a specific cache entry.
156
+ - `clearAll()`: Clear all cached repos.
157
+ - When `ttl` is `undefined`, cache entries never expire. Provide a TTL (ms) only when you want automatic invalidation.
158
+ - Advanced: if you already own an appstash instance, pass `dirs` to reuse it instead of letting CacheManager create its own.
159
+
160
+ ### GitCloner
161
+ - `clone(url, dest, options)`: Clone a repo to a destination.
162
+ - `normalizeUrl(url)`: Normalize a git URL for consistency.
163
+
164
+ ### Templatizer
165
+ - `process(templateDir, outputDir, options)`: Run the full template generation pipeline (extract -> prompt -> replace).
142
166
 
143
- See `packages/create-gen-app-test/dev/README.md` for the local development helper script (`pnpm --filter create-gen-app-test dev`).
167
+ See `packages/create-gen-app-test` for a complete reference implementation.
@@ -0,0 +1,60 @@
1
+ import { AppStashResult } from 'appstash';
2
+ import { CacheManagerConfig, CacheMetadata, CacheEntryInfo } from './types';
3
+ export declare class CacheManager {
4
+ private config;
5
+ private dirs;
6
+ private reposDir;
7
+ private metadataDir;
8
+ constructor(config: CacheManagerConfig);
9
+ /**
10
+ * Public accessor for the repos cache directory path.
11
+ */
12
+ getReposDir(): string;
13
+ /**
14
+ * Public accessor for the metadata cache directory path.
15
+ */
16
+ getMetadataDir(): string;
17
+ /**
18
+ * Public accessor for resolved appstash directories.
19
+ */
20
+ getAppstashDirs(): AppStashResult;
21
+ /**
22
+ * Get cached directory path if exists and not expired
23
+ * Returns null if not cached or expired
24
+ */
25
+ get(key: string): string | null;
26
+ /**
27
+ * Store directory in cache with metadata
28
+ * Does NOT perform cloning - just registers the directory
29
+ */
30
+ set(key: string, sourcePath: string): string;
31
+ /**
32
+ * Check if cache entry is expired based on TTL
33
+ * Returns null if no metadata or not expired, returns metadata if expired
34
+ */
35
+ checkExpiration(key: string): CacheMetadata | null;
36
+ /**
37
+ * Clear specific cache entry
38
+ */
39
+ clear(key: string): void;
40
+ /**
41
+ * Clear all cache entries
42
+ */
43
+ clearAll(): void;
44
+ /**
45
+ * List all cache entries with expiration status
46
+ */
47
+ listAll(): CacheEntryInfo[];
48
+ /**
49
+ * Get metadata for a cache entry
50
+ */
51
+ getMetadata(key: string): CacheMetadata | null;
52
+ /**
53
+ * Create a cache key from identifier (e.g., git URL + branch)
54
+ */
55
+ createKey(identifier: string, variant?: string): string;
56
+ private ensureDirectories;
57
+ private isExpired;
58
+ private getCachePath;
59
+ private getMetadataPath;
60
+ }
@@ -0,0 +1,228 @@
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.CacheManager = void 0;
37
+ const crypto = __importStar(require("crypto"));
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const appstash_1 = require("appstash");
41
+ class CacheManager {
42
+ config;
43
+ dirs;
44
+ reposDir;
45
+ metadataDir;
46
+ constructor(config) {
47
+ // Validate required toolName
48
+ if (!config.toolName) {
49
+ throw new Error('CacheManager requires toolName parameter');
50
+ }
51
+ this.config = {
52
+ toolName: config.toolName,
53
+ baseDir: config.baseDir,
54
+ ttl: config.ttl,
55
+ dirs: config.dirs,
56
+ };
57
+ this.dirs =
58
+ config.dirs ??
59
+ (0, appstash_1.appstash)(this.config.toolName, {
60
+ ensure: true,
61
+ baseDir: this.config.baseDir,
62
+ });
63
+ this.reposDir = (0, appstash_1.resolve)(this.dirs, 'cache', 'repos');
64
+ this.metadataDir = (0, appstash_1.resolve)(this.dirs, 'cache', 'metadata');
65
+ this.ensureDirectories();
66
+ }
67
+ /**
68
+ * Public accessor for the repos cache directory path.
69
+ */
70
+ getReposDir() {
71
+ return this.reposDir;
72
+ }
73
+ /**
74
+ * Public accessor for the metadata cache directory path.
75
+ */
76
+ getMetadataDir() {
77
+ return this.metadataDir;
78
+ }
79
+ /**
80
+ * Public accessor for resolved appstash directories.
81
+ */
82
+ getAppstashDirs() {
83
+ return this.dirs;
84
+ }
85
+ /**
86
+ * Get cached directory path if exists and not expired
87
+ * Returns null if not cached or expired
88
+ */
89
+ get(key) {
90
+ const cachePath = this.getCachePath(key);
91
+ if (!fs.existsSync(cachePath)) {
92
+ return null;
93
+ }
94
+ // Check expiration - if expired, return null
95
+ const expired = this.checkExpiration(key);
96
+ if (expired) {
97
+ return null;
98
+ }
99
+ return cachePath;
100
+ }
101
+ /**
102
+ * Store directory in cache with metadata
103
+ * Does NOT perform cloning - just registers the directory
104
+ */
105
+ set(key, sourcePath) {
106
+ const cachePath = this.getCachePath(key);
107
+ // Write metadata with current timestamp
108
+ const metadata = {
109
+ key,
110
+ identifier: sourcePath,
111
+ lastUpdated: Date.now(),
112
+ };
113
+ fs.writeFileSync(this.getMetadataPath(key), JSON.stringify(metadata, null, 2));
114
+ return cachePath;
115
+ }
116
+ /**
117
+ * Check if cache entry is expired based on TTL
118
+ * Returns null if no metadata or not expired, returns metadata if expired
119
+ */
120
+ checkExpiration(key) {
121
+ const metadata = this.getMetadata(key);
122
+ if (!metadata) {
123
+ return null;
124
+ }
125
+ if (this.isExpired(metadata)) {
126
+ return metadata;
127
+ }
128
+ return null;
129
+ }
130
+ /**
131
+ * Clear specific cache entry
132
+ */
133
+ clear(key) {
134
+ const cachePath = this.getCachePath(key);
135
+ const metadataPath = this.getMetadataPath(key);
136
+ if (fs.existsSync(cachePath)) {
137
+ fs.rmSync(cachePath, { recursive: true, force: true });
138
+ }
139
+ if (fs.existsSync(metadataPath)) {
140
+ fs.rmSync(metadataPath, { force: true });
141
+ }
142
+ }
143
+ /**
144
+ * Clear all cache entries
145
+ */
146
+ clearAll() {
147
+ if (fs.existsSync(this.reposDir)) {
148
+ fs.rmSync(this.reposDir, { recursive: true, force: true });
149
+ fs.mkdirSync(this.reposDir, { recursive: true });
150
+ }
151
+ if (fs.existsSync(this.metadataDir)) {
152
+ fs.rmSync(this.metadataDir, { recursive: true, force: true });
153
+ fs.mkdirSync(this.metadataDir, { recursive: true });
154
+ }
155
+ }
156
+ /**
157
+ * List all cache entries with expiration status
158
+ */
159
+ listAll() {
160
+ if (!fs.existsSync(this.metadataDir)) {
161
+ return [];
162
+ }
163
+ const results = [];
164
+ const files = fs.readdirSync(this.metadataDir);
165
+ for (const file of files) {
166
+ if (!file.endsWith('.json')) {
167
+ continue;
168
+ }
169
+ const metadataPath = path.join(this.metadataDir, file);
170
+ try {
171
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
172
+ results.push({
173
+ ...metadata,
174
+ path: this.getCachePath(metadata.key),
175
+ expired: this.isExpired(metadata),
176
+ });
177
+ }
178
+ catch {
179
+ // Skip corrupted metadata
180
+ }
181
+ }
182
+ return results;
183
+ }
184
+ /**
185
+ * Get metadata for a cache entry
186
+ */
187
+ getMetadata(key) {
188
+ const metadataPath = this.getMetadataPath(key);
189
+ if (!fs.existsSync(metadataPath)) {
190
+ return null;
191
+ }
192
+ try {
193
+ return JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
194
+ }
195
+ catch {
196
+ return null;
197
+ }
198
+ }
199
+ /**
200
+ * Create a cache key from identifier (e.g., git URL + branch)
201
+ */
202
+ createKey(identifier, variant) {
203
+ const input = variant ? `${identifier}#${variant}` : identifier;
204
+ return crypto.createHash('md5').update(input).digest('hex');
205
+ }
206
+ ensureDirectories() {
207
+ if (!fs.existsSync(this.reposDir)) {
208
+ fs.mkdirSync(this.reposDir, { recursive: true });
209
+ }
210
+ if (!fs.existsSync(this.metadataDir)) {
211
+ fs.mkdirSync(this.metadataDir, { recursive: true });
212
+ }
213
+ }
214
+ isExpired(metadata) {
215
+ if (!this.config.ttl) {
216
+ return false;
217
+ }
218
+ const age = Date.now() - metadata.lastUpdated;
219
+ return age > this.config.ttl;
220
+ }
221
+ getCachePath(key) {
222
+ return path.join(this.reposDir, key);
223
+ }
224
+ getMetadataPath(key) {
225
+ return path.join(this.metadataDir, `${key}.json`);
226
+ }
227
+ }
228
+ exports.CacheManager = CacheManager;
@@ -0,0 +1,22 @@
1
+ import { AppStashResult } from 'appstash';
2
+ export interface CacheManagerConfig {
3
+ toolName: string;
4
+ baseDir?: string;
5
+ ttl?: number;
6
+ /**
7
+ * Optional pre-resolved appstash directories owned by the caller.
8
+ * When provided, CacheManager will use these instead of creating its own.
9
+ */
10
+ dirs?: AppStashResult;
11
+ }
12
+ export interface CacheMetadata {
13
+ key: string;
14
+ identifier: string;
15
+ variant?: string;
16
+ lastUpdated: number;
17
+ source?: string;
18
+ }
19
+ export interface CacheEntryInfo extends CacheMetadata {
20
+ path: string;
21
+ expired: boolean;
22
+ }
package/cache/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,191 @@
1
+ import * as crypto from 'crypto';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { appstash, resolve as resolveAppstash } from 'appstash';
5
+ export class CacheManager {
6
+ config;
7
+ dirs;
8
+ reposDir;
9
+ metadataDir;
10
+ constructor(config) {
11
+ // Validate required toolName
12
+ if (!config.toolName) {
13
+ throw new Error('CacheManager requires toolName parameter');
14
+ }
15
+ this.config = {
16
+ toolName: config.toolName,
17
+ baseDir: config.baseDir,
18
+ ttl: config.ttl,
19
+ dirs: config.dirs,
20
+ };
21
+ this.dirs =
22
+ config.dirs ??
23
+ appstash(this.config.toolName, {
24
+ ensure: true,
25
+ baseDir: this.config.baseDir,
26
+ });
27
+ this.reposDir = resolveAppstash(this.dirs, 'cache', 'repos');
28
+ this.metadataDir = resolveAppstash(this.dirs, 'cache', 'metadata');
29
+ this.ensureDirectories();
30
+ }
31
+ /**
32
+ * Public accessor for the repos cache directory path.
33
+ */
34
+ getReposDir() {
35
+ return this.reposDir;
36
+ }
37
+ /**
38
+ * Public accessor for the metadata cache directory path.
39
+ */
40
+ getMetadataDir() {
41
+ return this.metadataDir;
42
+ }
43
+ /**
44
+ * Public accessor for resolved appstash directories.
45
+ */
46
+ getAppstashDirs() {
47
+ return this.dirs;
48
+ }
49
+ /**
50
+ * Get cached directory path if exists and not expired
51
+ * Returns null if not cached or expired
52
+ */
53
+ get(key) {
54
+ const cachePath = this.getCachePath(key);
55
+ if (!fs.existsSync(cachePath)) {
56
+ return null;
57
+ }
58
+ // Check expiration - if expired, return null
59
+ const expired = this.checkExpiration(key);
60
+ if (expired) {
61
+ return null;
62
+ }
63
+ return cachePath;
64
+ }
65
+ /**
66
+ * Store directory in cache with metadata
67
+ * Does NOT perform cloning - just registers the directory
68
+ */
69
+ set(key, sourcePath) {
70
+ const cachePath = this.getCachePath(key);
71
+ // Write metadata with current timestamp
72
+ const metadata = {
73
+ key,
74
+ identifier: sourcePath,
75
+ lastUpdated: Date.now(),
76
+ };
77
+ fs.writeFileSync(this.getMetadataPath(key), JSON.stringify(metadata, null, 2));
78
+ return cachePath;
79
+ }
80
+ /**
81
+ * Check if cache entry is expired based on TTL
82
+ * Returns null if no metadata or not expired, returns metadata if expired
83
+ */
84
+ checkExpiration(key) {
85
+ const metadata = this.getMetadata(key);
86
+ if (!metadata) {
87
+ return null;
88
+ }
89
+ if (this.isExpired(metadata)) {
90
+ return metadata;
91
+ }
92
+ return null;
93
+ }
94
+ /**
95
+ * Clear specific cache entry
96
+ */
97
+ clear(key) {
98
+ const cachePath = this.getCachePath(key);
99
+ const metadataPath = this.getMetadataPath(key);
100
+ if (fs.existsSync(cachePath)) {
101
+ fs.rmSync(cachePath, { recursive: true, force: true });
102
+ }
103
+ if (fs.existsSync(metadataPath)) {
104
+ fs.rmSync(metadataPath, { force: true });
105
+ }
106
+ }
107
+ /**
108
+ * Clear all cache entries
109
+ */
110
+ clearAll() {
111
+ if (fs.existsSync(this.reposDir)) {
112
+ fs.rmSync(this.reposDir, { recursive: true, force: true });
113
+ fs.mkdirSync(this.reposDir, { recursive: true });
114
+ }
115
+ if (fs.existsSync(this.metadataDir)) {
116
+ fs.rmSync(this.metadataDir, { recursive: true, force: true });
117
+ fs.mkdirSync(this.metadataDir, { recursive: true });
118
+ }
119
+ }
120
+ /**
121
+ * List all cache entries with expiration status
122
+ */
123
+ listAll() {
124
+ if (!fs.existsSync(this.metadataDir)) {
125
+ return [];
126
+ }
127
+ const results = [];
128
+ const files = fs.readdirSync(this.metadataDir);
129
+ for (const file of files) {
130
+ if (!file.endsWith('.json')) {
131
+ continue;
132
+ }
133
+ const metadataPath = path.join(this.metadataDir, file);
134
+ try {
135
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
136
+ results.push({
137
+ ...metadata,
138
+ path: this.getCachePath(metadata.key),
139
+ expired: this.isExpired(metadata),
140
+ });
141
+ }
142
+ catch {
143
+ // Skip corrupted metadata
144
+ }
145
+ }
146
+ return results;
147
+ }
148
+ /**
149
+ * Get metadata for a cache entry
150
+ */
151
+ getMetadata(key) {
152
+ const metadataPath = this.getMetadataPath(key);
153
+ if (!fs.existsSync(metadataPath)) {
154
+ return null;
155
+ }
156
+ try {
157
+ return JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ /**
164
+ * Create a cache key from identifier (e.g., git URL + branch)
165
+ */
166
+ createKey(identifier, variant) {
167
+ const input = variant ? `${identifier}#${variant}` : identifier;
168
+ return crypto.createHash('md5').update(input).digest('hex');
169
+ }
170
+ ensureDirectories() {
171
+ if (!fs.existsSync(this.reposDir)) {
172
+ fs.mkdirSync(this.reposDir, { recursive: true });
173
+ }
174
+ if (!fs.existsSync(this.metadataDir)) {
175
+ fs.mkdirSync(this.metadataDir, { recursive: true });
176
+ }
177
+ }
178
+ isExpired(metadata) {
179
+ if (!this.config.ttl) {
180
+ return false;
181
+ }
182
+ const age = Date.now() - metadata.lastUpdated;
183
+ return age > this.config.ttl;
184
+ }
185
+ getCachePath(key) {
186
+ return path.join(this.reposDir, key);
187
+ }
188
+ getMetadataPath(key) {
189
+ return path.join(this.metadataDir, `${key}.json`);
190
+ }
191
+ }
@@ -0,0 +1 @@
1
+ export {};