Skip to content

Commit ae4b130

Browse files
committed
Allow optional TTL, expose cache dirs, and read CLI package name dynamically
- add TTL opt-out flow to create-gen-app-test (new --ttl/--no-ttl flags) and avoid forcing 1-week default - read package name from package.json (fallback @launchql/cli) for version checks instead of hard-coding create-gen-app - expose CacheManager appstash dirs via getters and allow caller-provided dirs to own appstash lifecycle - update callers/tests to use getReposDir() and refresh README to document optional TTL and dirs injection
1 parent fad1d13 commit ae4b130

File tree

7 files changed

+86
-21
lines changed

7 files changed

+86
-21
lines changed

packages/create-gen-app-test/src/__tests__/cached-template.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe('cached template integration tests', () => {
5858
const cacheKey = cacheManager.createKey(normalizedUrl);
5959

6060
// Clone to cache directory
61-
const cachePath = path.join(cacheManager['reposDir'], cacheKey);
61+
const cachePath = path.join(cacheManager.getReposDir(), cacheKey);
6262
gitCloner.clone(normalizedUrl, cachePath, { depth: 1 });
6363

6464
// Register in cache manager

packages/create-gen-app-test/src/cli.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ const DEFAULT_PATH = ".";
1414
const DEFAULT_OUTPUT_FALLBACK = "create-gen-app-output";
1515
const DEFAULT_TOOL_NAME = "create-gen-app-test";
1616
const DEFAULT_TTL = 604800000; // 1 week
17+
const DEFAULT_TTL_DAYS = DEFAULT_TTL / (24 * 60 * 60 * 1000);
1718

1819
// Import package.json for version
1920
import * as createGenPackageJson from "create-gen-app/package.json";
21+
const PACKAGE_NAME = createGenPackageJson.name ?? "@launchql/cli";
2022
const PACKAGE_VERSION = createGenPackageJson.version ?? "0.0.0";
2123

2224
const RESERVED_ARG_KEYS = new Set([
@@ -41,6 +43,8 @@ const RESERVED_ARG_KEYS = new Set([
4143
"n",
4244
"clear-cache",
4345
"c",
46+
"ttl",
47+
"no-ttl",
4448
]);
4549

4650
export interface CliResult {
@@ -63,9 +67,10 @@ export async function runCli(
6367
v: "version",
6468
n: "no-tty",
6569
c: "clear-cache",
70+
// no alias for ttl to keep it explicit
6671
},
67-
string: ["repo", "branch", "path", "template", "output"],
68-
boolean: ["force", "help", "version", "no-tty", "clear-cache"],
72+
string: ["repo", "branch", "path", "template", "output", "ttl"],
73+
boolean: ["force", "help", "version", "no-tty", "clear-cache", "no-ttl"],
6974
default: {
7075
repo: DEFAULT_REPO,
7176
path: DEFAULT_PATH,
@@ -84,21 +89,23 @@ export async function runCli(
8489

8590
// Check for updates
8691
try {
87-
const versionCheck = await checkNpmVersion("create-gen-app", PACKAGE_VERSION);
92+
const versionCheck = await checkNpmVersion(PACKAGE_NAME, PACKAGE_VERSION);
8893
if (versionCheck.isOutdated && versionCheck.latestVersion) {
8994
console.warn(
9095
`\n⚠️ New version available: ${versionCheck.currentVersion}${versionCheck.latestVersion}`
9196
);
92-
console.warn(` Run: npm install -g create-gen-app@latest\n`);
97+
console.warn(` Run: npm install -g ${PACKAGE_NAME}@latest\n`);
9398
}
9499
} catch {
95100
// Silently ignore version check failures
96101
}
97102

103+
const ttl = resolveTtlOption(args);
104+
98105
// Initialize modules
99106
const cacheManager = new CacheManager({
100107
toolName: DEFAULT_TOOL_NAME,
101-
ttl: DEFAULT_TTL,
108+
ttl,
102109
});
103110

104111
// Handle --clear-cache
@@ -139,7 +146,7 @@ export async function runCli(
139146
if (args.branch) {
140147
console.log(`Using branch ${args.branch}`);
141148
}
142-
const tempDest = path.join(cacheManager['reposDir'], cacheKey);
149+
const tempDest = path.join(cacheManager.getReposDir(), cacheKey);
143150
gitCloner.clone(normalizedUrl, tempDest, { branch: args.branch, depth: 1 });
144151
cacheManager.set(cacheKey, tempDest);
145152
templateDir = tempDest;
@@ -213,7 +220,7 @@ export async function runCli(
213220
answers: answerOverrides,
214221
noTty,
215222
toolName: DEFAULT_TOOL_NAME,
216-
ttl: DEFAULT_TTL,
223+
ttl,
217224
});
218225

219226
console.log(`\n✨ Done! Project ready at ${outputDir}`);
@@ -238,6 +245,8 @@ Options:
238245
-o, --output <dir> Output directory (defaults to ./<template>)
239246
-f, --force Overwrite the output directory if it exists
240247
-c, --clear-cache Clear the template cache and exit
248+
--ttl <ms> Set cache TTL in milliseconds (flag alone uses 1 week)
249+
--no-ttl Disable TTL (cache never expires)
241250
-v, --version Show CLI version
242251
-n, --no-tty Disable TTY mode for prompts
243252
-h, --help Show this help message
@@ -246,7 +255,7 @@ You can also pass variable overrides, e.g.:
246255
node cli --template module --PROJECT_NAME my-app
247256
248257
Cache is stored at: ~/.${DEFAULT_TOOL_NAME}/cache/repos
249-
TTL: ${DEFAULT_TTL / (24 * 60 * 60 * 1000)} days
258+
TTL: none by default; use --ttl to enable (default ${DEFAULT_TTL_DAYS} days when flag provided)
250259
`);
251260
}
252261

@@ -317,3 +326,26 @@ if (require.main === module) {
317326
process.exitCode = 1;
318327
});
319328
}
329+
330+
function resolveTtlOption(args: ParsedArgs): number | undefined {
331+
const disableTtl = Boolean(args["no-ttl"] ?? (args as Record<string, unknown>).noTtl);
332+
if (disableTtl) {
333+
return undefined;
334+
}
335+
336+
if (args.ttl === undefined) {
337+
return undefined;
338+
}
339+
340+
// Support --ttl with no value to use the default 1-week TTL
341+
if (args.ttl === true) {
342+
return DEFAULT_TTL;
343+
}
344+
345+
const ttlMs = Number(args.ttl);
346+
if (Number.isNaN(ttlMs) || ttlMs < 0) {
347+
throw new Error("TTL must be a non-negative number of milliseconds");
348+
}
349+
350+
return ttlMs === 0 ? undefined : ttlMs;
351+
}

packages/create-gen-app-test/src/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { GitCloner } from 'create-gen-app';
44
import { Templatizer } from 'create-gen-app';
55

66
// Configuration constants (top-most layer owns defaults)
7-
const DEFAULT_TTL = 604800000; // 1 week in milliseconds
87
const DEFAULT_TOOL_NAME = 'pgpm';
98

109
export interface CreateFromTemplateOptions {
@@ -39,7 +38,7 @@ export async function createFromTemplate(
3938
answers = {},
4039
toolName = DEFAULT_TOOL_NAME,
4140
branch,
42-
ttl = DEFAULT_TTL,
41+
ttl,
4342
baseDir,
4443
fromPath,
4544
noTty = false,
@@ -79,7 +78,7 @@ export async function createFromTemplate(
7978
} else {
8079
// 4. Clone to cache
8180
console.log(`Cloning ${normalizedUrl}...`);
82-
const tempDest = path.join(cacheManager['reposDir'], cacheKey);
81+
const tempDest = path.join(cacheManager.getReposDir(), cacheKey);
8382

8483
gitCloner.clone(normalizedUrl, tempDest, { branch, depth: 1 });
8584
cacheManager.set(cacheKey, tempDest);

packages/create-gen-app/README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ A TypeScript-first library for cloning template repositories, asking the user fo
2525
- Merge auto-discovered variables with `.questions.{json,js}` (questions win)
2626
- Interactive prompts powered by `inquirerer`, with flexible override mapping (`argv` support) and non-TTY mode for CI
2727
- 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)
2929

3030
## Installation
3131

@@ -60,7 +60,8 @@ async function main() {
6060
// 1. Initialize components
6161
const cacheManager = new CacheManager({
6262
toolName: "my-cli", // ~/.my-cli/cache
63-
ttl: 604800000, // 1 week
63+
// ttl is optional; omit to keep cache forever, or set (e.g., 1 week) to enable expiration
64+
// ttl: 604800000,
6465
});
6566
const gitCloner = new GitCloner();
6667
const templatizer = new Templatizer();
@@ -78,7 +79,7 @@ async function main() {
7879
if (isExpired) cacheManager.clear(cacheKey);
7980

8081
// Clone to a temporary location managed by CacheManager
81-
const tempDest = path.join(cacheManager.reposDir, cacheKey);
82+
const tempDest = path.join(cacheManager.getReposDir(), cacheKey);
8283
await gitCloner.clone(normalizedUrl, tempDest, { depth: 1 });
8384

8485
// Register and update cache
@@ -147,12 +148,14 @@ No code changes are needed; the generator discovers templates at runtime and wil
147148
## API Overview
148149

149150
### CacheManager
150-
- `new CacheManager(config)`: Initialize with `toolName` and `ttl`.
151+
- `new CacheManager(config)`: Initialize with `toolName` and optional `ttl`.
151152
- `get(key)`: Get path to cached repo if exists.
152153
- `set(key, path)`: Register a path in the cache.
153154
- `checkExpiration(key)`: Check if a cache entry is expired.
154155
- `clear(key)`: Remove a specific cache entry.
155156
- `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.
156159

157160
### GitCloner
158161
- `clone(url, dest, options)`: Clone a repo to a destination.

packages/create-gen-app/__tests__/ttl-expiration.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ describe('TTL expiration', () => {
1919
try {
2020
// First clone - cache miss
2121
const cacheKey = cacheManager.createKey(testRepo);
22-
const tempDest = path.join(cacheManager['reposDir'], cacheKey);
22+
const tempDest = path.join(cacheManager.getReposDir(), cacheKey);
2323

2424
console.log(`Cloning test repository to ${tempDest}...`);
2525
gitCloner.clone(testRepo, tempDest, { depth: 1 });

packages/create-gen-app/src/cache/cache-manager.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,43 @@ export class CacheManager {
2020
toolName: config.toolName,
2121
baseDir: config.baseDir,
2222
ttl: config.ttl,
23+
dirs: config.dirs,
2324
};
2425

25-
this.dirs = appstash(this.config.toolName, {
26-
ensure: true,
27-
baseDir: this.config.baseDir,
28-
});
26+
this.dirs =
27+
config.dirs ??
28+
appstash(this.config.toolName, {
29+
ensure: true,
30+
baseDir: this.config.baseDir,
31+
});
2932

3033
this.reposDir = resolveAppstash(this.dirs, 'cache', 'repos');
3134
this.metadataDir = resolveAppstash(this.dirs, 'cache', 'metadata');
3235

3336
this.ensureDirectories();
3437
}
3538

39+
/**
40+
* Public accessor for the repos cache directory path.
41+
*/
42+
getReposDir(): string {
43+
return this.reposDir;
44+
}
45+
46+
/**
47+
* Public accessor for the metadata cache directory path.
48+
*/
49+
getMetadataDir(): string {
50+
return this.metadataDir;
51+
}
52+
53+
/**
54+
* Public accessor for resolved appstash directories.
55+
*/
56+
getAppstashDirs(): AppStashResult {
57+
return this.dirs;
58+
}
59+
3660
/**
3761
* Get cached directory path if exists and not expired
3862
* Returns null if not cached or expired

packages/create-gen-app/src/cache/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
import { AppStashResult } from 'appstash';
2+
13
export interface CacheManagerConfig {
24
toolName: string;
35
baseDir?: string;
46
ttl?: number;
7+
/**
8+
* Optional pre-resolved appstash directories owned by the caller.
9+
* When provided, CacheManager will use these instead of creating its own.
10+
*/
11+
dirs?: AppStashResult;
512
}
613

714
export interface CacheMetadata {

0 commit comments

Comments
 (0)