create-gen-app 0.1.7 → 0.2.1

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 CHANGED
@@ -16,52 +16,31 @@
16
16
  <a href="https://www.npmjs.com/package/create-gen-app"><img height="20" src="https://img.shields.io/github/package-json/v/hyperweb-io/dev-utils?filename=packages%2Fcreate-gen-app%2Fpackage.json"></a>
17
17
  </p>
18
18
 
19
- A TypeScript-first CLI/library for cloning template repositories, asking the user for variables, and generating a new project with sensible defaults.
19
+ A TypeScript-first library for cloning template repositories, asking the user for variables, and generating a new project with sensible defaults.
20
20
 
21
21
  ## Features
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
25
  - Merge auto-discovered variables with `.questions.{json,js}` (questions win, including `ignore` patterns)
26
- - Interactive prompts powered by `inquirerer`, with CLI flag overrides (`--VAR value`) and non-TTY mode for CI
27
- - Built-in CLI (`create-gen-app` / `cga`) that discovers templates, prompts once, and writes output safely
26
+ - Interactive prompts powered by `inquirerer`, with flexible override mapping (`argv` support) and non-TTY mode for CI
28
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)
29
29
 
30
30
  ## Installation
31
31
 
32
32
  ```bash
33
33
  npm install create-gen-app
34
- # or for CLI only
35
- npm install -g create-gen-app
36
34
  ```
37
35
 
38
- ## CLI Usage
39
-
40
- ```bash
41
- # interactively pick a template from launchql/pgpm-boilerplates
42
- create-gen-app --output ./workspace
43
-
44
- # short alias
45
- cga --template module --branch main --output ./module \
46
- --USERFULLNAME "Jane Dev" --USEREMAIL jane@example.com
47
-
48
- # point to a different repo/branch/path
49
- cga --repo github:my-org/my-templates --branch release \
50
- --path ./templates --template api --output ./api
51
- ```
52
-
53
- Key flags:
54
-
55
- - `--repo`, `--branch`, `--path` – choose the Git repo, branch/tag, and subdirectory that contains templates
56
- - `--template` – folder inside `--path` (auto-prompted if omitted)
57
- - `--output` – destination directory (defaults to `./<template>`); use `--force` to overwrite
58
- - `--no-tty` – disable interactive prompts (ideal for CI)
59
- - `--version`, `--help` – standard metadata
60
- - Any extra `--VAR value` pairs become variable overrides
36
+ > **Note:** The published package is API-only. An internal CLI harness used for integration testing now lives in `packages/create-gen-app-test/`.
61
37
 
62
38
  ## Library Usage
63
39
 
64
40
  ```typescript
41
+ import * as os from "os";
42
+ import * as path from "path";
43
+
65
44
  import { createGen } from "create-gen-app";
66
45
 
67
46
  await createGen({
@@ -76,9 +55,33 @@ await createGen({
76
55
  LICENSE: "MIT",
77
56
  },
78
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
+ },
79
63
  });
80
64
  ```
81
65
 
66
+ ### Template Caching
67
+
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.
69
+
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:
73
+
74
+ ```ts
75
+ const tempBase = fs.mkdtempSync(path.join(os.tmpdir(), "create-gen-cache-"));
76
+
77
+ await createGen({
78
+ ...options,
79
+ cache: { baseDir: tempBase, toolName: "pgpm-test-suite" },
80
+ });
81
+ ```
82
+
83
+ The cache directory never mutates the template, so reusing the same cached repo across many runs is safe.
84
+
82
85
  ### Template Variables
83
86
 
84
87
  Variables should be wrapped in four underscores on each side:
@@ -126,14 +129,15 @@ Or `.questions.js` for dynamic logic. Question names can use `____var____` or pl
126
129
 
127
130
  - `{{YEAR}}`, `{{AUTHOR}}`, `{{EMAIL_LINE}}`
128
131
 
129
- No code changes are needed; the CLI discovers templates at runtime and will warn if a `.questions` option doesn’t have a matching template.
132
+ No code changes are needed; the generator discovers templates at runtime and will warn if a `.questions` option doesn’t have a matching template.
130
133
 
131
134
  ## API Overview
132
135
 
133
136
  - `createGen(options)` – full pipeline (clone → extract → prompt → replace)
134
137
  - `cloneRepo(url, { branch })` – clone to a temp dir
138
+ - `normalizeCacheOptions(cache)` / `prepareTemplateDirectory(...)` – inspect or reuse cached template repos
135
139
  - `extractVariables(dir)` – parse file/folder names + content for variables, load `.questions`
136
- - `promptUser(extracted, argv, noTty)` – run interactive questions with CLI overrides and alias deduping
140
+ - `promptUser(extracted, argv, noTty)` – run interactive questions with override alias deduping
137
141
  - `replaceVariables(templateDir, outputDir, extracted, answers)` – copy files, rename paths, render licenses
138
142
 
139
- See `dev/README.md` for the local development helper script (`pnpm dev`).
143
+ See `packages/create-gen-app-test/dev/README.md` for the local development helper script (`pnpm --filter create-gen-app-test dev`).
package/cache.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { CacheOptions } from "./types";
2
+ export interface TemplateSource {
3
+ templateDir: string;
4
+ cacheUsed: boolean;
5
+ cleanup: () => void;
6
+ }
7
+ interface PrepareTemplateArgs {
8
+ templateUrl: string;
9
+ branch?: string;
10
+ cache: CacheOptions | false;
11
+ }
12
+ export declare function prepareTemplateDirectory(args: PrepareTemplateArgs): Promise<TemplateSource>;
13
+ export { TemplateCache } from "./template-cache";
package/cache.js ADDED
@@ -0,0 +1,76 @@
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.TemplateCache = void 0;
37
+ exports.prepareTemplateDirectory = prepareTemplateDirectory;
38
+ const fs = __importStar(require("fs"));
39
+ const clone_1 = require("./clone");
40
+ const template_cache_1 = require("./template-cache");
41
+ async function prepareTemplateDirectory(args) {
42
+ const { templateUrl, branch, cache } = args;
43
+ const templateCache = new template_cache_1.TemplateCache(cache);
44
+ if (!templateCache.isEnabled()) {
45
+ const tempDir = await (0, clone_1.cloneRepo)(templateUrl, { branch });
46
+ return {
47
+ templateDir: tempDir,
48
+ cacheUsed: false,
49
+ cleanup: () => cleanupDir(tempDir),
50
+ };
51
+ }
52
+ // Try to get from cache
53
+ const cachedPath = templateCache.get(templateUrl, branch);
54
+ if (cachedPath) {
55
+ return {
56
+ templateDir: cachedPath,
57
+ cacheUsed: true,
58
+ cleanup: () => { },
59
+ };
60
+ }
61
+ // Cache miss or expired - clone and cache
62
+ const cachePath = templateCache.set(templateUrl, branch);
63
+ return {
64
+ templateDir: cachePath,
65
+ cacheUsed: false,
66
+ cleanup: () => { },
67
+ };
68
+ }
69
+ function cleanupDir(dir) {
70
+ if (fs.existsSync(dir)) {
71
+ fs.rmSync(dir, { recursive: true, force: true });
72
+ }
73
+ }
74
+ // Re-export TemplateCache for external use
75
+ var template_cache_2 = require("./template-cache");
76
+ Object.defineProperty(exports, "TemplateCache", { enumerable: true, get: function () { return template_cache_2.TemplateCache; } });
package/clone.d.ts CHANGED
@@ -7,3 +7,9 @@ export interface CloneOptions {
7
7
  * @returns Path to the cloned repository
8
8
  */
9
9
  export declare function cloneRepo(url: string, options?: CloneOptions): Promise<string>;
10
+ /**
11
+ * Normalize a URL to a git-cloneable format
12
+ * @param url - Input URL
13
+ * @returns Normalized git URL
14
+ */
15
+ export declare function normalizeGitUrl(url: string): string;
package/clone.js CHANGED
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.cloneRepo = cloneRepo;
37
+ exports.normalizeGitUrl = normalizeGitUrl;
37
38
  const child_process_1 = require("child_process");
38
39
  const fs = __importStar(require("fs"));
39
40
  const os = __importStar(require("os"));
@@ -44,15 +45,16 @@ const path = __importStar(require("path"));
44
45
  * @returns Path to the cloned repository
45
46
  */
46
47
  async function cloneRepo(url, options = {}) {
47
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'create-gen-'));
48
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-gen-"));
48
49
  const { branch } = options;
49
50
  try {
50
51
  const gitUrl = normalizeGitUrl(url);
51
- const branchArgs = branch ? ` --branch ${branch} --single-branch` : '';
52
- (0, child_process_1.execSync)(`git clone${branchArgs} ${gitUrl} ${tempDir}`, {
53
- stdio: 'inherit'
52
+ const branchArgs = branch ? ` --branch ${branch} --single-branch` : "";
53
+ const depthArgs = " --depth 1"; // use shallow clone for speed; remove if future features need full history
54
+ (0, child_process_1.execSync)(`git clone${branchArgs}${depthArgs} ${gitUrl} ${tempDir}`, {
55
+ stdio: "inherit",
54
56
  });
55
- const gitDir = path.join(tempDir, '.git');
57
+ const gitDir = path.join(tempDir, ".git");
56
58
  if (fs.existsSync(gitDir)) {
57
59
  fs.rmSync(gitDir, { recursive: true, force: true });
58
60
  }
@@ -72,7 +74,9 @@ async function cloneRepo(url, options = {}) {
72
74
  * @returns Normalized git URL
73
75
  */
74
76
  function normalizeGitUrl(url) {
75
- if (url.startsWith('git@') || url.startsWith('https://') || url.startsWith('http://')) {
77
+ if (url.startsWith("git@") ||
78
+ url.startsWith("https://") ||
79
+ url.startsWith("http://")) {
76
80
  return url;
77
81
  }
78
82
  if (/^[\w-]+\/[\w-]+$/.test(url)) {
package/esm/cache.js ADDED
@@ -0,0 +1,38 @@
1
+ import * as fs from "fs";
2
+ import { cloneRepo } from "./clone";
3
+ import { TemplateCache } from "./template-cache";
4
+ export async function prepareTemplateDirectory(args) {
5
+ const { templateUrl, branch, cache } = args;
6
+ const templateCache = new TemplateCache(cache);
7
+ if (!templateCache.isEnabled()) {
8
+ const tempDir = await cloneRepo(templateUrl, { branch });
9
+ return {
10
+ templateDir: tempDir,
11
+ cacheUsed: false,
12
+ cleanup: () => cleanupDir(tempDir),
13
+ };
14
+ }
15
+ // Try to get from cache
16
+ const cachedPath = templateCache.get(templateUrl, branch);
17
+ if (cachedPath) {
18
+ return {
19
+ templateDir: cachedPath,
20
+ cacheUsed: true,
21
+ cleanup: () => { },
22
+ };
23
+ }
24
+ // Cache miss or expired - clone and cache
25
+ const cachePath = templateCache.set(templateUrl, branch);
26
+ return {
27
+ templateDir: cachePath,
28
+ cacheUsed: false,
29
+ cleanup: () => { },
30
+ };
31
+ }
32
+ function cleanupDir(dir) {
33
+ if (fs.existsSync(dir)) {
34
+ fs.rmSync(dir, { recursive: true, force: true });
35
+ }
36
+ }
37
+ // Re-export TemplateCache for external use
38
+ export { TemplateCache } from "./template-cache";
package/esm/clone.js CHANGED
@@ -1,22 +1,23 @@
1
- import { execSync } from 'child_process';
2
- import * as fs from 'fs';
3
- import * as os from 'os';
4
- import * as path from 'path';
1
+ import { execSync } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
5
  /**
6
6
  * Clone a repository to a temporary directory
7
7
  * @param url - Repository URL (GitHub or any git URL)
8
8
  * @returns Path to the cloned repository
9
9
  */
10
10
  export async function cloneRepo(url, options = {}) {
11
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'create-gen-'));
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-gen-"));
12
12
  const { branch } = options;
13
13
  try {
14
14
  const gitUrl = normalizeGitUrl(url);
15
- const branchArgs = branch ? ` --branch ${branch} --single-branch` : '';
16
- execSync(`git clone${branchArgs} ${gitUrl} ${tempDir}`, {
17
- stdio: 'inherit'
15
+ const branchArgs = branch ? ` --branch ${branch} --single-branch` : "";
16
+ const depthArgs = " --depth 1"; // use shallow clone for speed; remove if future features need full history
17
+ execSync(`git clone${branchArgs}${depthArgs} ${gitUrl} ${tempDir}`, {
18
+ stdio: "inherit",
18
19
  });
19
- const gitDir = path.join(tempDir, '.git');
20
+ const gitDir = path.join(tempDir, ".git");
20
21
  if (fs.existsSync(gitDir)) {
21
22
  fs.rmSync(gitDir, { recursive: true, force: true });
22
23
  }
@@ -35,8 +36,10 @@ export async function cloneRepo(url, options = {}) {
35
36
  * @param url - Input URL
36
37
  * @returns Normalized git URL
37
38
  */
38
- function normalizeGitUrl(url) {
39
- if (url.startsWith('git@') || url.startsWith('https://') || url.startsWith('http://')) {
39
+ export function normalizeGitUrl(url) {
40
+ if (url.startsWith("git@") ||
41
+ url.startsWith("https://") ||
42
+ url.startsWith("http://")) {
40
43
  return url;
41
44
  }
42
45
  if (/^[\w-]+\/[\w-]+$/.test(url)) {
package/esm/index.js CHANGED
@@ -1,48 +1,61 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { cloneRepo } from './clone';
4
- import { extractVariables } from './extract';
5
- import { promptUser } from './prompt';
6
- import { replaceVariables } from './replace';
7
- export * from './clone';
8
- export * from './extract';
9
- export * from './prompt';
10
- export * from './replace';
11
- export * from './types';
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { prepareTemplateDirectory } from "./cache";
4
+ import { extractVariables } from "./extract";
5
+ import { promptUser } from "./prompt";
6
+ import { replaceVariables } from "./replace";
7
+ export * from "./cache";
8
+ export * from "./clone";
9
+ export * from "./extract";
10
+ export * from "./prompt";
11
+ export * from "./replace";
12
+ export * from "./types";
13
+ export * from "./template-cache";
12
14
  /**
13
15
  * Create a new project from a template repository
14
16
  * @param options - Options for creating the project
15
17
  * @returns Path to the generated project
16
18
  */
17
19
  export async function createGen(options) {
18
- const { templateUrl, outputDir, argv = {}, noTty = false, fromBranch, fromPath } = options;
19
- console.log(`Cloning template from ${templateUrl}...`);
20
- const tempDir = await cloneRepo(templateUrl, { branch: fromBranch });
21
- const normalizedPath = fromPath ? path.normalize(fromPath) : '.';
22
- const templateRoot = normalizedPath && normalizedPath !== '.'
23
- ? path.join(tempDir, normalizedPath)
24
- : tempDir;
20
+ const { templateUrl, outputDir, argv = {}, noTty = false, fromBranch, fromPath, cache, } = options;
21
+ console.log(`Preparing template from ${templateUrl}...`);
22
+ const templateSource = await prepareTemplateDirectory({
23
+ templateUrl,
24
+ branch: fromBranch,
25
+ cache,
26
+ });
27
+ const cacheEnabled = cache !== false && (cache?.enabled !== false);
28
+ if (cacheEnabled) {
29
+ console.log(templateSource.cacheUsed
30
+ ? "Using cached repository"
31
+ : "Caching repository for future runs...");
32
+ }
33
+ else {
34
+ console.log("Cloning repository without cache...");
35
+ }
36
+ const normalizedPath = fromPath ? path.normalize(fromPath) : ".";
37
+ const templateRoot = normalizedPath && normalizedPath !== "."
38
+ ? path.join(templateSource.templateDir, normalizedPath)
39
+ : templateSource.templateDir;
25
40
  try {
26
41
  if (!fs.existsSync(templateRoot)) {
27
42
  throw new Error(`Template path "${fromPath}" does not exist in repository ${templateUrl}.`);
28
43
  }
29
- console.log('Extracting template variables...');
44
+ console.log("Extracting template variables...");
30
45
  const extractedVariables = await extractVariables(templateRoot);
31
46
  console.log(`Found ${extractedVariables.fileReplacers.length} file replacers`);
32
47
  console.log(`Found ${extractedVariables.contentReplacers.length} content replacers`);
33
48
  if (extractedVariables.projectQuestions) {
34
49
  console.log(`Found ${extractedVariables.projectQuestions.questions.length} project questions`);
35
50
  }
36
- console.log('Prompting for variable values...');
51
+ console.log("Prompting for variable values...");
37
52
  const answers = await promptUser(extractedVariables, argv, noTty);
38
53
  console.log(`Generating project in ${outputDir}...`);
39
54
  await replaceVariables(templateRoot, outputDir, extractedVariables, answers);
40
- console.log('Project created successfully!');
55
+ console.log("Project created successfully!");
41
56
  return outputDir;
42
57
  }
43
58
  finally {
44
- if (fs.existsSync(tempDir)) {
45
- fs.rmSync(tempDir, { recursive: true, force: true });
46
- }
59
+ templateSource.cleanup();
47
60
  }
48
61
  }
package/esm/replace.js CHANGED
@@ -53,7 +53,7 @@ async function walkAndReplace(sourceDir, destDir, extractedVariables, answers, s
53
53
  }
54
54
  }
55
55
  async function ensureLicenseFile(outputDir, answers) {
56
- const licenseValue = answers?.LICENSE;
56
+ const licenseValue = getAnswer(answers, ["LICENSE", "license"]);
57
57
  if (typeof licenseValue !== 'string' || licenseValue.trim() === '') {
58
58
  return;
59
59
  }
@@ -62,12 +62,17 @@ async function ensureLicenseFile(outputDir, answers) {
62
62
  console.warn(`[create-gen-app] License "${selectedLicense}" is not supported by the built-in templates. Leaving template LICENSE file as-is.`);
63
63
  return;
64
64
  }
65
- const author = answers?.USERFULLNAME ??
66
- answers?.AUTHOR ??
67
- answers?.AUTHORFULLNAME ??
68
- answers?.USERNAME ??
69
- 'Unknown Author';
70
- const email = answers?.USEREMAIL ?? answers?.EMAIL ?? '';
65
+ const author = getAnswer(answers, [
66
+ "USERFULLNAME",
67
+ "AUTHOR",
68
+ "AUTHORFULLNAME",
69
+ "USERNAME",
70
+ "fullName",
71
+ "author",
72
+ "authorFullName",
73
+ "userName",
74
+ ]) ?? "Unknown Author";
75
+ const email = getAnswer(answers, ["USEREMAIL", "EMAIL", "email", "userEmail"]) ?? "";
71
76
  const content = renderLicense(selectedLicense, {
72
77
  author: String(author),
73
78
  email: String(email || ''),
@@ -80,6 +85,15 @@ async function ensureLicenseFile(outputDir, answers) {
80
85
  fs.writeFileSync(licensePath, content.trimEnd() + '\n', 'utf8');
81
86
  console.log(`[create-gen-app] LICENSE updated with ${selectedLicense} template.`);
82
87
  }
88
+ function getAnswer(answers, keys) {
89
+ for (const key of keys) {
90
+ const value = answers?.[key];
91
+ if (typeof value === "string" && value.trim() !== "") {
92
+ return value;
93
+ }
94
+ }
95
+ return undefined;
96
+ }
83
97
  /**
84
98
  * Replace variables in a file using streams
85
99
  * @param sourcePath - Source file path