@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,183 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import {
|
|
6
|
+
detectDevcode,
|
|
7
|
+
prepareRelease,
|
|
8
|
+
registerPrepareReleaseCommand,
|
|
9
|
+
} from './prepare-release';
|
|
10
|
+
|
|
11
|
+
describe('prepare-release command', () => {
|
|
12
|
+
const testDir = path.join(import.meta.dir, '../../.test-prepare-release');
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
16
|
+
await fs.mkdir(testDir, { recursive: true });
|
|
17
|
+
await fs.mkdir(path.join(testDir, '.github/workflows'), {
|
|
18
|
+
recursive: true,
|
|
19
|
+
});
|
|
20
|
+
await fs.mkdir(path.join(testDir, '.github/codeql'), { recursive: true });
|
|
21
|
+
await fs.mkdir(path.join(testDir, 'src'), { recursive: true });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await fs.rm(testDir, { recursive: true, force: true });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('detectDevcode', () => {
|
|
29
|
+
test('should return devcode name when private: true', async () => {
|
|
30
|
+
const packageJson = {
|
|
31
|
+
name: 'my-devcode',
|
|
32
|
+
version: '0.0.0',
|
|
33
|
+
private: true,
|
|
34
|
+
};
|
|
35
|
+
await fs.writeFile(
|
|
36
|
+
path.join(testDir, 'package.json'),
|
|
37
|
+
JSON.stringify(packageJson),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const devcode = await detectDevcode(testDir);
|
|
41
|
+
expect(devcode).toBe('my-devcode');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should throw when private flag is missing', async () => {
|
|
45
|
+
const packageJson = { name: 'my-project', version: '0.0.0' };
|
|
46
|
+
await fs.writeFile(
|
|
47
|
+
path.join(testDir, 'package.json'),
|
|
48
|
+
JSON.stringify(packageJson),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(detectDevcode(testDir)).rejects.toThrow('not a devcode project');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('should throw when package.json not found', async () => {
|
|
55
|
+
expect(detectDevcode(testDir)).rejects.toThrow('package.json not found');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('prepareRelease', () => {
|
|
60
|
+
test('should replace only in managed locations', async () => {
|
|
61
|
+
// Setup test files with private: true
|
|
62
|
+
await fs.writeFile(
|
|
63
|
+
path.join(testDir, 'package.json'),
|
|
64
|
+
JSON.stringify({ name: 'devcode', version: '0.0.0', private: true }),
|
|
65
|
+
);
|
|
66
|
+
await fs.writeFile(
|
|
67
|
+
path.join(testDir, '.github/workflows/tagpr.yml'),
|
|
68
|
+
`name: tagpr
|
|
69
|
+
jobs:
|
|
70
|
+
tagpr:
|
|
71
|
+
steps:
|
|
72
|
+
- uses: actions/checkout@v6
|
|
73
|
+
# TODO: After replace-devcode, add token: \${{ secrets.PAT_FOR_TAGPR }}
|
|
74
|
+
- uses: Songmu/tagpr@v1
|
|
75
|
+
env:
|
|
76
|
+
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
77
|
+
`,
|
|
78
|
+
);
|
|
79
|
+
await fs.writeFile(
|
|
80
|
+
path.join(testDir, '.github/codeql/codeql-config.yml'),
|
|
81
|
+
'name: "CodeQL config for devcode"\n\npaths:\n - src',
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
await prepareRelease({
|
|
85
|
+
publishName: '@scope/package',
|
|
86
|
+
targetDir: testDir,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Verify package.json - name should be replaced
|
|
90
|
+
const pkg = JSON.parse(
|
|
91
|
+
await fs.readFile(path.join(testDir, 'package.json'), 'utf-8'),
|
|
92
|
+
);
|
|
93
|
+
expect(pkg.name).toBe('@scope/package');
|
|
94
|
+
expect(pkg.private).toBeUndefined();
|
|
95
|
+
|
|
96
|
+
// Verify tagpr.yml - tokens should be replaced
|
|
97
|
+
const tagpr = await fs.readFile(
|
|
98
|
+
path.join(testDir, '.github/workflows/tagpr.yml'),
|
|
99
|
+
'utf-8',
|
|
100
|
+
);
|
|
101
|
+
expect(tagpr).toContain('secrets.PAT_FOR_TAGPR');
|
|
102
|
+
expect(tagpr).not.toContain('secrets.GITHUB_TOKEN');
|
|
103
|
+
|
|
104
|
+
// Verify codeql-config.yml - only name field should be replaced
|
|
105
|
+
const codeql = await fs.readFile(
|
|
106
|
+
path.join(testDir, '.github/codeql/codeql-config.yml'),
|
|
107
|
+
'utf-8',
|
|
108
|
+
);
|
|
109
|
+
expect(codeql).toContain('@scope/package');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('should detect unmanaged occurrences', async () => {
|
|
113
|
+
// Setup with devcode appearing in an unmanaged file
|
|
114
|
+
await fs.writeFile(
|
|
115
|
+
path.join(testDir, 'package.json'),
|
|
116
|
+
JSON.stringify({ name: 'my-devcode', version: '0.0.0', private: true }),
|
|
117
|
+
);
|
|
118
|
+
await fs.writeFile(
|
|
119
|
+
path.join(testDir, 'src/index.ts'),
|
|
120
|
+
'console.log("Hello from my-devcode!");',
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Capture console output
|
|
124
|
+
const logs: string[] = [];
|
|
125
|
+
const originalLog = console.log;
|
|
126
|
+
console.log = (...args) => logs.push(args.join(' '));
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await prepareRelease({
|
|
130
|
+
publishName: '@scope/package',
|
|
131
|
+
targetDir: testDir,
|
|
132
|
+
});
|
|
133
|
+
} finally {
|
|
134
|
+
console.log = originalLog;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Verify unmanaged occurrence was reported
|
|
138
|
+
const output = logs.join('\n');
|
|
139
|
+
expect(output).toContain('unmanaged occurrence');
|
|
140
|
+
expect(output).toContain('src/index.ts');
|
|
141
|
+
|
|
142
|
+
// Verify unmanaged file was NOT modified
|
|
143
|
+
const srcContent = await fs.readFile(
|
|
144
|
+
path.join(testDir, 'src/index.ts'),
|
|
145
|
+
'utf-8',
|
|
146
|
+
);
|
|
147
|
+
expect(srcContent).toContain('my-devcode');
|
|
148
|
+
expect(srcContent).not.toContain('@scope/package');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('should fail if not a devcode project', async () => {
|
|
152
|
+
await fs.writeFile(
|
|
153
|
+
path.join(testDir, 'package.json'),
|
|
154
|
+
JSON.stringify({ name: 'not-devcode', version: '1.0.0' }),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(
|
|
158
|
+
prepareRelease({ publishName: '@scope/package', targetDir: testDir }),
|
|
159
|
+
).rejects.toThrow('not a devcode project');
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('registerPrepareReleaseCommand', () => {
|
|
164
|
+
test('should register prepare-release command with publish-name argument', () => {
|
|
165
|
+
const program = new Command();
|
|
166
|
+
registerPrepareReleaseCommand(program);
|
|
167
|
+
|
|
168
|
+
const cmd = program.commands.find((c) => c.name() === 'prepare-release');
|
|
169
|
+
expect(cmd).toBeDefined();
|
|
170
|
+
expect(cmd?.description()).toContain('auto-detects');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('should have target-dir option', () => {
|
|
174
|
+
const program = new Command();
|
|
175
|
+
registerPrepareReleaseCommand(program);
|
|
176
|
+
|
|
177
|
+
const cmd = program.commands.find((c) => c.name() === 'prepare-release');
|
|
178
|
+
const options = cmd?.options.map((o) => o.long);
|
|
179
|
+
|
|
180
|
+
expect(options).toContain('--target-dir');
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import type { Command } from 'commander';
|
|
4
|
+
|
|
5
|
+
export interface PrepareReleaseOptions {
|
|
6
|
+
/** New package name for release */
|
|
7
|
+
publishName: string;
|
|
8
|
+
/** Target directory (defaults to current directory) */
|
|
9
|
+
targetDir?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PackageJson {
|
|
13
|
+
name: string;
|
|
14
|
+
private?: boolean;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Managed locations where devcode replacement is performed automatically.
|
|
20
|
+
* Each location specifies the file and how to replace.
|
|
21
|
+
*/
|
|
22
|
+
interface ManagedLocation {
|
|
23
|
+
file: string;
|
|
24
|
+
description: string;
|
|
25
|
+
replace: (
|
|
26
|
+
targetDir: string,
|
|
27
|
+
devcode: string,
|
|
28
|
+
publishName: string,
|
|
29
|
+
) => Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reads package.json and detects if this is a devcode project
|
|
34
|
+
* Returns the devcode name if private: true, otherwise throws
|
|
35
|
+
*/
|
|
36
|
+
export async function detectDevcode(targetDir: string): Promise<string> {
|
|
37
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
38
|
+
|
|
39
|
+
let content: string;
|
|
40
|
+
try {
|
|
41
|
+
content = await fs.readFile(packageJsonPath, 'utf-8');
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const fsError = error as NodeJS.ErrnoException;
|
|
44
|
+
if (fsError.code === 'ENOENT') {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'package.json not found. Are you in a project directory?',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pkg = JSON.parse(content) as PackageJson;
|
|
53
|
+
|
|
54
|
+
if (!pkg.private) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
'This project is not a devcode project (missing "private": true in package.json)',
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return pkg.name;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Updates package.json: replaces name and removes "private": true
|
|
65
|
+
* This is a MANAGED replacement - only touches the "name" field
|
|
66
|
+
*/
|
|
67
|
+
async function replaceInPackageJson(
|
|
68
|
+
targetDir: string,
|
|
69
|
+
_devcode: string,
|
|
70
|
+
publishName: string,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
73
|
+
const content = await fs.readFile(packageJsonPath, 'utf-8');
|
|
74
|
+
const pkg = JSON.parse(content) as PackageJson;
|
|
75
|
+
|
|
76
|
+
// Only replace the managed "name" field
|
|
77
|
+
pkg.name = publishName;
|
|
78
|
+
|
|
79
|
+
// Remove private flag
|
|
80
|
+
delete pkg.private;
|
|
81
|
+
|
|
82
|
+
await fs.writeFile(
|
|
83
|
+
packageJsonPath,
|
|
84
|
+
`${JSON.stringify(pkg, null, 2)}\n`,
|
|
85
|
+
'utf-8',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Updates codeql-config.yml: replaces only the "name" field
|
|
91
|
+
* This is a MANAGED replacement - only touches the YAML name field
|
|
92
|
+
*/
|
|
93
|
+
async function replaceInCodeqlConfig(
|
|
94
|
+
targetDir: string,
|
|
95
|
+
devcode: string,
|
|
96
|
+
publishName: string,
|
|
97
|
+
): Promise<void> {
|
|
98
|
+
const configPath = path.join(targetDir, '.github/codeql/codeql-config.yml');
|
|
99
|
+
|
|
100
|
+
let content: string;
|
|
101
|
+
try {
|
|
102
|
+
content = await fs.readFile(configPath, 'utf-8');
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const fsError = error as NodeJS.ErrnoException;
|
|
105
|
+
if (fsError.code === 'ENOENT') {
|
|
106
|
+
return; // File doesn't exist, nothing to do
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Line-based parsing to avoid ReDoS vulnerability
|
|
112
|
+
// Only replace devcode in the first line that starts with "name:"
|
|
113
|
+
const lines = content.split('\n');
|
|
114
|
+
for (let i = 0; i < lines.length; i++) {
|
|
115
|
+
const line = lines[i];
|
|
116
|
+
if (line.trimStart().startsWith('name:')) {
|
|
117
|
+
// Replace devcode only in this specific line
|
|
118
|
+
lines[i] = line.replace(devcode, publishName);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
content = lines.join('\n');
|
|
123
|
+
|
|
124
|
+
await fs.writeFile(configPath, content, 'utf-8');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Updates tagpr.yml: switches from GITHUB_TOKEN to PAT_FOR_TAGPR
|
|
129
|
+
* This is a MANAGED replacement - only touches specific workflow patterns
|
|
130
|
+
*/
|
|
131
|
+
async function replaceInTagprWorkflow(
|
|
132
|
+
targetDir: string,
|
|
133
|
+
_devcode: string,
|
|
134
|
+
_publishName: string,
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
const workflowPath = path.join(targetDir, '.github/workflows/tagpr.yml');
|
|
137
|
+
|
|
138
|
+
let content: string;
|
|
139
|
+
try {
|
|
140
|
+
content = await fs.readFile(workflowPath, 'utf-8');
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const fsError = error as NodeJS.ErrnoException;
|
|
143
|
+
if (fsError.code === 'ENOENT') {
|
|
144
|
+
return; // File doesn't exist, nothing to do
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Replace GITHUB_TOKEN with PAT_FOR_TAGPR in env
|
|
150
|
+
content = content.replace(
|
|
151
|
+
/GITHUB_TOKEN: \$\{\{ secrets\.GITHUB_TOKEN \}\}/g,
|
|
152
|
+
'GITHUB_TOKEN: ${{ secrets.PAT_FOR_TAGPR }}',
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
// Add token to checkout if not present
|
|
156
|
+
content = content.replace(
|
|
157
|
+
/(uses: actions\/checkout@v\d+)\n(\s*)# TODO: After replace-devcode, add token: \$\{\{ secrets\.PAT_FOR_TAGPR \}\}/g,
|
|
158
|
+
'$1\n$2with:\n$2 token: ${{ secrets.PAT_FOR_TAGPR }}',
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Remove remaining TODO comments
|
|
162
|
+
content = content.replace(/\s*# TODO: After replace-devcode.*\n/g, '\n');
|
|
163
|
+
|
|
164
|
+
// Clean up extra blank lines
|
|
165
|
+
content = content.replace(/\n{3,}/g, '\n\n');
|
|
166
|
+
|
|
167
|
+
await fs.writeFile(workflowPath, content, 'utf-8');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Managed locations where devcode is automatically replaced
|
|
172
|
+
*/
|
|
173
|
+
const MANAGED_LOCATIONS: ManagedLocation[] = [
|
|
174
|
+
{
|
|
175
|
+
file: 'package.json',
|
|
176
|
+
description: 'name field',
|
|
177
|
+
replace: replaceInPackageJson,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
file: '.github/codeql/codeql-config.yml',
|
|
181
|
+
description: 'name field',
|
|
182
|
+
replace: replaceInCodeqlConfig,
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
file: '.github/workflows/tagpr.yml',
|
|
186
|
+
description: 'GITHUB_TOKEN ā PAT_FOR_TAGPR',
|
|
187
|
+
replace: replaceInTagprWorkflow,
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Scans project for unmanaged occurrences of devcode
|
|
193
|
+
*/
|
|
194
|
+
async function findUnmanagedOccurrences(
|
|
195
|
+
targetDir: string,
|
|
196
|
+
devcode: string,
|
|
197
|
+
): Promise<{ file: string; line: number; content: string }[]> {
|
|
198
|
+
const occurrences: { file: string; line: number; content: string }[] = [];
|
|
199
|
+
const managedFiles = new Set(MANAGED_LOCATIONS.map((l) => l.file));
|
|
200
|
+
|
|
201
|
+
// Files to scan (excluding node_modules, .git, etc.)
|
|
202
|
+
const filesToScan = await findFilesRecursive(targetDir, [
|
|
203
|
+
'node_modules',
|
|
204
|
+
'.git',
|
|
205
|
+
'dist',
|
|
206
|
+
'bun.lockb',
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
for (const file of filesToScan) {
|
|
210
|
+
const relativePath = path.relative(targetDir, file);
|
|
211
|
+
|
|
212
|
+
// Skip managed files
|
|
213
|
+
if (managedFiles.has(relativePath)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
219
|
+
const lines = content.split('\n');
|
|
220
|
+
|
|
221
|
+
for (let i = 0; i < lines.length; i++) {
|
|
222
|
+
if (lines[i].includes(devcode)) {
|
|
223
|
+
occurrences.push({
|
|
224
|
+
file: relativePath,
|
|
225
|
+
line: i + 1,
|
|
226
|
+
content: lines[i].trim().substring(0, 80),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
// Skip files that can't be read (binary, etc.)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return occurrences;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Recursively find all files in a directory
|
|
240
|
+
*/
|
|
241
|
+
async function findFilesRecursive(
|
|
242
|
+
dir: string,
|
|
243
|
+
excludeDirs: string[],
|
|
244
|
+
): Promise<string[]> {
|
|
245
|
+
const files: string[] = [];
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
249
|
+
|
|
250
|
+
for (const entry of entries) {
|
|
251
|
+
const fullPath = path.join(dir, entry.name);
|
|
252
|
+
|
|
253
|
+
if (entry.isDirectory()) {
|
|
254
|
+
if (!excludeDirs.includes(entry.name)) {
|
|
255
|
+
files.push(...(await findFilesRecursive(fullPath, excludeDirs)));
|
|
256
|
+
}
|
|
257
|
+
} else if (entry.isFile()) {
|
|
258
|
+
files.push(fullPath);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
// Skip directories that can't be read
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return files;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Main prepare-release logic
|
|
270
|
+
*/
|
|
271
|
+
export async function prepareRelease(
|
|
272
|
+
options: PrepareReleaseOptions,
|
|
273
|
+
): Promise<void> {
|
|
274
|
+
const targetDir = options.targetDir ?? process.cwd();
|
|
275
|
+
|
|
276
|
+
// Detect devcode from package.json (private: true)
|
|
277
|
+
const devcode = await detectDevcode(targetDir);
|
|
278
|
+
|
|
279
|
+
console.log(`Detected devcode project: ${devcode}`);
|
|
280
|
+
console.log(`Preparing release: ${devcode} ā ${options.publishName}\n`);
|
|
281
|
+
|
|
282
|
+
// Process managed locations
|
|
283
|
+
console.log('š Managed replacements:');
|
|
284
|
+
for (const location of MANAGED_LOCATIONS) {
|
|
285
|
+
try {
|
|
286
|
+
await location.replace(targetDir, devcode, options.publishName);
|
|
287
|
+
console.log(` ā
${location.file} (${location.description})`);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.log(
|
|
290
|
+
` ā ļø ${location.file}: ${error instanceof Error ? error.message : String(error)}`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Scan for unmanaged occurrences
|
|
296
|
+
const unmanaged = await findUnmanagedOccurrences(targetDir, devcode);
|
|
297
|
+
|
|
298
|
+
if (unmanaged.length > 0) {
|
|
299
|
+
console.log(
|
|
300
|
+
`\nā ļø Found ${unmanaged.length} unmanaged occurrence(s) of "${devcode}":`,
|
|
301
|
+
);
|
|
302
|
+
console.log(
|
|
303
|
+
' These were NOT automatically replaced. Please review manually:',
|
|
304
|
+
);
|
|
305
|
+
for (const occurrence of unmanaged) {
|
|
306
|
+
console.log(` - ${occurrence.file}:${occurrence.line}`);
|
|
307
|
+
console.log(` ${occurrence.content}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log(`\nš Release preparation complete!`);
|
|
312
|
+
console.log(` Package renamed: ${devcode} ā ${options.publishName}`);
|
|
313
|
+
console.log(` Private flag removed`);
|
|
314
|
+
console.log(` Workflows updated to use PAT_FOR_TAGPR`);
|
|
315
|
+
|
|
316
|
+
console.log(`\nā ļø Action required: Set up PAT_FOR_TAGPR secret`);
|
|
317
|
+
console.log(` 1. Create a Personal Access Token (classic) at:`);
|
|
318
|
+
console.log(` https://github.com/settings/tokens/new`);
|
|
319
|
+
console.log(` 2. Required permissions:`);
|
|
320
|
+
console.log(` ⢠repo (Full control of private repositories)`);
|
|
321
|
+
console.log(` - or for public repos: public_repo`);
|
|
322
|
+
console.log(` ⢠workflow (Update GitHub Action workflows)`);
|
|
323
|
+
console.log(
|
|
324
|
+
` 3. Add the token as a repository secret named PAT_FOR_TAGPR:`,
|
|
325
|
+
);
|
|
326
|
+
console.log(
|
|
327
|
+
` Settings ā Secrets and variables ā Actions ā New repository secret`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function registerPrepareReleaseCommand(program: Command): void {
|
|
332
|
+
program
|
|
333
|
+
.command('prepare-release <publish-name>')
|
|
334
|
+
.description(
|
|
335
|
+
'Prepare a devcode project for release (auto-detects devcode from package.json)',
|
|
336
|
+
)
|
|
337
|
+
.option(
|
|
338
|
+
'-t, --target-dir <path>',
|
|
339
|
+
'Target directory (defaults to current directory)',
|
|
340
|
+
)
|
|
341
|
+
.action(async (publishName: string, opts: { targetDir?: string }) => {
|
|
342
|
+
try {
|
|
343
|
+
await prepareRelease({
|
|
344
|
+
publishName,
|
|
345
|
+
targetDir: opts.targetDir,
|
|
346
|
+
});
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error(
|
|
349
|
+
`ā Failed to prepare release: ${error instanceof Error ? error.message : String(error)}`,
|
|
350
|
+
);
|
|
351
|
+
process.exit(1);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Actions configuration for generated workflows
|
|
3
|
+
* Key: action name, Value: fallback version when API fetch fails
|
|
4
|
+
*/
|
|
5
|
+
export const GITHUB_ACTIONS = {
|
|
6
|
+
'actions/checkout': 'v6',
|
|
7
|
+
'Songmu/tagpr': 'v1',
|
|
8
|
+
'oven-sh/setup-bun': 'v2',
|
|
9
|
+
'github/codeql-action': 'v3',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
/** Default fallback version for unknown actions */
|
|
13
|
+
export const DEFAULT_ACTION_VERSION = 'v1';
|