@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,31 @@
1
+ name: tagpr
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: write
10
+ pull-requests: write
11
+
12
+ jobs:
13
+ tagpr:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
17
+ <% if (isDevcode) { -%>
18
+ # TODO: After replace-devcode, add token: ${{ secrets.PAT_FOR_TAGPR }}
19
+ <% } else { -%>
20
+ with:
21
+ token: ${{ secrets.PAT_FOR_TAGPR }}
22
+ <% } -%>
23
+
24
+ - uses: Songmu/tagpr@<%= actionVersions['Songmu/tagpr'] %>
25
+ env:
26
+ <% if (isDevcode) { -%>
27
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
28
+ # TODO: After replace-devcode, use PAT_FOR_TAGPR instead
29
+ <% } else { -%>
30
+ GITHUB_TOKEN: ${{ secrets.PAT_FOR_TAGPR }}
31
+ <% } -%>
@@ -0,0 +1,5 @@
1
+ # tagpr configuration
2
+ # https://github.com/Songmu/tagpr
3
+
4
+ [tagpr]
5
+ versionFile = "package.json"
@@ -0,0 +1,7 @@
1
+ name: "CodeQL config for <%= name %>"
2
+
3
+ paths:
4
+ - src
5
+
6
+ paths-ignore:
7
+ - node_modules/
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "<%= name %>",
3
+ "version": "0.0.0",
4
+ <% if (isDevcode) { -%>
5
+ "private": true,
6
+ <% } -%>
7
+ "description": "",
8
+ "type": "module",
9
+ "scripts": {
10
+ "dev": "bun run src/index.ts",
11
+ "build": "bun build src/index.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
+ "prepare": "husky"
17
+ },
18
+ "keywords": [],
19
+ "author": "<%= author %>",
20
+ "license": "MIT",
21
+ "devDependencies": {
22
+ "@biomejs/biome": "^<%= versions['@biomejs/biome'] %>",
23
+ "@commitlint/cli": "^<%= versions['@commitlint/cli'] %>",
24
+ "@commitlint/config-conventional": "^<%= versions['@commitlint/config-conventional'] %>",
25
+ "bun-types": "^<%= versions['bun-types'] %>",
26
+ "husky": "^<%= versions['husky'] %>",
27
+ "typescript": "^<%= versions['typescript'] %>"
28
+ }
29
+ }
@@ -0,0 +1 @@
1
+ console.log('Hello from <%= name %>!');
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "declaration": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "types": ["bun-types"]
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }
17
+
@@ -0,0 +1,58 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - 'src/**'
9
+ - 'package.json'
10
+ - 'tsconfig.json'
11
+ - 'biome.json'
12
+ - 'bun.lock'
13
+ - '.github/workflows/ci.yml'
14
+ push:
15
+ branches:
16
+ - main
17
+ paths:
18
+ - 'src/**'
19
+ - 'package.json'
20
+ - 'tsconfig.json'
21
+ - 'biome.json'
22
+ - 'bun.lock'
23
+ - '.github/workflows/ci.yml'
24
+
25
+ permissions:
26
+ contents: read
27
+
28
+ jobs:
29
+ check:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
33
+
34
+ - uses: oven-sh/setup-bun@<%= actionVersions['oven-sh/setup-bun'] %>
35
+ with:
36
+ bun-version: latest
37
+
38
+ - run: bun install --frozen-lockfile
39
+
40
+ - name: Biome check
41
+ run: bun run check
42
+
43
+ - name: TypeScript check
44
+ run: bun run build
45
+
46
+ test:
47
+ runs-on: ubuntu-latest
48
+ steps:
49
+ - uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
50
+
51
+ - uses: oven-sh/setup-bun@<%= actionVersions['oven-sh/setup-bun'] %>
52
+ with:
53
+ bun-version: latest
54
+
55
+ - run: bun install --frozen-lockfile
56
+
57
+ - name: Test
58
+ run: bun test
@@ -0,0 +1,46 @@
1
+ name: CodeQL
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - 'src/**'
8
+ - 'package.json'
9
+ - 'tsconfig.json'
10
+ - '.github/workflows/codeql.yml'
11
+ pull_request:
12
+ branches: [main]
13
+ paths:
14
+ - 'src/**'
15
+ - 'package.json'
16
+ - 'tsconfig.json'
17
+ - '.github/workflows/codeql.yml'
18
+ schedule:
19
+ # Run weekly on Sunday at 00:00 UTC
20
+ - cron: '0 0 * * 0'
21
+ workflow_dispatch:
22
+
23
+ jobs:
24
+ analyze:
25
+ name: Analyze
26
+ runs-on: ubuntu-latest
27
+ timeout-minutes: 10
28
+ permissions:
29
+ actions: read
30
+ contents: read
31
+ security-events: write
32
+
33
+ steps:
34
+ - name: Checkout repository
35
+ uses: actions/checkout@<%= actionVersions['actions/checkout'] %>
36
+
37
+ - name: Initialize CodeQL
38
+ uses: github/codeql-action/init@<%= actionVersions['github/codeql-action'] %>
39
+ with:
40
+ languages: javascript-typescript
41
+ config-file: ./.github/codeql/codeql-config.yml
42
+
43
+ - name: Perform CodeQL Analysis
44
+ uses: github/codeql-action/analyze@<%= actionVersions['github/codeql-action'] %>
45
+ with:
46
+ category: '/language:javascript-typescript'
package/src/types.ts ADDED
@@ -0,0 +1,13 @@
1
+ export type Language = 'typescript';
2
+
3
+ export interface InitOptions {
4
+ /** The name used for npm publish (e.g., @scope/package-name) */
5
+ projectName: string;
6
+ lang: Language;
7
+ /** If true, projectName is a development code that will be replaced later */
8
+ isDevcode: boolean;
9
+ /** Optional output directory path. Defaults to projectName if not specified. */
10
+ targetDir?: string;
11
+ /** Package author. If not specified, detected from language-specific tools (e.g., npm whoami for TypeScript). */
12
+ author?: string;
13
+ }
@@ -0,0 +1,34 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { hasGitHubToken } from './github-repo';
3
+
4
+ describe('github-repo utils', () => {
5
+ describe('hasGitHubToken', () => {
6
+ const originalToken = process.env.GITHUB_TOKEN;
7
+
8
+ afterEach(() => {
9
+ if (originalToken !== undefined) {
10
+ process.env.GITHUB_TOKEN = originalToken;
11
+ } else {
12
+ delete process.env.GITHUB_TOKEN;
13
+ }
14
+ });
15
+
16
+ test('should return true when GITHUB_TOKEN is set', () => {
17
+ process.env.GITHUB_TOKEN = 'test-token';
18
+ expect(hasGitHubToken()).toBe(true);
19
+ });
20
+
21
+ test('should return false when GITHUB_TOKEN is not set', () => {
22
+ delete process.env.GITHUB_TOKEN;
23
+ expect(hasGitHubToken()).toBe(false);
24
+ });
25
+
26
+ test('should return false when GITHUB_TOKEN is empty', () => {
27
+ process.env.GITHUB_TOKEN = '';
28
+ expect(hasGitHubToken()).toBe(false);
29
+ });
30
+ });
31
+
32
+ // Note: createGitHubRepo is not tested here because it requires
33
+ // actual GitHub API calls. Integration tests should be done separately.
34
+ });
@@ -0,0 +1,141 @@
1
+ import { Octokit } from 'octokit';
2
+
3
+ export interface CreateRepoOptions {
4
+ name: string;
5
+ description?: string;
6
+ isPrivate: boolean;
7
+ }
8
+
9
+ export interface GitHubRepoResult {
10
+ url: string;
11
+ cloneUrl: string;
12
+ alreadyExisted: boolean;
13
+ }
14
+
15
+ /**
16
+ * Labels required for tagpr workflow
17
+ */
18
+ const TAGPR_LABELS = [
19
+ {
20
+ name: 'tagpr:minor',
21
+ color: '0e8a16',
22
+ description: 'have tagpr bump minor version',
23
+ },
24
+ {
25
+ name: 'tagpr:major',
26
+ color: 'd93f0b',
27
+ description: 'have tagpr bump major version',
28
+ },
29
+ ] as const;
30
+
31
+ export class RepoAlreadyExistsError extends Error {
32
+ constructor(
33
+ public readonly repoName: string,
34
+ public readonly repoUrl: string,
35
+ public readonly cloneUrl: string,
36
+ ) {
37
+ super(`Repository "${repoName}" already exists`);
38
+ this.name = 'RepoAlreadyExistsError';
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Creates a GitHub repository and sets up tagpr labels
44
+ */
45
+ export async function createGitHubRepo(
46
+ options: CreateRepoOptions,
47
+ ): Promise<GitHubRepoResult> {
48
+ const token = process.env.GITHUB_TOKEN;
49
+
50
+ if (!token) {
51
+ throw new Error(
52
+ 'GITHUB_TOKEN environment variable is required for repository creation',
53
+ );
54
+ }
55
+
56
+ const octokit = new Octokit({ auth: token });
57
+
58
+ // Get authenticated user
59
+ const { data: user } = await octokit.rest.users.getAuthenticated();
60
+
61
+ let repoUrl: string;
62
+ let cloneUrl: string;
63
+ let alreadyExisted = false;
64
+
65
+ // Create repository
66
+ try {
67
+ const { data: repo } = await octokit.rest.repos.createForAuthenticatedUser({
68
+ name: options.name,
69
+ description: options.description ?? '',
70
+ private: options.isPrivate,
71
+ auto_init: false,
72
+ });
73
+ repoUrl = repo.html_url;
74
+ cloneUrl = repo.clone_url;
75
+ } catch (error) {
76
+ const apiError = error as { status?: number; message?: string };
77
+
78
+ // Repository already exists (422 Unprocessable Entity)
79
+ if (apiError.status === 422) {
80
+ // Fetch existing repo info
81
+ try {
82
+ const { data: existingRepo } = await octokit.rest.repos.get({
83
+ owner: user.login,
84
+ repo: options.name,
85
+ });
86
+ repoUrl = existingRepo.html_url;
87
+ cloneUrl = existingRepo.clone_url;
88
+ alreadyExisted = true;
89
+ } catch {
90
+ throw new Error(
91
+ `Repository "${options.name}" already exists but could not fetch its details`,
92
+ );
93
+ }
94
+ } else {
95
+ throw error;
96
+ }
97
+ }
98
+
99
+ // Create tagpr labels (skip silently if already exist)
100
+ await createTagprLabels(octokit, user.login, options.name);
101
+
102
+ return {
103
+ url: repoUrl,
104
+ cloneUrl,
105
+ alreadyExisted,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Creates tagpr labels for a repository
111
+ */
112
+ async function createTagprLabels(
113
+ octokit: Octokit,
114
+ owner: string,
115
+ repo: string,
116
+ ): Promise<void> {
117
+ for (const label of TAGPR_LABELS) {
118
+ try {
119
+ await octokit.rest.issues.createLabel({
120
+ owner,
121
+ repo,
122
+ name: label.name,
123
+ color: label.color,
124
+ description: label.description,
125
+ });
126
+ } catch (error) {
127
+ // Label already exists (422), ignore
128
+ const apiError = error as { status?: number };
129
+ if (apiError.status !== 422) {
130
+ throw error;
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Checks if GITHUB_TOKEN is available
138
+ */
139
+ export function hasGitHubToken(): boolean {
140
+ return !!process.env.GITHUB_TOKEN;
141
+ }
@@ -0,0 +1,47 @@
1
+ import { DEFAULT_ACTION_VERSION, GITHUB_ACTIONS } from '../config';
2
+
3
+ /**
4
+ * Fetches the latest major version tag for a GitHub Action
5
+ * @param repo - Repository in format "owner/repo" (e.g., "actions/checkout")
6
+ * @returns Latest major version tag (e.g., "v4")
7
+ */
8
+ export async function getLatestActionVersion(repo: string): Promise<string> {
9
+ const response = await fetch(
10
+ `https://api.github.com/repos/${repo}/releases/latest`,
11
+ );
12
+
13
+ if (!response.ok) {
14
+ throw new Error(`Failed to fetch latest release for ${repo}`);
15
+ }
16
+
17
+ const data = (await response.json()) as { tag_name: string };
18
+ // Extract major version (e.g., "v4.2.0" -> "v4")
19
+ const match = data.tag_name.match(/^v?(\d+)/);
20
+ return match ? `v${match[1]}` : data.tag_name;
21
+ }
22
+
23
+ /**
24
+ * Fetches latest major versions for all configured GitHub Actions
25
+ */
26
+ export async function getLatestActionVersions(
27
+ actions: readonly string[] = Object.keys(GITHUB_ACTIONS),
28
+ ): Promise<Record<string, string>> {
29
+ const versions: Record<string, string> = {};
30
+
31
+ await Promise.all(
32
+ actions.map(async (action) => {
33
+ try {
34
+ versions[action] = await getLatestActionVersion(action);
35
+ } catch {
36
+ versions[action] =
37
+ GITHUB_ACTIONS[action as keyof typeof GITHUB_ACTIONS] ??
38
+ DEFAULT_ACTION_VERSION;
39
+ }
40
+ }),
41
+ );
42
+
43
+ return versions;
44
+ }
45
+
46
+ // Re-export for convenience
47
+ export { GITHUB_ACTIONS };
@@ -0,0 +1,99 @@
1
+ import { afterEach, describe, expect, mock, test } from 'bun:test';
2
+ import { getLatestVersion, getLatestVersions, getNpmUsername } from './npm';
3
+
4
+ describe('npm utils', () => {
5
+ describe('getLatestVersion', () => {
6
+ const originalFetch = globalThis.fetch;
7
+
8
+ afterEach(() => {
9
+ globalThis.fetch = originalFetch;
10
+ });
11
+
12
+ test('should return version from npm registry', async () => {
13
+ globalThis.fetch = mock(() =>
14
+ Promise.resolve({
15
+ ok: true,
16
+ json: () => Promise.resolve({ version: '1.2.3' }),
17
+ } as Response),
18
+ ) as unknown as typeof fetch;
19
+
20
+ const version = await getLatestVersion('some-package');
21
+ expect(version).toBe('1.2.3');
22
+ });
23
+
24
+ test('should throw error when fetch fails', async () => {
25
+ globalThis.fetch = mock(() =>
26
+ Promise.resolve({
27
+ ok: false,
28
+ } as Response),
29
+ ) as unknown as typeof fetch;
30
+
31
+ expect(getLatestVersion('some-package')).rejects.toThrow(
32
+ 'Failed to fetch version',
33
+ );
34
+ });
35
+ });
36
+
37
+ describe('getLatestVersions', () => {
38
+ const originalFetch = globalThis.fetch;
39
+
40
+ afterEach(() => {
41
+ globalThis.fetch = originalFetch;
42
+ });
43
+
44
+ test('should return versions for multiple packages', async () => {
45
+ globalThis.fetch = mock((url: string) => {
46
+ const packageName = url.toString().split('/').slice(-2, -1)[0];
47
+ const versions: Record<string, string> = {
48
+ 'package-a': '1.0.0',
49
+ 'package-b': '2.0.0',
50
+ };
51
+ return Promise.resolve({
52
+ ok: true,
53
+ json: () => Promise.resolve({ version: versions[packageName] }),
54
+ } as Response);
55
+ }) as unknown as typeof fetch;
56
+
57
+ const versions = await getLatestVersions(['package-a', 'package-b']);
58
+ expect(versions['package-a']).toBe('1.0.0');
59
+ expect(versions['package-b']).toBe('2.0.0');
60
+ });
61
+
62
+ test('should fallback to "latest" when fetch fails', async () => {
63
+ globalThis.fetch = mock(() =>
64
+ Promise.resolve({
65
+ ok: false,
66
+ } as Response),
67
+ ) as unknown as typeof fetch;
68
+
69
+ const versions = await getLatestVersions(['failing-package']);
70
+ expect(versions['failing-package']).toBe('latest');
71
+ });
72
+ });
73
+
74
+ describe('getNpmUsername', () => {
75
+ test('should return username when logged in', async () => {
76
+ const mockExec = mock(() =>
77
+ Promise.resolve({ stdout: 'mocked-user\n', stderr: '' }),
78
+ );
79
+
80
+ const username = await getNpmUsername(mockExec);
81
+ expect(username).toBe('mocked-user');
82
+ expect(mockExec).toHaveBeenCalledWith('npm whoami');
83
+ });
84
+
85
+ test('should return null when stdout is empty', async () => {
86
+ const mockExec = mock(() => Promise.resolve({ stdout: '', stderr: '' }));
87
+
88
+ const username = await getNpmUsername(mockExec);
89
+ expect(username).toBeNull();
90
+ });
91
+
92
+ test('should return null when npm whoami fails', async () => {
93
+ const mockExec = mock(() => Promise.reject(new Error('Not logged in')));
94
+
95
+ const username = await getNpmUsername(mockExec);
96
+ expect(username).toBeNull();
97
+ });
98
+ });
99
+ });
@@ -0,0 +1,59 @@
1
+ import { exec } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ const execAsync = promisify(exec);
5
+
6
+ type ExecFn = (command: string) => Promise<{ stdout: string; stderr: string }>;
7
+
8
+ /**
9
+ * Fetches the latest version of a package from npm registry
10
+ */
11
+ export async function getLatestVersion(packageName: string): Promise<string> {
12
+ const response = await fetch(
13
+ `https://registry.npmjs.org/${packageName}/latest`,
14
+ );
15
+
16
+ if (!response.ok) {
17
+ throw new Error(`Failed to fetch version for ${packageName}`);
18
+ }
19
+
20
+ const data = (await response.json()) as { version: string };
21
+ return data.version;
22
+ }
23
+
24
+ /**
25
+ * Gets the current npm username if logged in
26
+ * @param execFn - Optional exec function for testing (defaults to execAsync)
27
+ */
28
+ export async function getNpmUsername(
29
+ execFn: ExecFn = execAsync,
30
+ ): Promise<string | null> {
31
+ try {
32
+ const { stdout } = await execFn('npm whoami');
33
+ return stdout.trim() || null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Fetches latest versions for multiple packages
41
+ */
42
+ export async function getLatestVersions(
43
+ packageNames: string[],
44
+ ): Promise<Record<string, string>> {
45
+ const versions: Record<string, string> = {};
46
+
47
+ await Promise.all(
48
+ packageNames.map(async (name) => {
49
+ try {
50
+ versions[name] = await getLatestVersion(name);
51
+ } catch {
52
+ // Fallback to a default version if fetch fails
53
+ versions[name] = 'latest';
54
+ }
55
+ }),
56
+ );
57
+
58
+ return versions;
59
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "declaration": true,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "types": ["bun-types"]
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }