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.
- package/README.md +73 -49
- package/cache/cache-manager.d.ts +60 -0
- package/cache/cache-manager.js +228 -0
- package/cache/types.d.ts +22 -0
- package/cache/types.js +2 -0
- package/esm/cache/cache-manager.js +191 -0
- package/esm/cache/types.js +1 -0
- package/esm/git/git-cloner.js +92 -0
- package/esm/git/types.js +1 -0
- package/esm/index.js +26 -53
- package/esm/licenses.js +30 -0
- package/esm/{extract.js → template/extract.js} +4 -80
- package/esm/{prompt.js → template/prompt.js} +4 -88
- package/esm/{replace.js → template/replace.js} +5 -23
- package/esm/template/templatizer.js +69 -0
- package/esm/template/types.js +1 -0
- package/esm/utils/npm-version-check.js +52 -0
- package/esm/utils/types.js +1 -0
- package/git/git-cloner.d.ts +32 -0
- package/git/git-cloner.js +129 -0
- package/git/types.d.ts +15 -0
- package/git/types.js +2 -0
- package/index.d.ts +19 -6
- package/index.js +27 -75
- package/licenses.d.ts +6 -0
- package/licenses.js +34 -0
- package/package.json +5 -5
- package/{extract.d.ts → template/extract.d.ts} +1 -1
- package/{extract.js → template/extract.js} +4 -80
- package/{prompt.d.ts → template/prompt.d.ts} +1 -1
- package/{prompt.js → template/prompt.js} +4 -88
- package/{replace.d.ts → template/replace.d.ts} +1 -1
- package/{replace.js → template/replace.js} +5 -23
- package/template/templatizer.d.ts +29 -0
- package/template/templatizer.js +106 -0
- package/template/types.d.ts +11 -0
- package/template/types.js +2 -0
- package/types.d.ts +0 -1
- package/utils/npm-version-check.d.ts +17 -0
- package/utils/npm-version-check.js +57 -0
- package/utils/types.d.ts +6 -0
- package/utils/types.js +2 -0
- package/cache.d.ts +0 -13
- package/cache.js +0 -76
- package/clone.d.ts +0 -15
- package/clone.js +0 -86
- package/esm/cache.js +0 -38
- package/esm/clone.js +0 -49
- package/esm/template-cache.js +0 -223
- package/template-cache.d.ts +0 -59
- 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
|
|
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
|
-
|
|
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
|
-
###
|
|
42
|
+
### Core Components
|
|
67
43
|
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
137
|
-
- `
|
|
138
|
-
- `
|
|
139
|
-
- `
|
|
140
|
-
- `
|
|
141
|
-
- `
|
|
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
|
|
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;
|
package/cache/types.d.ts
ADDED
|
@@ -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,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 {};
|