@rindrics/initrepo 0.0.1 → 0.1.4

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.
Files changed (45) hide show
  1. package/.github/codeql/codeql-config.yml +7 -0
  2. package/.github/dependabot.yml +11 -0
  3. package/.github/release.yml +4 -0
  4. package/.github/workflows/ci.yml +67 -0
  5. package/.github/workflows/codeql.yml +46 -0
  6. package/.github/workflows/publish.yml +35 -0
  7. package/.github/workflows/tagpr.yml +21 -0
  8. package/.husky/commit-msg +1 -0
  9. package/.husky/pre-push +2 -0
  10. package/.tagpr +7 -0
  11. package/.tool-versions +1 -0
  12. package/CHANGELOG.md +25 -0
  13. package/README.md +40 -28
  14. package/biome.json +38 -0
  15. package/bun.lock +334 -0
  16. package/commitlint.config.js +3 -0
  17. package/dist/cli.js +11215 -0
  18. package/docs/adr/0001-simple-module-structure-over-ddd.md +111 -0
  19. package/package.json +37 -7
  20. package/src/cli.test.ts +20 -0
  21. package/src/cli.ts +27 -0
  22. package/src/commands/init.test.ts +170 -0
  23. package/src/commands/init.ts +172 -0
  24. package/src/commands/prepare-release.test.ts +183 -0
  25. package/src/commands/prepare-release.ts +354 -0
  26. package/src/config.ts +13 -0
  27. package/src/generators/project.test.ts +363 -0
  28. package/src/generators/project.ts +300 -0
  29. package/src/templates/common/dependabot.yml.ejs +12 -0
  30. package/src/templates/common/release.yml.ejs +4 -0
  31. package/src/templates/common/workflows/tagpr.yml.ejs +31 -0
  32. package/src/templates/typescript/.tagpr.ejs +5 -0
  33. package/src/templates/typescript/codeql/codeql-config.yml.ejs +7 -0
  34. package/src/templates/typescript/package.json.ejs +29 -0
  35. package/src/templates/typescript/src/index.ts.ejs +1 -0
  36. package/src/templates/typescript/tsconfig.json.ejs +17 -0
  37. package/src/templates/typescript/workflows/ci.yml.ejs +58 -0
  38. package/src/templates/typescript/workflows/codeql.yml.ejs +46 -0
  39. package/src/types.ts +13 -0
  40. package/src/utils/github-repo.test.ts +34 -0
  41. package/src/utils/github-repo.ts +141 -0
  42. package/src/utils/github.ts +47 -0
  43. package/src/utils/npm.test.ts +99 -0
  44. package/src/utils/npm.ts +59 -0
  45. package/tsconfig.json +16 -0
@@ -0,0 +1,363 @@
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import { getLatestActionVersions } from '../utils/github';
5
+ import * as npmUtils from '../utils/npm';
6
+ import {
7
+ generateEntryPoint,
8
+ generatePackageJson,
9
+ generateProject,
10
+ generateTagprConfig,
11
+ generateTagprWorkflow,
12
+ generateTsconfig,
13
+ loadTemplate,
14
+ ProjectNameError,
15
+ TemplateError,
16
+ validateProjectName,
17
+ writeGeneratedFiles,
18
+ } from './project';
19
+
20
+ describe('project generator', () => {
21
+ const testDir = path.join(import.meta.dir, '../../.test-output');
22
+
23
+ beforeEach(async () => {
24
+ await fs.rm(testDir, { recursive: true, force: true });
25
+ });
26
+
27
+ afterEach(async () => {
28
+ await fs.rm(testDir, { recursive: true, force: true });
29
+ });
30
+
31
+ describe('loadTemplate', () => {
32
+ test('should throw TemplateError for path traversal attempt', async () => {
33
+ expect(loadTemplate('../../../etc/passwd', {})).rejects.toThrow(
34
+ TemplateError,
35
+ );
36
+ expect(loadTemplate('../../../etc/passwd', {})).rejects.toThrow(
37
+ 'resolves outside templates directory',
38
+ );
39
+ });
40
+
41
+ test('should throw TemplateError for non-existent template', async () => {
42
+ expect(loadTemplate('non-existent.ejs', {})).rejects.toThrow(
43
+ TemplateError,
44
+ );
45
+ expect(loadTemplate('non-existent.ejs', {})).rejects.toThrow(
46
+ 'Template not found',
47
+ );
48
+ });
49
+
50
+ test('should load and render valid template', async () => {
51
+ const result = await loadTemplate('typescript/package.json.ejs', {
52
+ name: 'test',
53
+ author: '',
54
+ isDevcode: false,
55
+ versions: {
56
+ '@biomejs/biome': '1.0.0',
57
+ '@commitlint/cli': '1.0.0',
58
+ '@commitlint/config-conventional': '1.0.0',
59
+ 'bun-types': '1.0.0',
60
+ husky: '1.0.0',
61
+ typescript: '1.0.0',
62
+ },
63
+ });
64
+
65
+ expect(result).toContain('"name": "test"');
66
+ });
67
+ });
68
+
69
+ describe('generatePackageJson', () => {
70
+ let getNpmUsernameSpy: ReturnType<typeof spyOn>;
71
+ let getLatestVersionsSpy: ReturnType<typeof spyOn>;
72
+
73
+ beforeEach(() => {
74
+ getNpmUsernameSpy = spyOn(npmUtils, 'getNpmUsername').mockResolvedValue(
75
+ 'mocked-user',
76
+ );
77
+ getLatestVersionsSpy = spyOn(
78
+ npmUtils,
79
+ 'getLatestVersions',
80
+ ).mockResolvedValue({
81
+ '@biomejs/biome': '1.0.0',
82
+ '@commitlint/cli': '1.0.0',
83
+ '@commitlint/config-conventional': '1.0.0',
84
+ 'bun-types': '1.0.0',
85
+ husky: '1.0.0',
86
+ typescript: '1.0.0',
87
+ });
88
+ });
89
+
90
+ afterEach(() => {
91
+ getNpmUsernameSpy.mockRestore();
92
+ getLatestVersionsSpy.mockRestore();
93
+ });
94
+
95
+ test('should generate package.json with project name', async () => {
96
+ const result = await generatePackageJson({
97
+ projectName: 'my-awesome-project',
98
+ lang: 'typescript',
99
+ isDevcode: false,
100
+ });
101
+
102
+ expect(result.path).toBe('package.json');
103
+ expect(result.content).toContain('"name": "my-awesome-project"');
104
+ });
105
+
106
+ test('should include standard scripts', async () => {
107
+ const result = await generatePackageJson({
108
+ projectName: 'test-project',
109
+ lang: 'typescript',
110
+ isDevcode: false,
111
+ });
112
+
113
+ expect(result.content).toContain('"dev"');
114
+ expect(result.content).toContain('"build"');
115
+ expect(result.content).toContain('"test"');
116
+ expect(result.content).toContain('"check"');
117
+ });
118
+
119
+ test('should include mocked author', async () => {
120
+ const result = await generatePackageJson({
121
+ projectName: 'test-project',
122
+ lang: 'typescript',
123
+ isDevcode: false,
124
+ });
125
+
126
+ expect(result.content).toContain('"author": "mocked-user"');
127
+ });
128
+ });
129
+
130
+ describe('writeGeneratedFiles', () => {
131
+ test('should create directory and write files', async () => {
132
+ const files = [
133
+ { path: 'test.txt', content: 'hello world' },
134
+ { path: 'nested/file.txt', content: 'nested content' },
135
+ ];
136
+
137
+ await writeGeneratedFiles(testDir, files);
138
+
139
+ const file1 = await fs.readFile(path.join(testDir, 'test.txt'), 'utf-8');
140
+ const file2 = await fs.readFile(
141
+ path.join(testDir, 'nested/file.txt'),
142
+ 'utf-8',
143
+ );
144
+
145
+ expect(file1).toBe('hello world');
146
+ expect(file2).toBe('nested content');
147
+ });
148
+ });
149
+
150
+ describe('validateProjectName', () => {
151
+ test('should accept valid project names', () => {
152
+ expect(() => validateProjectName('my-project')).not.toThrow();
153
+ expect(() => validateProjectName('my_project')).not.toThrow();
154
+ expect(() => validateProjectName('my-project-123')).not.toThrow();
155
+ expect(() => validateProjectName('@scope/my-project')).not.toThrow();
156
+ expect(() => validateProjectName('a')).not.toThrow();
157
+ });
158
+
159
+ test('should throw for empty project name', () => {
160
+ expect(() => validateProjectName('')).toThrow(ProjectNameError);
161
+ expect(() => validateProjectName(' ')).toThrow('cannot be empty');
162
+ });
163
+
164
+ test('should throw for path separators', () => {
165
+ expect(() => validateProjectName('my/project')).toThrow(ProjectNameError);
166
+ expect(() => validateProjectName('my\\project')).toThrow(
167
+ 'invalid path separators',
168
+ );
169
+ });
170
+
171
+ test('should throw for leading/trailing dots', () => {
172
+ expect(() => validateProjectName('.my-project')).toThrow(
173
+ ProjectNameError,
174
+ );
175
+ expect(() => validateProjectName('my-project.')).toThrow(
176
+ 'cannot start or end with a dot',
177
+ );
178
+ });
179
+
180
+ test('should throw for invalid characters', () => {
181
+ expect(() => validateProjectName('my project')).toThrow(ProjectNameError);
182
+ expect(() => validateProjectName('my$project')).toThrow(
183
+ 'invalid characters',
184
+ );
185
+ });
186
+ });
187
+
188
+ describe('generateTsconfig', () => {
189
+ test('should generate tsconfig.json', async () => {
190
+ const result = await generateTsconfig({
191
+ projectName: 'test-project',
192
+ lang: 'typescript',
193
+ isDevcode: false,
194
+ });
195
+
196
+ expect(result.path).toBe('tsconfig.json');
197
+ expect(result.content).toContain('"target": "ES2022"');
198
+ expect(result.content).toContain('"moduleResolution": "bundler"');
199
+ expect(result.content).toContain('"bun-types"');
200
+ });
201
+ });
202
+
203
+ describe('generateEntryPoint', () => {
204
+ test('should generate src/index.ts with project name', async () => {
205
+ const result = await generateEntryPoint({
206
+ projectName: 'my-cool-project',
207
+ lang: 'typescript',
208
+ isDevcode: false,
209
+ });
210
+
211
+ expect(result.path).toBe('src/index.ts');
212
+ expect(result.content).toContain('my-cool-project');
213
+ });
214
+ });
215
+
216
+ describe('generateTagprConfig', () => {
217
+ test('should generate .tagpr with versionFile for TypeScript', async () => {
218
+ const result = await generateTagprConfig({
219
+ projectName: 'test-project',
220
+ lang: 'typescript',
221
+ isDevcode: false,
222
+ });
223
+
224
+ expect(result.path).toBe('.tagpr');
225
+ expect(result.content).toContain('versionFile = "package.json"');
226
+ });
227
+ });
228
+
229
+ describe('generateTagprWorkflow', () => {
230
+ // Helper to extract major version number from "vN" format
231
+ function extractMajorVersion(content: string, action: string): number {
232
+ const regex = new RegExp(`${action.replace('/', '\\/')}@v(\\d+)`);
233
+ const match = content.match(regex);
234
+ return match ? Number.parseInt(match[1], 10) : 0;
235
+ }
236
+
237
+ test('should generate tagpr.yml for devcode project', async () => {
238
+ const actionVersions = await getLatestActionVersions();
239
+ const result = await generateTagprWorkflow(
240
+ {
241
+ projectName: 'test-devcode',
242
+ lang: 'typescript',
243
+ isDevcode: true,
244
+ },
245
+ actionVersions,
246
+ );
247
+
248
+ expect(result.path).toBe('.github/workflows/tagpr.yml');
249
+ expect(result.content).toContain(
250
+ 'GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}',
251
+ );
252
+ expect(result.content).toContain('# TODO: After replace-devcode');
253
+
254
+ // Version should be at least the minimum expected
255
+ expect(
256
+ extractMajorVersion(result.content, 'actions/checkout'),
257
+ ).toBeGreaterThanOrEqual(5);
258
+ expect(
259
+ extractMajorVersion(result.content, 'Songmu/tagpr'),
260
+ ).toBeGreaterThanOrEqual(1);
261
+ });
262
+
263
+ test('should generate tagpr.yml for production project', async () => {
264
+ const actionVersions = await getLatestActionVersions();
265
+ const result = await generateTagprWorkflow(
266
+ {
267
+ projectName: 'test-prod',
268
+ lang: 'typescript',
269
+ isDevcode: false,
270
+ },
271
+ actionVersions,
272
+ );
273
+
274
+ expect(result.path).toBe('.github/workflows/tagpr.yml');
275
+ expect(result.content).toContain(
276
+ 'GITHUB_TOKEN: ${{ secrets.PAT_FOR_TAGPR }}',
277
+ );
278
+ expect(result.content).not.toContain('# TODO: After replace-devcode');
279
+
280
+ // Version should be at least the minimum expected
281
+ expect(
282
+ extractMajorVersion(result.content, 'actions/checkout'),
283
+ ).toBeGreaterThanOrEqual(4);
284
+ expect(
285
+ extractMajorVersion(result.content, 'Songmu/tagpr'),
286
+ ).toBeGreaterThanOrEqual(1);
287
+ });
288
+ });
289
+
290
+ describe('generateProject', () => {
291
+ let getNpmUsernameSpy: ReturnType<typeof spyOn>;
292
+ let getLatestVersionsSpy: ReturnType<typeof spyOn>;
293
+
294
+ beforeEach(() => {
295
+ getNpmUsernameSpy = spyOn(npmUtils, 'getNpmUsername').mockResolvedValue(
296
+ 'mocked-user',
297
+ );
298
+ getLatestVersionsSpy = spyOn(
299
+ npmUtils,
300
+ 'getLatestVersions',
301
+ ).mockResolvedValue({
302
+ '@biomejs/biome': '1.0.0',
303
+ '@commitlint/cli': '1.0.0',
304
+ '@commitlint/config-conventional': '1.0.0',
305
+ 'bun-types': '1.0.0',
306
+ husky: '1.0.0',
307
+ typescript: '1.0.0',
308
+ });
309
+ });
310
+
311
+ afterEach(() => {
312
+ getNpmUsernameSpy.mockRestore();
313
+ getLatestVersionsSpy.mockRestore();
314
+ });
315
+
316
+ test('should throw for invalid project name', async () => {
317
+ expect(
318
+ generateProject({
319
+ projectName: '../evil-path',
320
+ lang: 'typescript',
321
+ isDevcode: false,
322
+ }),
323
+ ).rejects.toThrow(ProjectNameError);
324
+ });
325
+
326
+ test('should use targetDir for output while keeping projectName for package.json', async () => {
327
+ const projectDir = path.join(testDir, 'new-project');
328
+
329
+ await generateProject({
330
+ projectName: 'new-project',
331
+ lang: 'typescript',
332
+ isDevcode: false,
333
+ targetDir: projectDir,
334
+ });
335
+
336
+ const packageJsonPath = path.join(projectDir, 'package.json');
337
+ const content = await fs.readFile(packageJsonPath, 'utf-8');
338
+ const pkg = JSON.parse(content);
339
+
340
+ expect(pkg.name).toBe('new-project');
341
+ expect(pkg.author).toBe('mocked-user');
342
+ });
343
+
344
+ test('should generate all expected files', async () => {
345
+ const projectDir = path.join(testDir, 'full-project');
346
+
347
+ await generateProject({
348
+ projectName: 'full-project',
349
+ lang: 'typescript',
350
+ isDevcode: true,
351
+ targetDir: projectDir,
352
+ });
353
+
354
+ // Check all files exist
355
+ const files = await fs.readdir(projectDir, { recursive: true });
356
+ expect(files).toContain('package.json');
357
+ expect(files).toContain('tsconfig.json');
358
+ expect(files).toContain('.tagpr');
359
+ expect(files).toContain(path.join('src', 'index.ts'));
360
+ expect(files).toContain(path.join('.github', 'workflows', 'tagpr.yml'));
361
+ });
362
+ });
363
+ });
@@ -0,0 +1,300 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import ejs from 'ejs';
4
+ import type { InitOptions } from '../types';
5
+ import { getLatestActionVersions } from '../utils/github';
6
+ import { getLatestVersions, getNpmUsername } from '../utils/npm';
7
+
8
+ const TEMPLATES_DIR = path.join(import.meta.dir, '../templates');
9
+
10
+ const DEV_DEPENDENCIES = [
11
+ '@biomejs/biome',
12
+ '@commitlint/cli',
13
+ '@commitlint/config-conventional',
14
+ 'bun-types',
15
+ 'husky',
16
+ 'typescript',
17
+ ];
18
+
19
+ export interface GeneratedFile {
20
+ path: string;
21
+ content: string;
22
+ }
23
+
24
+ export class TemplateError extends Error {
25
+ constructor(
26
+ message: string,
27
+ public readonly templatePath: string,
28
+ public readonly cause?: Error,
29
+ ) {
30
+ super(message);
31
+ this.name = 'TemplateError';
32
+ }
33
+ }
34
+
35
+ export async function loadTemplate(
36
+ templatePath: string,
37
+ data: Record<string, unknown>,
38
+ ): Promise<string> {
39
+ const resolvedTemplatesDir = path.resolve(TEMPLATES_DIR);
40
+ const fullPath = path.resolve(TEMPLATES_DIR, templatePath);
41
+
42
+ if (!fullPath.startsWith(resolvedTemplatesDir + path.sep)) {
43
+ throw new TemplateError(
44
+ `Invalid template path: "${templatePath}" resolves outside templates directory`,
45
+ templatePath,
46
+ );
47
+ }
48
+
49
+ let template: string;
50
+ try {
51
+ template = await fs.readFile(fullPath, 'utf-8');
52
+ } catch (error) {
53
+ const fsError = error as NodeJS.ErrnoException;
54
+ if (fsError.code === 'ENOENT') {
55
+ throw new TemplateError(
56
+ `Template not found: "${templatePath}"`,
57
+ templatePath,
58
+ fsError,
59
+ );
60
+ }
61
+ throw new TemplateError(
62
+ `Failed to read template "${templatePath}": ${fsError.message}`,
63
+ templatePath,
64
+ fsError,
65
+ );
66
+ }
67
+
68
+ try {
69
+ return ejs.render(template, data);
70
+ } catch (error) {
71
+ const renderError = error as Error;
72
+ throw new TemplateError(
73
+ `Failed to render template "${templatePath}": ${renderError.message}`,
74
+ templatePath,
75
+ renderError,
76
+ );
77
+ }
78
+ }
79
+
80
+ export async function generatePackageJson(
81
+ options: InitOptions,
82
+ ): Promise<GeneratedFile> {
83
+ try {
84
+ const [versions, detectedAuthor] = await Promise.all([
85
+ getLatestVersions(DEV_DEPENDENCIES),
86
+ options.author ? Promise.resolve(options.author) : getNpmUsername(),
87
+ ]);
88
+ const author = detectedAuthor ?? '';
89
+ const templatePath = `${options.lang}/package.json.ejs`;
90
+ const content = await loadTemplate(templatePath, {
91
+ name: options.projectName,
92
+ isDevcode: options.isDevcode,
93
+ author,
94
+ versions,
95
+ });
96
+ return { path: 'package.json', content };
97
+ } catch (error) {
98
+ throw new Error(
99
+ `Failed to generate package.json: ${error instanceof Error ? error.message : String(error)}`,
100
+ );
101
+ }
102
+ }
103
+
104
+ export async function generateTsconfig(
105
+ options: InitOptions,
106
+ ): Promise<GeneratedFile> {
107
+ const content = await loadTemplate(`${options.lang}/tsconfig.json.ejs`, {});
108
+ return { path: 'tsconfig.json', content };
109
+ }
110
+
111
+ export async function generateEntryPoint(
112
+ options: InitOptions,
113
+ ): Promise<GeneratedFile> {
114
+ const content = await loadTemplate(`${options.lang}/src/index.ts.ejs`, {
115
+ name: options.projectName,
116
+ });
117
+ return { path: 'src/index.ts', content };
118
+ }
119
+
120
+ export async function generateTagprConfig(
121
+ options: InitOptions,
122
+ ): Promise<GeneratedFile> {
123
+ const content = await loadTemplate(`${options.lang}/.tagpr.ejs`, {});
124
+ return { path: '.tagpr', content };
125
+ }
126
+
127
+ export async function generateTagprWorkflow(
128
+ options: InitOptions,
129
+ actionVersions: Record<string, string>,
130
+ ): Promise<GeneratedFile> {
131
+ const content = await loadTemplate('common/workflows/tagpr.yml.ejs', {
132
+ isDevcode: options.isDevcode,
133
+ actionVersions,
134
+ });
135
+ return { path: '.github/workflows/tagpr.yml', content };
136
+ }
137
+
138
+ export async function generateCiWorkflow(
139
+ options: InitOptions,
140
+ actionVersions: Record<string, string>,
141
+ ): Promise<GeneratedFile> {
142
+ const content = await loadTemplate(`${options.lang}/workflows/ci.yml.ejs`, {
143
+ actionVersions,
144
+ });
145
+ return { path: '.github/workflows/ci.yml', content };
146
+ }
147
+
148
+ export async function generateCodeqlWorkflow(
149
+ options: InitOptions,
150
+ actionVersions: Record<string, string>,
151
+ ): Promise<GeneratedFile> {
152
+ const content = await loadTemplate(
153
+ `${options.lang}/workflows/codeql.yml.ejs`,
154
+ { actionVersions },
155
+ );
156
+ return { path: '.github/workflows/codeql.yml', content };
157
+ }
158
+
159
+ export async function generateCodeqlConfig(
160
+ options: InitOptions,
161
+ ): Promise<GeneratedFile> {
162
+ const content = await loadTemplate(
163
+ `${options.lang}/codeql/codeql-config.yml.ejs`,
164
+ { name: options.projectName },
165
+ );
166
+ return { path: '.github/codeql/codeql-config.yml', content };
167
+ }
168
+
169
+ export async function generateDependabot(
170
+ options: InitOptions,
171
+ ): Promise<GeneratedFile> {
172
+ const content = await loadTemplate('common/dependabot.yml.ejs', {
173
+ lang: options.lang,
174
+ });
175
+ return { path: '.github/dependabot.yml', content };
176
+ }
177
+
178
+ export async function generateReleaseConfig(): Promise<GeneratedFile> {
179
+ const content = await loadTemplate('common/release.yml.ejs', {});
180
+ return { path: '.github/release.yml', content };
181
+ }
182
+
183
+ export class FileWriteError extends Error {
184
+ constructor(
185
+ message: string,
186
+ public readonly targetPath: string,
187
+ public readonly cause?: Error,
188
+ ) {
189
+ super(message);
190
+ this.name = 'FileWriteError';
191
+ }
192
+ }
193
+
194
+ export async function writeGeneratedFiles(
195
+ targetDir: string,
196
+ files: GeneratedFile[],
197
+ ): Promise<void> {
198
+ try {
199
+ await fs.mkdir(targetDir, { recursive: true });
200
+ } catch (error) {
201
+ const fsError = error as NodeJS.ErrnoException;
202
+ throw new FileWriteError(
203
+ `Failed to create target directory "${targetDir}": ${fsError.message}`,
204
+ targetDir,
205
+ fsError,
206
+ );
207
+ }
208
+
209
+ for (const file of files) {
210
+ const filePath = path.join(targetDir, file.path);
211
+ const fileDir = path.dirname(filePath);
212
+
213
+ try {
214
+ if (fileDir !== targetDir) {
215
+ await fs.mkdir(fileDir, { recursive: true });
216
+ }
217
+ await fs.writeFile(filePath, file.content, 'utf-8');
218
+ } catch (error) {
219
+ const fsError = error as NodeJS.ErrnoException;
220
+ throw new FileWriteError(
221
+ `Failed to write file "${file.path}" in "${targetDir}": ${fsError.message}`,
222
+ filePath,
223
+ fsError,
224
+ );
225
+ }
226
+ }
227
+ }
228
+
229
+ export class ProjectNameError extends Error {
230
+ constructor(
231
+ message: string,
232
+ public readonly projectName: string,
233
+ ) {
234
+ super(message);
235
+ this.name = 'ProjectNameError';
236
+ }
237
+ }
238
+
239
+ const VALID_PROJECT_NAME_REGEX = /^(@[\w-]+\/)?[\w][\w.-]*[\w]$|^[\w]$/;
240
+
241
+ export function validateProjectName(projectName: string): void {
242
+ if (!projectName || projectName.trim() === '') {
243
+ throw new ProjectNameError('Project name cannot be empty', projectName);
244
+ }
245
+
246
+ const nameWithoutScope = projectName.replace(/^@[\w-]+\//, '');
247
+ if (nameWithoutScope.includes('/') || nameWithoutScope.includes('\\')) {
248
+ throw new ProjectNameError(
249
+ `Project name "${projectName}" contains invalid path separators`,
250
+ projectName,
251
+ );
252
+ }
253
+
254
+ if (nameWithoutScope.startsWith('.') || nameWithoutScope.endsWith('.')) {
255
+ throw new ProjectNameError(
256
+ `Project name "${projectName}" cannot start or end with a dot`,
257
+ projectName,
258
+ );
259
+ }
260
+
261
+ if (!VALID_PROJECT_NAME_REGEX.test(projectName)) {
262
+ throw new ProjectNameError(
263
+ `Project name "${projectName}" contains invalid characters. Use only letters, numbers, hyphens, underscores, and dots.`,
264
+ projectName,
265
+ );
266
+ }
267
+ }
268
+
269
+ export async function generateProject(options: InitOptions): Promise<void> {
270
+ validateProjectName(options.projectName);
271
+
272
+ const outputDir = options.targetDir ?? options.projectName;
273
+ const actionVersions = await getLatestActionVersions();
274
+
275
+ const files: GeneratedFile[] = await Promise.all([
276
+ generatePackageJson(options),
277
+ generateTsconfig(options),
278
+ generateEntryPoint(options),
279
+ generateTagprConfig(options),
280
+ generateTagprWorkflow(options, actionVersions),
281
+ generateCiWorkflow(options, actionVersions),
282
+ generateCodeqlWorkflow(options, actionVersions),
283
+ generateCodeqlConfig(options),
284
+ generateDependabot(options),
285
+ generateReleaseConfig(),
286
+ ]);
287
+
288
+ try {
289
+ await writeGeneratedFiles(outputDir, files);
290
+ } catch (error) {
291
+ if (error instanceof FileWriteError) {
292
+ throw error;
293
+ }
294
+ throw new FileWriteError(
295
+ `Failed to generate project "${options.projectName}": ${error instanceof Error ? error.message : String(error)}`,
296
+ outputDir,
297
+ error instanceof Error ? error : undefined,
298
+ );
299
+ }
300
+ }
@@ -0,0 +1,12 @@
1
+ version: 2
2
+ updates:
3
+ <% if (lang === 'typescript') { -%>
4
+ - package-ecosystem: "npm"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "weekly"
8
+ <% } -%>
9
+ - package-ecosystem: "github-actions"
10
+ directory: "/"
11
+ schedule:
12
+ interval: "weekly"
@@ -0,0 +1,4 @@
1
+ changelog:
2
+ exclude:
3
+ labels:
4
+ - tagpr