@rindrics/initrepo 0.0.1 → 0.1.5
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/.github/codeql/codeql-config.yml +7 -0
- package/.github/dependabot.yml +11 -0
- package/.github/release.yml +4 -0
- package/.github/workflows/ci.yml +67 -0
- package/.github/workflows/codeql.yml +46 -0
- package/.github/workflows/publish.yml +35 -0
- package/.github/workflows/tagpr.yml +21 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-push +2 -0
- package/.tagpr +7 -0
- package/.tool-versions +1 -0
- package/CHANGELOG.md +28 -0
- package/README.md +40 -28
- package/biome.json +38 -0
- package/bun.lock +334 -0
- package/commitlint.config.js +3 -0
- package/dist/cli.js +11215 -0
- package/docs/adr/0001-simple-module-structure-over-ddd.md +111 -0
- package/package.json +37 -7
- package/src/cli.test.ts +20 -0
- package/src/cli.ts +27 -0
- package/src/commands/init.test.ts +170 -0
- package/src/commands/init.ts +172 -0
- package/src/commands/prepare-release.test.ts +183 -0
- package/src/commands/prepare-release.ts +354 -0
- package/src/config.ts +13 -0
- package/src/generators/project.test.ts +363 -0
- package/src/generators/project.ts +300 -0
- package/src/templates/common/dependabot.yml.ejs +12 -0
- package/src/templates/common/release.yml.ejs +4 -0
- package/src/templates/common/workflows/tagpr.yml.ejs +31 -0
- package/src/templates/typescript/.tagpr.ejs +5 -0
- package/src/templates/typescript/codeql/codeql-config.yml.ejs +7 -0
- package/src/templates/typescript/package.json.ejs +29 -0
- package/src/templates/typescript/src/index.ts.ejs +1 -0
- package/src/templates/typescript/tsconfig.json.ejs +17 -0
- package/src/templates/typescript/workflows/ci.yml.ejs +58 -0
- package/src/templates/typescript/workflows/codeql.yml.ejs +46 -0
- package/src/types.ts +13 -0
- package/src/utils/github-repo.test.ts +34 -0
- package/src/utils/github-repo.ts +141 -0
- package/src/utils/github.ts +47 -0
- package/src/utils/npm.test.ts +99 -0
- package/src/utils/npm.ts +59 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# ADR 0001: Simple Module Structure Over DDD
|
|
2
|
+
|
|
3
|
+
## Status
|
|
4
|
+
|
|
5
|
+
Accepted
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
We are building `@rindrics/initrepo`, a CLI tool that generates project scaffolding with:
|
|
10
|
+
|
|
11
|
+
- CI/CD workflows (test, lint, tagpr, release)
|
|
12
|
+
- husky configuration
|
|
13
|
+
- Language-specific setup (TypeScript initially)
|
|
14
|
+
- GitHub repository setup (repo creation, tags, branch protection)
|
|
15
|
+
|
|
16
|
+
The tool has two main commands:
|
|
17
|
+
|
|
18
|
+
1. `init` - Initialize a new project with all configurations
|
|
19
|
+
2. `replace-devcode` - Replace development placeholder with production name and enable release workflow
|
|
20
|
+
|
|
21
|
+
We needed to decide on an appropriate architecture for this tool.
|
|
22
|
+
|
|
23
|
+
## Decision
|
|
24
|
+
|
|
25
|
+
We chose a **simple module-based structure** instead of Domain-Driven Design (DDD).
|
|
26
|
+
|
|
27
|
+
### Chosen Structure
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
src/
|
|
31
|
+
├── cli.ts # CLI entry point (Commander.js)
|
|
32
|
+
├── commands/
|
|
33
|
+
│ ├── init.ts # init command implementation
|
|
34
|
+
│ └── replace-devcode.ts # replace-devcode command implementation
|
|
35
|
+
├── generators/
|
|
36
|
+
│ ├── languages/ # Language-specific logic
|
|
37
|
+
│ │ ├── index.ts # Common interface & factory
|
|
38
|
+
│ │ └── typescript.ts # TypeScript generator
|
|
39
|
+
│ ├── project.ts # package.json, etc.
|
|
40
|
+
│ ├── cicd.ts # GitHub Actions workflows
|
|
41
|
+
│ └── husky.ts # husky setup
|
|
42
|
+
├── github/
|
|
43
|
+
│ └── client.ts # GitHub API operations (Octokit)
|
|
44
|
+
├── templates/
|
|
45
|
+
│ ├── common/ # Language-agnostic templates
|
|
46
|
+
│ └── typescript/ # TypeScript-specific templates
|
|
47
|
+
└── types.ts # Shared type definitions
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Why Not DDD?
|
|
51
|
+
|
|
52
|
+
DDD excels when:
|
|
53
|
+
|
|
54
|
+
- Complex business rules exist (aggregates, value objects, domain events)
|
|
55
|
+
- A ubiquitous language with domain experts is needed
|
|
56
|
+
- The domain model evolves over time
|
|
57
|
+
|
|
58
|
+
This tool is essentially a **file generation utility**:
|
|
59
|
+
|
|
60
|
+
- **Input**: Project name, language, options
|
|
61
|
+
- **Output**: Generated files, GitHub repository
|
|
62
|
+
|
|
63
|
+
There are no:
|
|
64
|
+
|
|
65
|
+
- Domain entities to model
|
|
66
|
+
- Complex business invariants to protect
|
|
67
|
+
- Data persistence requiring repository patterns
|
|
68
|
+
- Use cases complex enough to warrant a separate application layer
|
|
69
|
+
|
|
70
|
+
### Language Support Strategy
|
|
71
|
+
|
|
72
|
+
Languages are supported through two mechanisms:
|
|
73
|
+
|
|
74
|
+
1. **`generators/languages/*.ts`** - Logic for each language (what files to generate, what dependencies to include, CI configuration)
|
|
75
|
+
2. **`templates/{lang}/`** - Actual template files for each language
|
|
76
|
+
|
|
77
|
+
A common `LanguageGenerator` interface ensures consistent behavior:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
interface LanguageGenerator {
|
|
81
|
+
name: string;
|
|
82
|
+
getFiles(): GeneratedFile[];
|
|
83
|
+
getDependencies(): Dependencies;
|
|
84
|
+
getCiConfig(): CiConfig;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Adding a new language requires:
|
|
89
|
+
|
|
90
|
+
1. Implementing `LanguageGenerator` in `generators/languages/`
|
|
91
|
+
2. Adding templates in `templates/{lang}/`
|
|
92
|
+
3. Registering in the factory function
|
|
93
|
+
|
|
94
|
+
## Consequences
|
|
95
|
+
|
|
96
|
+
### Positive
|
|
97
|
+
|
|
98
|
+
- **Simplicity**: Code is easy to navigate and understand
|
|
99
|
+
- **Testability**: Each module can be tested in isolation
|
|
100
|
+
- **Extensibility**: New generators or languages can be added without touching existing code
|
|
101
|
+
- **Right-sized**: No unnecessary abstraction layers
|
|
102
|
+
- **Fast development**: Less boilerplate, quicker iterations
|
|
103
|
+
|
|
104
|
+
### Negative
|
|
105
|
+
|
|
106
|
+
- **Limited structure for growth**: If the tool grows significantly in complexity, we may need to introduce more structure
|
|
107
|
+
- **No enforced boundaries**: Developers must exercise discipline to maintain separation of concerns
|
|
108
|
+
|
|
109
|
+
### Neutral
|
|
110
|
+
|
|
111
|
+
- If complex domain logic emerges (e.g., conditional generation rules, plugin systems), we can introduce targeted patterns without full DDD adoption
|
package/package.json
CHANGED
|
@@ -1,10 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rindrics/initrepo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "setup GitHub repo with dev tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"initrepo": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "bun run src/cli.ts",
|
|
11
|
+
"build": "bun build src/cli.ts --outdir dist --target node",
|
|
12
|
+
"lint": "biome lint src",
|
|
13
|
+
"format": "biome format src --write",
|
|
14
|
+
"check": "biome check src",
|
|
15
|
+
"test": "bun test",
|
|
16
|
+
"clean": "rm -rf test-* dist",
|
|
17
|
+
"prepare": "husky"
|
|
18
|
+
},
|
|
19
|
+
"keywords": ["cli", "scaffold", "setup", "repository"],
|
|
20
|
+
"author": "Rindrics",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/Rindrics/initrepo.git"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@biomejs/biome": "^2.0.0",
|
|
28
|
+
"@commitlint/cli": "^19.0.0",
|
|
29
|
+
"@commitlint/config-conventional": "^20.2.0",
|
|
30
|
+
"@types/ejs": "^3.1.5",
|
|
31
|
+
"bun-types": "^1.0.0",
|
|
32
|
+
"husky": "^9.0.0",
|
|
33
|
+
"typescript": "^5.0.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"commander": "^14.0.2",
|
|
37
|
+
"ejs": "^3.1.0",
|
|
38
|
+
"octokit": "^5.0.5"
|
|
39
|
+
}
|
|
10
40
|
}
|
package/src/cli.test.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import packageJson from '../package.json';
|
|
3
|
+
import { createProgram } from './cli';
|
|
4
|
+
|
|
5
|
+
describe('CLI', () => {
|
|
6
|
+
test('should have correct name from package.json', () => {
|
|
7
|
+
const program = createProgram();
|
|
8
|
+
expect(program.name()).toBe(packageJson.name);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('should have correct version from package.json', () => {
|
|
12
|
+
const program = createProgram();
|
|
13
|
+
expect(program.version()).toBe(packageJson.version);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('should have description', () => {
|
|
17
|
+
const program = createProgram();
|
|
18
|
+
expect(program.description()).toBe('Rapid repository setup CLI tool');
|
|
19
|
+
});
|
|
20
|
+
});
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import packageJson from '../package.json';
|
|
4
|
+
import { registerInitCommand } from './commands/init';
|
|
5
|
+
import { registerPrepareReleaseCommand } from './commands/prepare-release';
|
|
6
|
+
|
|
7
|
+
const { version: VERSION, name: NAME } = packageJson;
|
|
8
|
+
|
|
9
|
+
export function createProgram(): Command {
|
|
10
|
+
const program = new Command();
|
|
11
|
+
|
|
12
|
+
program
|
|
13
|
+
.name(NAME)
|
|
14
|
+
.description('Rapid repository setup CLI tool')
|
|
15
|
+
.version(VERSION);
|
|
16
|
+
|
|
17
|
+
registerInitCommand(program);
|
|
18
|
+
registerPrepareReleaseCommand(program);
|
|
19
|
+
|
|
20
|
+
return program;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Run CLI when executed directly
|
|
24
|
+
if (import.meta.main) {
|
|
25
|
+
const program = createProgram();
|
|
26
|
+
program.parse();
|
|
27
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import * as projectGenerator from '../generators/project';
|
|
4
|
+
import {
|
|
5
|
+
initProject,
|
|
6
|
+
promptYesNo,
|
|
7
|
+
registerInitCommand,
|
|
8
|
+
validateLanguage,
|
|
9
|
+
} from './init';
|
|
10
|
+
|
|
11
|
+
describe('init command', () => {
|
|
12
|
+
describe('validateLanguage', () => {
|
|
13
|
+
test('should accept typescript', () => {
|
|
14
|
+
expect(validateLanguage('typescript')).toBe('typescript');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should throw for unsupported language', () => {
|
|
18
|
+
expect(() => validateLanguage('python')).toThrow(
|
|
19
|
+
'Unsupported language: python',
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('initProject', () => {
|
|
25
|
+
let consoleSpy: ReturnType<typeof spyOn>;
|
|
26
|
+
let generateProjectSpy: ReturnType<typeof spyOn>;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
consoleSpy = spyOn(console, 'log').mockImplementation(() => {});
|
|
30
|
+
// Mock generateProject to avoid file operations and network calls
|
|
31
|
+
generateProjectSpy = spyOn(
|
|
32
|
+
projectGenerator,
|
|
33
|
+
'generateProject',
|
|
34
|
+
).mockResolvedValue(undefined);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
consoleSpy.mockRestore();
|
|
39
|
+
generateProjectSpy.mockRestore();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should log project creation message', async () => {
|
|
43
|
+
await initProject({
|
|
44
|
+
projectName: 'test-project',
|
|
45
|
+
lang: 'typescript',
|
|
46
|
+
isDevcode: false,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
50
|
+
'Creating project: test-project (typescript)',
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should include devcode label when isDevcode is true', async () => {
|
|
55
|
+
await initProject({
|
|
56
|
+
projectName: 'test-project',
|
|
57
|
+
lang: 'typescript',
|
|
58
|
+
isDevcode: true,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
62
|
+
'Creating project: test-project (typescript) [devcode]',
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('should pass author option to generateProject', async () => {
|
|
67
|
+
await initProject({
|
|
68
|
+
projectName: 'test-project',
|
|
69
|
+
lang: 'typescript',
|
|
70
|
+
isDevcode: false,
|
|
71
|
+
author: 'custom-author',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(generateProjectSpy).toHaveBeenCalledWith({
|
|
75
|
+
projectName: 'test-project',
|
|
76
|
+
lang: 'typescript',
|
|
77
|
+
isDevcode: false,
|
|
78
|
+
author: 'custom-author',
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('registerInitCommand', () => {
|
|
84
|
+
test('should register init command to program', () => {
|
|
85
|
+
const program = new Command();
|
|
86
|
+
registerInitCommand(program);
|
|
87
|
+
|
|
88
|
+
const initCmd = program.commands.find((cmd) => cmd.name() === 'init');
|
|
89
|
+
expect(initCmd).toBeDefined();
|
|
90
|
+
expect(initCmd?.description()).toContain('npm publish');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('should have --devcode option', () => {
|
|
94
|
+
const program = new Command();
|
|
95
|
+
registerInitCommand(program);
|
|
96
|
+
|
|
97
|
+
const initCmd = program.commands.find((cmd) => cmd.name() === 'init');
|
|
98
|
+
const devcodeOption = initCmd?.options.find(
|
|
99
|
+
(opt) => opt.long === '--devcode',
|
|
100
|
+
);
|
|
101
|
+
expect(devcodeOption).toBeDefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('should have --author option', () => {
|
|
105
|
+
const program = new Command();
|
|
106
|
+
registerInitCommand(program);
|
|
107
|
+
|
|
108
|
+
const initCmd = program.commands.find((cmd) => cmd.name() === 'init');
|
|
109
|
+
const authorOption = initCmd?.options.find(
|
|
110
|
+
(opt) => opt.long === '--author',
|
|
111
|
+
);
|
|
112
|
+
expect(authorOption).toBeDefined();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should have --create-repo option', () => {
|
|
116
|
+
const program = new Command();
|
|
117
|
+
registerInitCommand(program);
|
|
118
|
+
|
|
119
|
+
const initCmd = program.commands.find((cmd) => cmd.name() === 'init');
|
|
120
|
+
const createRepoOption = initCmd?.options.find(
|
|
121
|
+
(opt) => opt.long === '--create-repo',
|
|
122
|
+
);
|
|
123
|
+
expect(createRepoOption).toBeDefined();
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('should have --no-create-repo option', () => {
|
|
127
|
+
const program = new Command();
|
|
128
|
+
registerInitCommand(program);
|
|
129
|
+
|
|
130
|
+
const initCmd = program.commands.find((cmd) => cmd.name() === 'init');
|
|
131
|
+
const noCreateRepoOption = initCmd?.options.find(
|
|
132
|
+
(opt) => opt.long === '--no-create-repo',
|
|
133
|
+
);
|
|
134
|
+
expect(noCreateRepoOption).toBeDefined();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should have --no-devcode option', () => {
|
|
138
|
+
const program = new Command();
|
|
139
|
+
registerInitCommand(program);
|
|
140
|
+
|
|
141
|
+
const initCmd = program.commands.find((cmd) => cmd.name() === 'init');
|
|
142
|
+
const noDevcodeOption = initCmd?.options.find(
|
|
143
|
+
(opt) => opt.long === '--no-devcode',
|
|
144
|
+
);
|
|
145
|
+
expect(noDevcodeOption).toBeDefined();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('should have --private option', () => {
|
|
149
|
+
const program = new Command();
|
|
150
|
+
registerInitCommand(program);
|
|
151
|
+
|
|
152
|
+
const initCmd = program.commands.find((cmd) => cmd.name() === 'init');
|
|
153
|
+
const privateOption = initCmd?.options.find(
|
|
154
|
+
(opt) => opt.long === '--private',
|
|
155
|
+
);
|
|
156
|
+
expect(privateOption).toBeDefined();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('should have --no-private option', () => {
|
|
160
|
+
const program = new Command();
|
|
161
|
+
registerInitCommand(program);
|
|
162
|
+
|
|
163
|
+
const initCmd = program.commands.find((cmd) => cmd.name() === 'init');
|
|
164
|
+
const noPrivateOption = initCmd?.options.find(
|
|
165
|
+
(opt) => opt.long === '--no-private',
|
|
166
|
+
);
|
|
167
|
+
expect(noPrivateOption).toBeDefined();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import * as readline from 'node:readline/promises';
|
|
2
|
+
import type { Command } from 'commander';
|
|
3
|
+
import { generateProject } from '../generators/project';
|
|
4
|
+
import type { InitOptions, Language } from '../types';
|
|
5
|
+
import { createGitHubRepo, hasGitHubToken } from '../utils/github-repo';
|
|
6
|
+
|
|
7
|
+
const SUPPORTED_LANGUAGES: Language[] = ['typescript'];
|
|
8
|
+
|
|
9
|
+
export function validateLanguage(lang: string): Language {
|
|
10
|
+
if (!SUPPORTED_LANGUAGES.includes(lang as Language)) {
|
|
11
|
+
throw new Error(
|
|
12
|
+
`Unsupported language: ${lang}. Supported: ${SUPPORTED_LANGUAGES.join(', ')}`,
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return lang as Language;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Prompts user with a yes/no question
|
|
20
|
+
*/
|
|
21
|
+
export async function promptYesNo(
|
|
22
|
+
question: string,
|
|
23
|
+
defaultValue = false,
|
|
24
|
+
): Promise<boolean> {
|
|
25
|
+
const rl = readline.createInterface({
|
|
26
|
+
input: process.stdin,
|
|
27
|
+
output: process.stdout,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const hint = defaultValue ? '(Y/n)' : '(y/N)';
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const answer = await rl.question(`${question} ${hint}: `);
|
|
34
|
+
if (answer.trim() === '') {
|
|
35
|
+
return defaultValue;
|
|
36
|
+
}
|
|
37
|
+
return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
38
|
+
} finally {
|
|
39
|
+
rl.close();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface InitProjectOptions extends InitOptions {
|
|
44
|
+
createRepo?: boolean;
|
|
45
|
+
isPrivate?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function initProject(options: InitProjectOptions): Promise<void> {
|
|
49
|
+
const devcodeLabel = options.isDevcode ? ' [devcode]' : '';
|
|
50
|
+
console.log(
|
|
51
|
+
`Creating project: ${options.projectName} (${options.lang})${devcodeLabel}`,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Generate project files
|
|
55
|
+
await generateProject(options);
|
|
56
|
+
console.log(`✅ Project files created at ./${options.projectName}`);
|
|
57
|
+
|
|
58
|
+
// Create GitHub repository if requested
|
|
59
|
+
if (options.createRepo) {
|
|
60
|
+
if (!hasGitHubToken()) {
|
|
61
|
+
console.warn('⚠️ GITHUB_TOKEN not set, skipping repository creation');
|
|
62
|
+
console.warn(
|
|
63
|
+
' Set GITHUB_TOKEN environment variable to enable repo creation',
|
|
64
|
+
);
|
|
65
|
+
} else {
|
|
66
|
+
try {
|
|
67
|
+
console.log('📦 Creating GitHub repository...');
|
|
68
|
+
const result = await createGitHubRepo({
|
|
69
|
+
name: options.projectName,
|
|
70
|
+
description: options.isDevcode
|
|
71
|
+
? `[devcode] ${options.projectName}`
|
|
72
|
+
: undefined,
|
|
73
|
+
isPrivate: options.isPrivate ?? false,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (result.alreadyExisted) {
|
|
77
|
+
console.log(`ℹ️ Repository already exists: ${result.url}`);
|
|
78
|
+
console.log(` Labels ensured: tagpr:minor, tagpr:major`);
|
|
79
|
+
} else {
|
|
80
|
+
console.log(`✅ GitHub repository created: ${result.url}`);
|
|
81
|
+
console.log(` Labels created: tagpr:minor, tagpr:major`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log(`\n To push your code:`);
|
|
85
|
+
console.log(` cd ${options.projectName}`);
|
|
86
|
+
console.log(` git init`);
|
|
87
|
+
console.log(` git add .`);
|
|
88
|
+
console.log(` git commit -m "chore: initial commit"`);
|
|
89
|
+
console.log(` git remote add origin ${result.cloneUrl}`);
|
|
90
|
+
console.log(` git push -u origin main`);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(
|
|
93
|
+
`❌ Failed to create GitHub repository: ${error instanceof Error ? error.message : String(error)}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface InitCommandOptions {
|
|
101
|
+
lang: string;
|
|
102
|
+
devcode?: boolean;
|
|
103
|
+
author?: string;
|
|
104
|
+
createRepo?: boolean;
|
|
105
|
+
private?: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function registerInitCommand(program: Command): void {
|
|
109
|
+
program
|
|
110
|
+
.command('init <project-name>')
|
|
111
|
+
.description(
|
|
112
|
+
'Initialize a new project. <project-name> is the name used for npm publish.',
|
|
113
|
+
)
|
|
114
|
+
.option('-l, --lang <language>', 'Project language', 'typescript')
|
|
115
|
+
.option(
|
|
116
|
+
'-d, --devcode',
|
|
117
|
+
'Use devcode mode (adds private: true to package.json)',
|
|
118
|
+
)
|
|
119
|
+
.option('--no-devcode', 'Not a devcode project')
|
|
120
|
+
.option(
|
|
121
|
+
'-a, --author <name>',
|
|
122
|
+
'Package author (defaults to npm whoami for TypeScript)',
|
|
123
|
+
)
|
|
124
|
+
.option(
|
|
125
|
+
'--create-repo',
|
|
126
|
+
'Create GitHub repository and tagpr labels (requires GITHUB_TOKEN)',
|
|
127
|
+
)
|
|
128
|
+
.option('--no-create-repo', 'Skip GitHub repository creation')
|
|
129
|
+
.option('-p, --private', 'Make GitHub repository private')
|
|
130
|
+
.option('--no-private', 'Make GitHub repository public')
|
|
131
|
+
.action(async (projectName: string, opts: InitCommandOptions) => {
|
|
132
|
+
const lang = validateLanguage(opts.lang);
|
|
133
|
+
|
|
134
|
+
// Determine if devcode - prompt if not specified
|
|
135
|
+
let isDevcode: boolean;
|
|
136
|
+
if (opts.devcode !== undefined) {
|
|
137
|
+
isDevcode = opts.devcode;
|
|
138
|
+
} else {
|
|
139
|
+
isDevcode = await promptYesNo(
|
|
140
|
+
`Is "${projectName}" a developmental code?`,
|
|
141
|
+
false,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Determine if create repo - prompt if not specified
|
|
146
|
+
let createRepo: boolean;
|
|
147
|
+
if (opts.createRepo !== undefined) {
|
|
148
|
+
createRepo = opts.createRepo;
|
|
149
|
+
} else {
|
|
150
|
+
createRepo = await promptYesNo('Create GitHub repo?', true);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Determine if private repo - prompt only if creating repo and not specified
|
|
154
|
+
let isPrivate = false;
|
|
155
|
+
if (createRepo) {
|
|
156
|
+
if (opts.private !== undefined) {
|
|
157
|
+
isPrivate = opts.private;
|
|
158
|
+
} else {
|
|
159
|
+
isPrivate = await promptYesNo('Make repo private?', false);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await initProject({
|
|
164
|
+
projectName,
|
|
165
|
+
lang,
|
|
166
|
+
isDevcode,
|
|
167
|
+
author: opts.author,
|
|
168
|
+
createRepo,
|
|
169
|
+
isPrivate,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|