Skip to content
This repository was archived by the owner on Nov 23, 2025. It is now read-only.

Commit 0bd4dc1

Browse files
authored
Merge pull request #2 from hyperweb-io/feat/create-gen-app-cli
feat: ship create-gen-app CLI, real integration tests, and license automation
2 parents e1b5c30 + 389253a commit 0bd4dc1

File tree

22 files changed

+1507
-239
lines changed

22 files changed

+1507
-239
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
**/.DS_Store
33
**/dist
44
**/yarn-error.log
5-
lerna-debug.log
5+
lerna-debug.log
6+
test-output

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"eslint-config-prettier": "^9.1.0",
3232
"eslint-plugin-simple-import-sort": "^10.0.0",
3333
"eslint-plugin-unused-imports": "^3.0.0",
34+
"@jest/test-sequencer": "^29.6.2",
3435
"jest": "^29.6.2",
3536
"lerna": "^6",
3637
"prettier": "^3.0.2",

packages/create-gen-app/README.md

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,49 +19,75 @@ A TypeScript library for cloning and customizing template repositories with vari
1919
## Features
2020

2121
- Clone GitHub repositories or any git URL
22-
- Extract template variables from filenames and file contents using `__VARIABLE__` syntax
22+
- Extract template variables from filenames and file contents using `____VARIABLE____` syntax
2323
- Load custom questions from `.questions.json` or `.questions.js` files
2424
- Interactive prompts using inquirerer with CLI argument support
2525
- Stream-based file processing for efficient variable replacement
26+
- Auto-generate `LICENSE` file content (MIT, Apache-2.0, ISC) based on user answers
2627

2728
## Installation
2829

2930
```bash
3031
npm install create-gen-app
3132
```
3233

33-
## Usage
34+
## CLI Usage
35+
36+
Install globally (or use `npx`) and run the CLI. It defaults to cloning `launchql/pgpm-boilerplates` and listing the templates under the root directory:
37+
38+
```bash
39+
npm install -g create-gen-app
40+
41+
# Select a template interactively and generate it into ./workspace
42+
create-gen-app --output ./workspace
43+
44+
# Use the short alias and skip the prompt
45+
cga --template module --branch main --output ./module --LICENSE MIT
46+
```
47+
48+
Key flags:
49+
50+
- `--repo`, `--branch`, `--path`: point at any git repository / branch / subdirectory
51+
- `--template`: pick a folder inside `--path`; omitted means the CLI will prompt (or auto-select if only one)
52+
- `--output`: destination directory (defaults to `./<template>`); `--force` overwrites if it already exists
53+
- `--no-tty`: disable interactive mode for CI
54+
- `--version`: show the installed CLI version and exit
55+
- Any additional flags are forwarded as variable overrides (e.g. `--PACKAGE_NAME my-app`)
56+
57+
## Library Usage
3458

3559
### Basic Usage
3660

3761
```typescript
38-
import { createGen } from 'create-gen-app';
62+
import { createGen } from "create-gen-app";
3963

4064
await createGen({
41-
templateUrl: 'https://github.com/user/template-repo',
42-
outputDir: './my-new-project',
65+
templateUrl: "https://github.com/user/template-repo",
66+
outputDir: "./my-new-project",
4367
argv: {
44-
PROJECT_NAME: 'my-project',
45-
AUTHOR: 'John Doe'
46-
}
68+
PROJECT_NAME: "my-project",
69+
AUTHOR: "John Doe",
70+
},
4771
});
4872
```
4973

5074
### Template Variables
5175

52-
Variables in your template should be wrapped in double underscores:
76+
Variables in your template should be wrapped in `____` (four underscores) on both sides:
5377

5478
**Filename variables:**
79+
5580
```
56-
__PROJECT_NAME__/
57-
__MODULE_NAME__.ts
81+
____PROJECT_NAME____/
82+
____MODULE_NAME____.ts
5883
```
5984

6085
**Content variables:**
86+
6187
```typescript
62-
// __MODULE_NAME__.ts
63-
export const projectName = "__PROJECT_NAME__";
64-
export const author = "__AUTHOR__";
88+
// ____MODULE_NAME____.ts
89+
export const projectName = "____PROJECT_NAME____";
90+
export const author = "____AUTHOR____";
6591
```
6692

6793
### Custom Questions
@@ -97,12 +123,12 @@ Or use `.questions.js` for dynamic questions:
97123
module.exports = {
98124
questions: [
99125
{
100-
name: 'PROJECT_NAME',
101-
type: 'text',
102-
message: 'What is your project name?',
103-
required: true
104-
}
105-
]
126+
name: "PROJECT_NAME",
127+
type: "text",
128+
message: "What is your project name?",
129+
required: true,
130+
},
131+
],
106132
};
107133
```
108134

@@ -113,6 +139,7 @@ module.exports = {
113139
Main function to create a project from a template.
114140

115141
**Options:**
142+
116143
- `templateUrl` (string): URL or path to the template repository
117144
- `outputDir` (string): Destination directory for the generated project
118145
- `argv` (Record<string, any>): Command-line arguments to pre-populate answers
@@ -133,17 +160,19 @@ Replace variables in all files and filenames.
133160
## Variable Naming Rules
134161

135162
Variables can contain:
163+
136164
- Letters (a-z, A-Z)
137165
- Numbers (0-9)
138-
- Underscores (_)
166+
- Underscores (\_)
139167
- Must start with a letter or underscore
140168

141169
Examples of valid variables:
142-
- `__PROJECT_NAME__`
143-
- `__author__`
144-
- `__CamelCase__`
145-
- `__snake_case__`
146-
- `__VERSION_1__`
170+
171+
- `____PROJECT_NAME____`
172+
- `____author____`
173+
- `____CamelCase____`
174+
- `____snake_case____`
175+
- `____VERSION_1____`
147176

148177
## License
149178

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
4+
import { runCli } from "../src/cli";
5+
import {
6+
TEST_BRANCH,
7+
TEST_REPO,
8+
TEST_TEMPLATE,
9+
buildAnswers,
10+
cleanupWorkspace,
11+
createTempWorkspace,
12+
} from "../test-utils/integration-helpers";
13+
14+
jest.setTimeout(180_000);
15+
16+
describe("CLI integration (GitHub templates)", () => {
17+
it("generates a project using the real repo", async () => {
18+
const workspace = createTempWorkspace("cli");
19+
const answers = buildAnswers("cli");
20+
21+
const args = [
22+
"--repo",
23+
TEST_REPO,
24+
"--branch",
25+
TEST_BRANCH,
26+
"--path",
27+
".",
28+
"--template",
29+
TEST_TEMPLATE,
30+
"--output",
31+
workspace.outputDir,
32+
"--no-tty",
33+
];
34+
35+
for (const [key, value] of Object.entries(answers)) {
36+
args.push(`--${key}`, value);
37+
}
38+
39+
try {
40+
const result = await runCli(args);
41+
expect(result).toBeDefined();
42+
if (!result) {
43+
return;
44+
}
45+
46+
expect(result.template).toBe(TEST_TEMPLATE);
47+
expect(result.outputDir).toBe(path.resolve(workspace.outputDir));
48+
49+
const pkgPath = path.join(workspace.outputDir, "package.json");
50+
expect(fs.existsSync(pkgPath)).toBe(true);
51+
52+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
53+
expect(pkg.name).toBe(answers.PACKAGE_IDENTIFIER);
54+
expect(pkg.license).toBe(answers.LICENSE);
55+
56+
const licenseContent = fs.readFileSync(
57+
path.join(workspace.outputDir, "LICENSE"),
58+
"utf8"
59+
);
60+
expect(licenseContent).toContain("MIT License");
61+
expect(licenseContent).toContain(answers.USERFULLNAME);
62+
} finally {
63+
cleanupWorkspace(workspace);
64+
}
65+
});
66+
67+
it("prints version and exits when --version is provided", async () => {
68+
const logSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
69+
70+
await runCli(["--version"]);
71+
72+
expect(logSpy).toHaveBeenCalledWith(expect.stringMatching(/create-gen-app v/));
73+
logSpy.mockRestore();
74+
});
75+
});
76+
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as fs from "fs";
2+
import * as path from "path";
3+
4+
import { createGen } from "../src";
5+
import {
6+
TEST_BRANCH,
7+
TEST_REPO,
8+
TEST_TEMPLATE,
9+
buildAnswers,
10+
cleanupWorkspace,
11+
createTempWorkspace,
12+
} from "../test-utils/integration-helpers";
13+
14+
jest.setTimeout(180_000);
15+
16+
describe("createGen integration (GitHub templates)", () => {
17+
it("clones the default repo and generates the module template", async () => {
18+
const workspace = createTempWorkspace("flow");
19+
20+
try {
21+
const answers = buildAnswers("flow");
22+
const result = await createGen({
23+
templateUrl: TEST_REPO,
24+
fromBranch: TEST_BRANCH,
25+
fromPath: TEST_TEMPLATE,
26+
outputDir: workspace.outputDir,
27+
argv: answers,
28+
noTty: true,
29+
});
30+
31+
expect(result).toBe(workspace.outputDir);
32+
33+
const packageJsonPath = path.join(workspace.outputDir, "package.json");
34+
expect(fs.existsSync(packageJsonPath)).toBe(true);
35+
36+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
37+
expect(pkg.name).toBe(answers.PACKAGE_IDENTIFIER);
38+
expect(pkg.license).toBe(answers.LICENSE);
39+
expect(pkg.author).toContain(answers.USERFULLNAME);
40+
41+
const questionsJsonPath = path.join(
42+
workspace.outputDir,
43+
".questions.json"
44+
);
45+
expect(fs.existsSync(questionsJsonPath)).toBe(false);
46+
47+
const licensePath = path.join(workspace.outputDir, "LICENSE");
48+
expect(fs.existsSync(licensePath)).toBe(true);
49+
const licenseContent = fs.readFileSync(licensePath, "utf8");
50+
expect(licenseContent).toContain(answers.USERFULLNAME);
51+
expect(licenseContent).toContain("MIT License");
52+
} finally {
53+
cleanupWorkspace(workspace);
54+
}
55+
});
56+
});
57+

0 commit comments

Comments
 (0)