diff --git a/packages/create-gen-app-test/src/__tests__/__snapshots__/cached-template.test.ts.snap b/packages/create-gen-app-test/src/__tests__/__snapshots__/cached-template.test.ts.snap index 668769c..bd4cf99 100644 --- a/packages/create-gen-app-test/src/__tests__/__snapshots__/cached-template.test.ts.snap +++ b/packages/create-gen-app-test/src/__tests__/__snapshots__/cached-template.test.ts.snap @@ -2,61 +2,42 @@ exports[`cached template integration tests first clone with variable replacement should snapshot created directory structure 1`] = ` [ - "module/", - "module/README.md", - "module/__tests__/", - "module/__tests__/basic.test.ts", - "module/jest.config.js", - "module/package.json", - "workspace/", - "workspace/.eslintrc.json", - "workspace/.github/", - "workspace/.github/run-tests.yaml", - "workspace/.gitignore", - "workspace/.prettierrc.json", - "workspace/.vscode/", - "workspace/.vscode/settings.json", - "workspace/LICENSE", - "workspace/Makefile", - "workspace/README.md", - "workspace/bin/", - "workspace/bin/install.sh", - "workspace/docker-compose.yml", - "workspace/launchql.json", - "workspace/lerna.json", - "workspace/package.json", - "workspace/pnpm-workspace.yaml", - "workspace/tsconfig.json", + "LICENSE", + "README.md", + "__tests__/", + "__tests__/basic.test.ts", + "jest.config.js", + "package.json", ] `; exports[`cached template integration tests first clone with variable replacement should snapshot package.json files if they exist 1`] = ` { - "module/package.json": { - "author": "____fullName____ <____email____>", + "package.json": { + "author": "Test User test ", "bugs": { - "url": "https://github.com/____username____/____repoName____/issues", + "url": "https://github.com/Test User test/Test User test/issues", }, - "description": "____moduleDesc____", + "description": "Test Module test", "devDependencies": { "pgsql-test": "^2.13.2", }, - "homepage": "https://github.com/____username____/____repoName____", + "homepage": "https://github.com/Test User test/Test User test", "keywords": [], - "license": "____license____", - "name": "____packageIdentifier____", + "license": "MIT", + "name": "integration-test", "pnpm": { "overrides": { "graphql": "14.7.0", }, }, "publishConfig": { - "access": "____access____", + "access": "public", "directory": "dist", }, "repository": { "type": "git", - "url": "https://github.com/____username____/____repoName____", + "url": "https://github.com/Test User test/Test User test", }, "scripts": { "lint": "eslint . --fix", @@ -65,108 +46,47 @@ exports[`cached template integration tests first clone with variable replacement }, "version": "0.0.1", }, - "workspace/package.json": { - "author": "____fullName____ <____email____>", - "devDependencies": { - "@types/jest": "^29.5.14", - "@types/node": "^22.10.2", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", - "eslint": "^9.13.0", - "eslint-config-prettier": "^10.1.1", - "eslint-plugin-simple-import-sort": "^12.1.1", - "eslint-plugin-unused-imports": "^4.3.0", - "jest": "^29.7.0", - "lerna": "^8.2.4", - "pgsql-test": "^2.13.2", - "prettier": "^3.3.3", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - }, - "license": "SEE LICENSE IN LICENSE", - "name": "____moduleName____", - "pnpm": { - "overrides": { - "graphql": "14.7.0", - }, - }, - "private": true, - "publishConfig": { - "access": "restricted", - }, - "repository": { - "type": "git", - "url": "https://github.com/____username____/____moduleName____", - }, - "scripts": { - "lint": "pnpm -r run lint", - }, - "version": "0.0.1", - "workspaces": [ - "packages/*", - ], - }, } `; exports[`cached template integration tests second clone from cache should snapshot created directory structure from cache 1`] = ` [ - "module/", - "module/README.md", - "module/__tests__/", - "module/__tests__/basic.test.ts", - "module/jest.config.js", - "module/package.json", - "workspace/", - "workspace/.eslintrc.json", - "workspace/.github/", - "workspace/.github/run-tests.yaml", - "workspace/.gitignore", - "workspace/.prettierrc.json", - "workspace/.vscode/", - "workspace/.vscode/settings.json", - "workspace/LICENSE", - "workspace/Makefile", - "workspace/README.md", - "workspace/bin/", - "workspace/bin/install.sh", - "workspace/docker-compose.yml", - "workspace/launchql.json", - "workspace/lerna.json", - "workspace/package.json", - "workspace/pnpm-workspace.yaml", - "workspace/tsconfig.json", + "LICENSE", + "README.md", + "__tests__/", + "__tests__/basic.test.ts", + "jest.config.js", + "package.json", ] `; exports[`cached template integration tests second clone from cache should snapshot package.json files from cached template 1`] = ` { - "module/package.json": { - "author": "____fullName____ <____email____>", + "package.json": { + "author": "Test User cached ", "bugs": { - "url": "https://github.com/____username____/____repoName____/issues", + "url": "https://github.com/Test User cached/Test User cached/issues", }, - "description": "____moduleDesc____", + "description": "Test Module cached", "devDependencies": { "pgsql-test": "^2.13.2", }, - "homepage": "https://github.com/____username____/____repoName____", + "homepage": "https://github.com/Test User cached/Test User cached", "keywords": [], - "license": "____license____", - "name": "____packageIdentifier____", + "license": "MIT", + "name": "integration-cached", "pnpm": { "overrides": { "graphql": "14.7.0", }, }, "publishConfig": { - "access": "____access____", + "access": "public", "directory": "dist", }, "repository": { "type": "git", - "url": "https://github.com/____username____/____repoName____", + "url": "https://github.com/Test User cached/Test User cached", }, "scripts": { "lint": "eslint . --fix", @@ -175,47 +95,5 @@ exports[`cached template integration tests second clone from cache should snapsh }, "version": "0.0.1", }, - "workspace/package.json": { - "author": "____fullName____ <____email____>", - "devDependencies": { - "@types/jest": "^29.5.14", - "@types/node": "^22.10.2", - "@typescript-eslint/eslint-plugin": "^8.15.0", - "@typescript-eslint/parser": "^8.15.0", - "eslint": "^9.13.0", - "eslint-config-prettier": "^10.1.1", - "eslint-plugin-simple-import-sort": "^12.1.1", - "eslint-plugin-unused-imports": "^4.3.0", - "jest": "^29.7.0", - "lerna": "^8.2.4", - "pgsql-test": "^2.13.2", - "prettier": "^3.3.3", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "typescript": "^5.6.3", - }, - "license": "SEE LICENSE IN LICENSE", - "name": "____moduleName____", - "pnpm": { - "overrides": { - "graphql": "14.7.0", - }, - }, - "private": true, - "publishConfig": { - "access": "restricted", - }, - "repository": { - "type": "git", - "url": "https://github.com/____username____/____moduleName____", - }, - "scripts": { - "lint": "pnpm -r run lint", - }, - "version": "0.0.1", - "workspaces": [ - "packages/*", - ], - }, } `; diff --git a/packages/create-gen-app-test/src/__tests__/cached-template.test.ts b/packages/create-gen-app-test/src/__tests__/cached-template.test.ts index 0eed514..709709c 100644 --- a/packages/create-gen-app-test/src/__tests__/cached-template.test.ts +++ b/packages/create-gen-app-test/src/__tests__/cached-template.test.ts @@ -4,9 +4,10 @@ import * as path from 'path'; import { appstash, resolve } from 'appstash'; -import { createFromCachedTemplate, TemplateCache } from '../index'; +import { createFromTemplate, CacheManager, GitCloner } from '../index'; +import { buildAnswers, TEST_REPO, TEST_TEMPLATE } from '../test-utils/integration-helpers'; -const DEFAULT_TEMPLATE_URL = 'https://github.com/launchql/pgpm-boilerplates'; +const DEFAULT_TEMPLATE_URL = TEST_REPO; describe('cached template integration tests', () => { let testOutputDir: string; @@ -35,25 +36,36 @@ describe('cached template integration tests', () => { describe('cache functionality', () => { let sharedCachePath: string; - let templateCache: TemplateCache; + let cacheManager: CacheManager; beforeAll(() => { - templateCache = new TemplateCache({ - enabled: true, + cacheManager = new CacheManager({ toolName: testCacheTool, }); }); it('should return null when cache does not exist for new URL', () => { const nonExistentUrl = 'https://github.com/nonexistent/repo-test-123456'; - const cachedRepo = templateCache.get(nonExistentUrl); + const cacheKey = cacheManager.createKey(nonExistentUrl); + const cachedRepo = cacheManager.get(cacheKey); expect(cachedRepo).toBeNull(); }); it('should clone repository to cache', () => { - const cachePath = templateCache.set(DEFAULT_TEMPLATE_URL); + // Use GitCloner + CacheManager to clone and cache + const gitCloner = new GitCloner(); + const normalizedUrl = gitCloner.normalizeUrl(DEFAULT_TEMPLATE_URL); + const cacheKey = cacheManager.createKey(normalizedUrl); + + // Clone to cache directory + const cachePath = path.join(cacheManager.getReposDir(), cacheKey); + gitCloner.clone(normalizedUrl, cachePath, { depth: 1 }); + + // Register in cache manager + cacheManager.set(cacheKey, cachePath); sharedCachePath = cachePath; + // Verify cache was created correctly expect(fs.existsSync(cachePath)).toBe(true); expect(fs.existsSync(path.join(cachePath, '.git'))).toBe(false); @@ -62,7 +74,11 @@ describe('cached template integration tests', () => { }, 60000); it('should retrieve cached repository', () => { - const cachedRepo = templateCache.get(DEFAULT_TEMPLATE_URL); + const gitCloner = new GitCloner(); + const normalizedUrl = gitCloner.normalizeUrl(DEFAULT_TEMPLATE_URL); + const cacheKey = cacheManager.createKey(normalizedUrl); + const cachedRepo = cacheManager.get(cacheKey); + expect(cachedRepo).not.toBeNull(); expect(cachedRepo).toBe(sharedCachePath); expect(fs.existsSync(cachedRepo!)).toBe(true); @@ -76,16 +92,13 @@ describe('cached template integration tests', () => { beforeAll(async () => { firstOutputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'first-clone-')); - firstCloneResult = await createFromCachedTemplate({ + firstCloneResult = await createFromTemplate({ templateUrl: DEFAULT_TEMPLATE_URL, outputDir: firstOutputDir, - answers: { - PROJECT_NAME: 'test-project', - AUTHOR: 'Test Author', - DESCRIPTION: 'A test project', - MODULE_NAME: 'testmodule' - }, - cacheTool: testCacheTool + answers: buildAnswers('test'), + toolName: testCacheTool, + noTty: true, + fromPath: TEST_TEMPLATE }); }, 60000); @@ -128,11 +141,15 @@ describe('cached template integration tests', () => { }); it('should verify template cache was created', () => { - const templateCache = new TemplateCache({ - enabled: true, + // Verify cache was created by createFromTemplate + const cacheManager = new CacheManager({ toolName: testCacheTool, }); - const cachedRepo = templateCache.get(DEFAULT_TEMPLATE_URL); + const gitCloner = new GitCloner(); + const normalizedUrl = gitCloner.normalizeUrl(DEFAULT_TEMPLATE_URL); + const cacheKey = cacheManager.createKey(normalizedUrl); + const cachedRepo = cacheManager.get(cacheKey); + expect(cachedRepo).not.toBeNull(); expect(fs.existsSync(cachedRepo!)).toBe(true); }); @@ -143,28 +160,24 @@ describe('cached template integration tests', () => { let secondOutputDir: string; beforeAll(async () => { - await createFromCachedTemplate({ + await createFromTemplate({ templateUrl: DEFAULT_TEMPLATE_URL, outputDir: fs.mkdtempSync(path.join(os.tmpdir(), 'warmup-')), - answers: { - PROJECT_NAME: 'warmup', - MODULE_NAME: 'warmup' - }, - cacheTool: testCacheTool + answers: buildAnswers('warmup'), + toolName: testCacheTool, + noTty: true, + fromPath: TEST_TEMPLATE }); secondOutputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'second-clone-')); - secondCloneResult = await createFromCachedTemplate({ + secondCloneResult = await createFromTemplate({ templateUrl: DEFAULT_TEMPLATE_URL, outputDir: secondOutputDir, - answers: { - PROJECT_NAME: 'cached-project', - AUTHOR: 'Cached Author', - DESCRIPTION: 'A cached test project', - MODULE_NAME: 'cachedmodule' - }, - cacheTool: testCacheTool + answers: buildAnswers('cached'), + toolName: testCacheTool, + noTty: true, + fromPath: TEST_TEMPLATE }); }, 60000); diff --git a/packages/create-gen-app-test/src/cli.ts b/packages/create-gen-app-test/src/cli.ts index 5496817..b5b36ec 100644 --- a/packages/create-gen-app-test/src/cli.ts +++ b/packages/create-gen-app-test/src/cli.ts @@ -6,16 +6,20 @@ import * as path from "path"; import { Inquirerer, ListQuestion } from "inquirerer"; import minimist, { ParsedArgs } from "minimist"; -import { cloneRepo, createGen } from "create-gen-app"; +import { CacheManager, GitCloner, checkNpmVersion } from "create-gen-app"; +import { createFromTemplate } from './index'; const DEFAULT_REPO = "https://github.com/launchql/pgpm-boilerplates.git"; const DEFAULT_PATH = "."; const DEFAULT_OUTPUT_FALLBACK = "create-gen-app-output"; +const DEFAULT_TOOL_NAME = "create-gen-app-test"; +const DEFAULT_TTL = 604800000; // 1 week +const DEFAULT_TTL_DAYS = DEFAULT_TTL / (24 * 60 * 60 * 1000); -// Use require for package.json to avoid module resolution issues -const createGenPackageJson = require("create-gen-app/package.json"); -const PACKAGE_VERSION = - (createGenPackageJson as { version?: string }).version ?? "0.0.0"; +// Import package.json for version +import * as createGenPackageJson from "create-gen-app/package.json"; +const PACKAGE_NAME = createGenPackageJson.name ?? "@launchql/cli"; +const PACKAGE_VERSION = createGenPackageJson.version ?? "0.0.0"; const RESERVED_ARG_KEYS = new Set([ "_", @@ -37,6 +41,10 @@ const RESERVED_ARG_KEYS = new Set([ "v", "no-tty", "n", + "clear-cache", + "c", + "ttl", + "no-ttl", ]); export interface CliResult { @@ -58,9 +66,11 @@ export async function runCli( h: "help", v: "version", n: "no-tty", + c: "clear-cache", + // no alias for ttl to keep it explicit }, - string: ["repo", "branch", "path", "template", "output"], - boolean: ["force", "help", "version", "no-tty"], + string: ["repo", "branch", "path", "template", "output", "ttl"], + boolean: ["force", "help", "version", "no-tty", "clear-cache", "no-ttl"], default: { repo: DEFAULT_REPO, path: DEFAULT_PATH, @@ -77,19 +87,74 @@ export async function runCli( return; } + // Check for updates + try { + const versionCheck = await checkNpmVersion(PACKAGE_NAME, PACKAGE_VERSION); + if (versionCheck.isOutdated && versionCheck.latestVersion) { + console.warn( + `\n⚠️ New version available: ${versionCheck.currentVersion} → ${versionCheck.latestVersion}` + ); + console.warn(` Run: npm install -g ${PACKAGE_NAME}@latest\n`); + } + } catch { + // Silently ignore version check failures + } + + const ttl = resolveTtlOption(args); + + // Initialize modules + const cacheManager = new CacheManager({ + toolName: DEFAULT_TOOL_NAME, + ttl, + }); + + // Handle --clear-cache + if (args["clear-cache"]) { + console.log("Clearing cache..."); + cacheManager.clearAll(); + console.log("✨ Cache cleared successfully!"); + return; + } + + const gitCloner = new GitCloner(); + if (!args.output && args._[0]) { args.output = args._[0]; } - let tempDir: string | null = null; - try { + // Get or clone template + const normalizedUrl = gitCloner.normalizeUrl(args.repo); + const cacheKey = cacheManager.createKey(normalizedUrl, args.branch); + + let templateDir: string; + const cachedPath = cacheManager.get(cacheKey); + const expiredMetadata = cacheManager.checkExpiration(cacheKey); + + if (expiredMetadata) { + console.warn( + `⚠️ Cached template expired (last updated: ${new Date(expiredMetadata.lastUpdated).toLocaleString()})` + ); + console.log('Updating cache...'); + cacheManager.clear(cacheKey); + } + + if (cachedPath && !expiredMetadata) { + console.log(`Using cached template from ${cachedPath}`); + templateDir = cachedPath; + } else { console.log(`Cloning template from ${args.repo}...`); if (args.branch) { console.log(`Using branch ${args.branch}`); } - tempDir = await cloneRepo(args.repo, { branch: args.branch }); + const tempDest = path.join(cacheManager.getReposDir(), cacheKey); + gitCloner.clone(normalizedUrl, tempDest, { branch: args.branch, depth: 1 }); + cacheManager.set(cacheKey, tempDest); + templateDir = tempDest; + console.log('Template cached for future runs'); + } - const selectionRoot = path.join(tempDir, args.path); + try { + const selectionRoot = path.join(templateDir, args.path); if ( !fs.existsSync(selectionRoot) || !fs.statSync(selectionRoot).isDirectory() @@ -146,21 +211,22 @@ export async function runCli( args["no-tty"] ?? (args as Record).noTty ); - await createGen({ + // Use the createFromTemplate function which will use the same cache + await createFromTemplate({ templateUrl: args.repo, - fromBranch: args.branch, + branch: args.branch, fromPath, outputDir, - argv: answerOverrides, + answers: answerOverrides, noTty, + toolName: DEFAULT_TOOL_NAME, + ttl, }); console.log(`\n✨ Done! Project ready at ${outputDir}`); return { outputDir, template: selectedTemplate }; - } finally { - if (tempDir && fs.existsSync(tempDir)) { - fs.rmSync(tempDir, { recursive: true, force: true }); - } + } catch (error) { + throw error; } } @@ -178,12 +244,18 @@ Options: -t, --template Template folder to use (will prompt if omitted) -o, --output Output directory (defaults to ./