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.
@@ -0,0 +1,260 @@
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
+ const child_process_1 = require("child_process");
38
+ const crypto = __importStar(require("crypto"));
39
+ const fs = __importStar(require("fs"));
40
+ const path = __importStar(require("path"));
41
+ const appstash_1 = require("appstash");
42
+ const clone_1 = require("./clone");
43
+ const DEFAULT_TOOL = "pgpm";
44
+ /**
45
+ * Manages template repository caching with TTL support
46
+ */
47
+ class TemplateCache {
48
+ config;
49
+ reposDir;
50
+ metadataDir;
51
+ constructor(options) {
52
+ this.config = this.normalizeConfig(options);
53
+ if (this.config.enabled) {
54
+ const dirs = (0, appstash_1.appstash)(this.config.toolName, {
55
+ ensure: true,
56
+ baseDir: this.config.baseDir,
57
+ });
58
+ this.reposDir = (0, appstash_1.resolve)(dirs, "cache", "repos");
59
+ this.metadataDir = (0, appstash_1.resolve)(dirs, "cache", "metadata");
60
+ if (!fs.existsSync(this.reposDir)) {
61
+ fs.mkdirSync(this.reposDir, { recursive: true });
62
+ }
63
+ if (!fs.existsSync(this.metadataDir)) {
64
+ fs.mkdirSync(this.metadataDir, { recursive: true });
65
+ }
66
+ }
67
+ else {
68
+ this.reposDir = "";
69
+ this.metadataDir = "";
70
+ }
71
+ }
72
+ normalizeConfig(options) {
73
+ if (options === false) {
74
+ return {
75
+ enabled: false,
76
+ toolName: DEFAULT_TOOL,
77
+ };
78
+ }
79
+ const { enabled, toolName, baseDir, ttl, maxAge } = options ?? {};
80
+ return {
81
+ enabled: enabled !== false,
82
+ toolName: toolName ?? DEFAULT_TOOL,
83
+ baseDir,
84
+ ttl: ttl ?? maxAge,
85
+ };
86
+ }
87
+ /**
88
+ * Get cached template if it exists and is not expired
89
+ */
90
+ get(templateUrl, branch) {
91
+ if (!this.config.enabled) {
92
+ return null;
93
+ }
94
+ const key = this.createCacheKey(templateUrl, branch);
95
+ const cachePath = path.join(this.reposDir, key);
96
+ const metadataPath = path.join(this.metadataDir, `${key}.json`);
97
+ if (!fs.existsSync(cachePath)) {
98
+ return null;
99
+ }
100
+ if (this.config.ttl && fs.existsSync(metadataPath)) {
101
+ try {
102
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
103
+ if (this.isExpired(metadata)) {
104
+ this.clear(templateUrl, branch);
105
+ return null;
106
+ }
107
+ }
108
+ catch (error) {
109
+ // If metadata is corrupted, treat as expired
110
+ this.clear(templateUrl, branch);
111
+ return null;
112
+ }
113
+ }
114
+ return cachePath;
115
+ }
116
+ /**
117
+ * Cache a template repository
118
+ */
119
+ set(templateUrl, branch) {
120
+ if (!this.config.enabled) {
121
+ throw new Error("Cache is disabled");
122
+ }
123
+ const key = this.createCacheKey(templateUrl, branch);
124
+ const cachePath = path.join(this.reposDir, key);
125
+ const metadataPath = path.join(this.metadataDir, `${key}.json`);
126
+ // Clone the repository
127
+ this.cloneInto(templateUrl, cachePath, branch);
128
+ // Write metadata
129
+ const metadata = {
130
+ templateUrl,
131
+ branch,
132
+ timestamp: Date.now(),
133
+ gitUrl: (0, clone_1.normalizeGitUrl)(templateUrl),
134
+ };
135
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
136
+ return cachePath;
137
+ }
138
+ /**
139
+ * Clear a specific cached template
140
+ */
141
+ clear(templateUrl, branch) {
142
+ if (!this.config.enabled) {
143
+ return;
144
+ }
145
+ const key = this.createCacheKey(templateUrl, branch);
146
+ const cachePath = path.join(this.reposDir, key);
147
+ const metadataPath = path.join(this.metadataDir, `${key}.json`);
148
+ if (fs.existsSync(cachePath)) {
149
+ fs.rmSync(cachePath, { recursive: true, force: true });
150
+ }
151
+ if (fs.existsSync(metadataPath)) {
152
+ fs.rmSync(metadataPath, { force: true });
153
+ }
154
+ }
155
+ /**
156
+ * Clear all cached templates
157
+ */
158
+ clearAll() {
159
+ if (!this.config.enabled) {
160
+ return;
161
+ }
162
+ if (fs.existsSync(this.reposDir)) {
163
+ fs.rmSync(this.reposDir, { recursive: true, force: true });
164
+ fs.mkdirSync(this.reposDir, { recursive: true });
165
+ }
166
+ if (fs.existsSync(this.metadataDir)) {
167
+ fs.rmSync(this.metadataDir, { recursive: true, force: true });
168
+ fs.mkdirSync(this.metadataDir, { recursive: true });
169
+ }
170
+ }
171
+ /**
172
+ * Check if cache metadata is expired
173
+ */
174
+ isExpired(metadata) {
175
+ if (!this.config.ttl) {
176
+ return false;
177
+ }
178
+ const age = Date.now() - metadata.timestamp;
179
+ return age > this.config.ttl;
180
+ }
181
+ /**
182
+ * Get cache metadata for a template
183
+ */
184
+ getMetadata(templateUrl, branch) {
185
+ if (!this.config.enabled) {
186
+ return null;
187
+ }
188
+ const key = this.createCacheKey(templateUrl, branch);
189
+ const metadataPath = path.join(this.metadataDir, `${key}.json`);
190
+ if (!fs.existsSync(metadataPath)) {
191
+ return null;
192
+ }
193
+ try {
194
+ return JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
195
+ }
196
+ catch {
197
+ return null;
198
+ }
199
+ }
200
+ /**
201
+ * List all cached templates with their metadata
202
+ */
203
+ listAll() {
204
+ if (!this.config.enabled) {
205
+ return [];
206
+ }
207
+ if (!fs.existsSync(this.metadataDir)) {
208
+ return [];
209
+ }
210
+ const results = [];
211
+ const files = fs.readdirSync(this.metadataDir);
212
+ for (const file of files) {
213
+ if (!file.endsWith(".json")) {
214
+ continue;
215
+ }
216
+ const metadataPath = path.join(this.metadataDir, file);
217
+ try {
218
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
219
+ results.push({
220
+ ...metadata,
221
+ key: path.basename(file, ".json"),
222
+ expired: this.isExpired(metadata),
223
+ });
224
+ }
225
+ catch {
226
+ // Skip corrupted metadata
227
+ }
228
+ }
229
+ return results;
230
+ }
231
+ createCacheKey(templateUrl, branch) {
232
+ const gitUrl = (0, clone_1.normalizeGitUrl)(templateUrl);
233
+ return crypto
234
+ .createHash("md5")
235
+ .update(`${gitUrl}#${branch ?? "default"}`)
236
+ .digest("hex");
237
+ }
238
+ cloneInto(templateUrl, destination, branch) {
239
+ if (fs.existsSync(destination)) {
240
+ fs.rmSync(destination, { recursive: true, force: true });
241
+ }
242
+ const gitUrl = (0, clone_1.normalizeGitUrl)(templateUrl);
243
+ const branchArgs = branch ? ` --branch ${branch} --single-branch` : "";
244
+ const depthArgs = " --depth 1";
245
+ (0, child_process_1.execSync)(`git clone${branchArgs}${depthArgs} ${gitUrl} ${destination}`, {
246
+ stdio: "inherit",
247
+ });
248
+ const gitDir = path.join(destination, ".git");
249
+ if (fs.existsSync(gitDir)) {
250
+ fs.rmSync(gitDir, { recursive: true, force: true });
251
+ }
252
+ }
253
+ isEnabled() {
254
+ return this.config.enabled;
255
+ }
256
+ getConfig() {
257
+ return { ...this.config };
258
+ }
259
+ }
260
+ exports.TemplateCache = TemplateCache;
package/types.d.ts CHANGED
@@ -50,6 +50,37 @@ export interface CreateGenOptions {
50
50
  * Whether to use TTY for interactive prompts
51
51
  */
52
52
  noTty?: boolean;
53
+ /**
54
+ * Optional caching configuration. Pass `false` to disable caching entirely.
55
+ */
56
+ cache?: CacheOptions | false;
57
+ }
58
+ /**
59
+ * Options that control template caching behavior
60
+ */
61
+ export interface CacheOptions {
62
+ /**
63
+ * Enable or disable caching. Defaults to true.
64
+ */
65
+ enabled?: boolean;
66
+ /**
67
+ * Tool name used for appstash (affects ~/.<tool> dirs). Defaults to `pgpm`.
68
+ */
69
+ toolName?: string;
70
+ /**
71
+ * Optional base directory for appstash. Useful for tests to avoid touching the real home dir.
72
+ */
73
+ baseDir?: string;
74
+ /**
75
+ * Time-to-live in milliseconds. If set, cached templates older than this will be invalidated.
76
+ * Defaults to undefined (no expiration).
77
+ */
78
+ ttl?: number;
79
+ /**
80
+ * Alias for ttl. Maximum age in milliseconds for cached templates.
81
+ * Defaults to undefined (no expiration).
82
+ */
83
+ maxAge?: number;
53
84
  }
54
85
  /**
55
86
  * Result of extracting variables from a template
package/cli.d.ts DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env node
2
- export interface CliResult {
3
- outputDir: string;
4
- template: string;
5
- }
6
- export declare function runCli(rawArgv?: string[]): Promise<CliResult | void>;
package/cli.js DELETED
@@ -1,239 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- var desc = Object.getOwnPropertyDescriptor(m, k);
6
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
- desc = { enumerable: true, get: function() { return m[k]; } };
8
- }
9
- Object.defineProperty(o, k2, desc);
10
- }) : (function(o, m, k, k2) {
11
- if (k2 === undefined) k2 = k;
12
- o[k2] = m[k];
13
- }));
14
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
- Object.defineProperty(o, "default", { enumerable: true, value: v });
16
- }) : function(o, v) {
17
- o["default"] = v;
18
- });
19
- var __importStar = (this && this.__importStar) || (function () {
20
- var ownKeys = function(o) {
21
- ownKeys = Object.getOwnPropertyNames || function (o) {
22
- var ar = [];
23
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
- return ar;
25
- };
26
- return ownKeys(o);
27
- };
28
- return function (mod) {
29
- if (mod && mod.__esModule) return mod;
30
- var result = {};
31
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
- __setModuleDefault(result, mod);
33
- return result;
34
- };
35
- })();
36
- var __importDefault = (this && this.__importDefault) || function (mod) {
37
- return (mod && mod.__esModule) ? mod : { "default": mod };
38
- };
39
- Object.defineProperty(exports, "__esModule", { value: true });
40
- exports.runCli = runCli;
41
- const fs = __importStar(require("fs"));
42
- const path = __importStar(require("path"));
43
- const inquirerer_1 = require("inquirerer");
44
- const minimist_1 = __importDefault(require("minimist"));
45
- const clone_1 = require("./clone");
46
- const index_1 = require("./index");
47
- const package_json_1 = __importDefault(require("../package.json"));
48
- const DEFAULT_REPO = "https://github.com/launchql/pgpm-boilerplates.git";
49
- const DEFAULT_PATH = ".";
50
- const DEFAULT_OUTPUT_FALLBACK = "create-gen-app-output";
51
- const PACKAGE_VERSION = package_json_1.default.version ?? "0.0.0";
52
- const RESERVED_ARG_KEYS = new Set([
53
- "_",
54
- "repo",
55
- "r",
56
- "branch",
57
- "b",
58
- "path",
59
- "p",
60
- "template",
61
- "t",
62
- "output",
63
- "o",
64
- "force",
65
- "f",
66
- "help",
67
- "h",
68
- "version",
69
- "v",
70
- "no-tty",
71
- "n",
72
- ]);
73
- async function runCli(rawArgv = process.argv.slice(2)) {
74
- const args = (0, minimist_1.default)(rawArgv, {
75
- alias: {
76
- r: "repo",
77
- b: "branch",
78
- p: "path",
79
- t: "template",
80
- o: "output",
81
- f: "force",
82
- h: "help",
83
- v: "version",
84
- n: "no-tty",
85
- },
86
- string: ["repo", "branch", "path", "template", "output"],
87
- boolean: ["force", "help", "version", "no-tty"],
88
- default: {
89
- repo: DEFAULT_REPO,
90
- path: DEFAULT_PATH,
91
- },
92
- });
93
- if (args.help) {
94
- printHelp();
95
- return;
96
- }
97
- if (args.version) {
98
- printVersion();
99
- return;
100
- }
101
- if (!args.output && args._[0]) {
102
- args.output = args._[0];
103
- }
104
- let tempDir = null;
105
- try {
106
- console.log(`Cloning template from ${args.repo}...`);
107
- if (args.branch) {
108
- console.log(`Using branch ${args.branch}`);
109
- }
110
- tempDir = await (0, clone_1.cloneRepo)(args.repo, { branch: args.branch });
111
- const selectionRoot = path.join(tempDir, args.path);
112
- if (!fs.existsSync(selectionRoot) || !fs.statSync(selectionRoot).isDirectory()) {
113
- throw new Error(`Template path "${args.path}" does not exist in ${args.repo}`);
114
- }
115
- const templates = fs
116
- .readdirSync(selectionRoot, { withFileTypes: true })
117
- .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
118
- .map((entry) => entry.name)
119
- .sort();
120
- if (templates.length === 0) {
121
- throw new Error("No template folders found in repository");
122
- }
123
- let selectedTemplate = args.template;
124
- if (selectedTemplate) {
125
- if (!templates.includes(selectedTemplate)) {
126
- throw new Error(`Template "${selectedTemplate}" not found in ${args.repo}${args.path === "." ? "" : `/${args.path}`}`);
127
- }
128
- }
129
- else if (templates.length === 1) {
130
- selectedTemplate = templates[0];
131
- console.log(`Using the only available template: ${selectedTemplate}`);
132
- }
133
- else {
134
- selectedTemplate = await promptForTemplate(templates);
135
- }
136
- if (!selectedTemplate) {
137
- throw new Error("Template selection failed");
138
- }
139
- const normalizedBasePath = args.path === "." || args.path === "./"
140
- ? ""
141
- : args.path.replace(/^[./]+/, "").replace(/\/+$/, "");
142
- const fromPath = normalizedBasePath
143
- ? path.join(normalizedBasePath, selectedTemplate)
144
- : selectedTemplate;
145
- const outputDir = resolveOutputDir(args.output, selectedTemplate);
146
- ensureOutputDir(outputDir, Boolean(args.force));
147
- const answerOverrides = extractAnswerOverrides(args);
148
- const noTty = Boolean(args["no-tty"] ?? args.noTty);
149
- await (0, index_1.createGen)({
150
- templateUrl: args.repo,
151
- fromBranch: args.branch,
152
- fromPath,
153
- outputDir,
154
- argv: answerOverrides,
155
- noTty,
156
- });
157
- console.log(`\n✨ Done! Project ready at ${outputDir}`);
158
- return { outputDir, template: selectedTemplate };
159
- }
160
- finally {
161
- if (tempDir && fs.existsSync(tempDir)) {
162
- fs.rmSync(tempDir, { recursive: true, force: true });
163
- }
164
- }
165
- }
166
- function printHelp() {
167
- console.log(`
168
- create-gen-app CLI
169
-
170
- Usage:
171
- create-gen-app [options] [outputDir]
172
- cga [options] [outputDir]
173
-
174
- Options:
175
- -r, --repo <url> Git repository to clone (default: ${DEFAULT_REPO})
176
- -b, --branch <name> Branch to use when cloning
177
- -p, --path <dir> Subdirectory that contains templates (default: .)
178
- -t, --template <name> Template folder to use (will prompt if omitted)
179
- -o, --output <dir> Output directory (defaults to ./<template>)
180
- -f, --force Overwrite the output directory if it exists
181
- -v, --version Show CLI version
182
- -n, --no-tty Disable TTY mode for prompts
183
- -h, --help Show this help message
184
-
185
- You can also pass variable overrides, e.g.:
186
- create-gen-app --template module --PROJECT_NAME my-app
187
- `);
188
- }
189
- function printVersion() {
190
- console.log(`create-gen-app v${PACKAGE_VERSION}`);
191
- }
192
- async function promptForTemplate(templates) {
193
- const prompter = new inquirerer_1.Inquirerer();
194
- const question = {
195
- type: "list",
196
- name: "template",
197
- message: "Which template would you like to use?",
198
- options: templates,
199
- required: true,
200
- };
201
- try {
202
- const answers = (await prompter.prompt({}, [question]));
203
- return answers.template;
204
- }
205
- finally {
206
- if (typeof prompter.close === "function") {
207
- prompter.close();
208
- }
209
- }
210
- }
211
- function resolveOutputDir(outputArg, template) {
212
- const base = outputArg ?? (template ? path.join(process.cwd(), template) : DEFAULT_OUTPUT_FALLBACK);
213
- return path.resolve(base);
214
- }
215
- function ensureOutputDir(outputDir, force) {
216
- if (!fs.existsSync(outputDir)) {
217
- return;
218
- }
219
- if (!force) {
220
- throw new Error(`Output directory "${outputDir}" already exists. Use --force to overwrite or choose another path.`);
221
- }
222
- fs.rmSync(outputDir, { recursive: true, force: true });
223
- }
224
- function extractAnswerOverrides(args) {
225
- const overrides = {};
226
- for (const [key, value] of Object.entries(args)) {
227
- if (RESERVED_ARG_KEYS.has(key)) {
228
- continue;
229
- }
230
- overrides[key] = value;
231
- }
232
- return overrides;
233
- }
234
- if (require.main === module) {
235
- runCli().catch((error) => {
236
- console.error(error instanceof Error ? error.message : error);
237
- process.exitCode = 1;
238
- });
239
- }