@raftlabs/raftstack 1.0.0
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/.claude/skills/backend/SKILL.md +802 -0
- package/.claude/skills/code-quality/SKILL.md +318 -0
- package/.claude/skills/database/SKILL.md +465 -0
- package/.claude/skills/react/SKILL.md +418 -0
- package/.claude/skills/seo/SKILL.md +446 -0
- package/LICENSE +21 -0
- package/README.md +291 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2009 -0
- package/dist/cli.js.map +1 -0
- package/package.json +69 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2009 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import * as p2 from "@clack/prompts";
|
|
8
|
+
import pc2 from "picocolors";
|
|
9
|
+
|
|
10
|
+
// src/prompts/index.ts
|
|
11
|
+
import * as p from "@clack/prompts";
|
|
12
|
+
import pc from "picocolors";
|
|
13
|
+
|
|
14
|
+
// src/utils/detect-project.ts
|
|
15
|
+
import { existsSync } from "fs";
|
|
16
|
+
import { readFile } from "fs/promises";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
var INDICATORS = [
|
|
19
|
+
{ file: "nx.json", type: "nx", confidence: "high" },
|
|
20
|
+
{ file: "turbo.json", type: "turbo", confidence: "high" },
|
|
21
|
+
{ file: "pnpm-workspace.yaml", type: "pnpm-workspace", confidence: "high" },
|
|
22
|
+
{ file: "lerna.json", type: "pnpm-workspace", confidence: "medium" }
|
|
23
|
+
];
|
|
24
|
+
async function detectProjectType(targetDir = process.cwd()) {
|
|
25
|
+
const foundIndicators = [];
|
|
26
|
+
let detectedType = "single";
|
|
27
|
+
let confidence = "low";
|
|
28
|
+
for (const indicator of INDICATORS) {
|
|
29
|
+
const filePath = join(targetDir, indicator.file);
|
|
30
|
+
if (existsSync(filePath)) {
|
|
31
|
+
foundIndicators.push(indicator.file);
|
|
32
|
+
if (confidence === "low" || confidence === "medium" && indicator.confidence === "high") {
|
|
33
|
+
detectedType = indicator.type;
|
|
34
|
+
confidence = indicator.confidence;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (foundIndicators.length > 0 && confidence === "low") {
|
|
39
|
+
confidence = "medium";
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
type: detectedType,
|
|
43
|
+
confidence,
|
|
44
|
+
indicators: foundIndicators
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function hasTypeScript(targetDir = process.cwd()) {
|
|
48
|
+
const tsConfigPath = join(targetDir, "tsconfig.json");
|
|
49
|
+
return existsSync(tsConfigPath);
|
|
50
|
+
}
|
|
51
|
+
async function hasEslint(targetDir = process.cwd()) {
|
|
52
|
+
const eslintFiles = [
|
|
53
|
+
".eslintrc",
|
|
54
|
+
".eslintrc.js",
|
|
55
|
+
".eslintrc.cjs",
|
|
56
|
+
".eslintrc.json",
|
|
57
|
+
".eslintrc.yaml",
|
|
58
|
+
".eslintrc.yml",
|
|
59
|
+
"eslint.config.js",
|
|
60
|
+
"eslint.config.mjs",
|
|
61
|
+
"eslint.config.cjs"
|
|
62
|
+
];
|
|
63
|
+
for (const file of eslintFiles) {
|
|
64
|
+
if (existsSync(join(targetDir, file))) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const pkgPath = join(targetDir, "package.json");
|
|
70
|
+
if (existsSync(pkgPath)) {
|
|
71
|
+
const content = await readFile(pkgPath, "utf-8");
|
|
72
|
+
const pkg = JSON.parse(content);
|
|
73
|
+
if (pkg.eslintConfig) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
async function hasPrettier(targetDir = process.cwd()) {
|
|
82
|
+
const prettierFiles = [
|
|
83
|
+
".prettierrc",
|
|
84
|
+
".prettierrc.js",
|
|
85
|
+
".prettierrc.cjs",
|
|
86
|
+
".prettierrc.json",
|
|
87
|
+
".prettierrc.yaml",
|
|
88
|
+
".prettierrc.yml",
|
|
89
|
+
".prettierrc.toml",
|
|
90
|
+
"prettier.config.js",
|
|
91
|
+
"prettier.config.cjs",
|
|
92
|
+
"prettier.config.mjs"
|
|
93
|
+
];
|
|
94
|
+
for (const file of prettierFiles) {
|
|
95
|
+
if (existsSync(join(targetDir, file))) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const pkgPath = join(targetDir, "package.json");
|
|
101
|
+
if (existsSync(pkgPath)) {
|
|
102
|
+
const content = await readFile(pkgPath, "utf-8");
|
|
103
|
+
const pkg = JSON.parse(content);
|
|
104
|
+
if (pkg.prettier) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
function getProjectTypeDescription(type) {
|
|
113
|
+
switch (type) {
|
|
114
|
+
case "nx":
|
|
115
|
+
return "NX Monorepo";
|
|
116
|
+
case "turbo":
|
|
117
|
+
return "Turborepo";
|
|
118
|
+
case "pnpm-workspace":
|
|
119
|
+
return "pnpm Workspace";
|
|
120
|
+
case "single":
|
|
121
|
+
return "Single Package";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/prompts/index.ts
|
|
126
|
+
function showWelcome() {
|
|
127
|
+
console.log();
|
|
128
|
+
p.intro(pc.bgCyan(pc.black(" RaftStack ")));
|
|
129
|
+
console.log(
|
|
130
|
+
pc.dim(" Setting up Git hooks, commit conventions, and GitHub integration\n")
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
async function promptProjectType(detection) {
|
|
134
|
+
const description = getProjectTypeDescription(detection.type);
|
|
135
|
+
const confidenceText = detection.confidence === "high" ? pc.green("high confidence") : detection.confidence === "medium" ? pc.yellow("medium confidence") : pc.red("low confidence");
|
|
136
|
+
const confirmed = await p.confirm({
|
|
137
|
+
message: `Detected ${pc.cyan(description)} (${confidenceText}). Is this correct?`,
|
|
138
|
+
initialValue: true
|
|
139
|
+
});
|
|
140
|
+
if (p.isCancel(confirmed)) {
|
|
141
|
+
p.cancel("Setup cancelled.");
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
if (confirmed) {
|
|
145
|
+
return detection.type;
|
|
146
|
+
}
|
|
147
|
+
const selected = await p.select({
|
|
148
|
+
message: "Select your project type:",
|
|
149
|
+
options: [
|
|
150
|
+
{ value: "nx", label: "NX Monorepo" },
|
|
151
|
+
{ value: "turbo", label: "Turborepo" },
|
|
152
|
+
{ value: "pnpm-workspace", label: "pnpm Workspace" },
|
|
153
|
+
{ value: "single", label: "Single Package" }
|
|
154
|
+
]
|
|
155
|
+
});
|
|
156
|
+
if (p.isCancel(selected)) {
|
|
157
|
+
p.cancel("Setup cancelled.");
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
return selected;
|
|
161
|
+
}
|
|
162
|
+
async function promptAsanaConfig() {
|
|
163
|
+
const useAsana = await p.confirm({
|
|
164
|
+
message: "Do you want to link commits to Asana tasks?",
|
|
165
|
+
initialValue: true
|
|
166
|
+
});
|
|
167
|
+
if (p.isCancel(useAsana)) {
|
|
168
|
+
p.cancel("Setup cancelled.");
|
|
169
|
+
process.exit(0);
|
|
170
|
+
}
|
|
171
|
+
if (!useAsana) {
|
|
172
|
+
return void 0;
|
|
173
|
+
}
|
|
174
|
+
const baseUrl = await p.text({
|
|
175
|
+
message: "Enter your Asana workspace URL:",
|
|
176
|
+
placeholder: "https://app.asana.com/0/workspace-id",
|
|
177
|
+
validate: (value) => {
|
|
178
|
+
if (!value) return "URL is required";
|
|
179
|
+
if (!value.startsWith("https://app.asana.com/")) {
|
|
180
|
+
return "URL must start with https://app.asana.com/";
|
|
181
|
+
}
|
|
182
|
+
return void 0;
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
if (p.isCancel(baseUrl)) {
|
|
186
|
+
p.cancel("Setup cancelled.");
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
return baseUrl;
|
|
190
|
+
}
|
|
191
|
+
async function promptAIReview() {
|
|
192
|
+
const selected = await p.select({
|
|
193
|
+
message: "Select an AI code review tool (optional):",
|
|
194
|
+
options: [
|
|
195
|
+
{
|
|
196
|
+
value: "none",
|
|
197
|
+
label: "None",
|
|
198
|
+
hint: "Skip AI review setup"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
value: "coderabbit",
|
|
202
|
+
label: "CodeRabbit",
|
|
203
|
+
hint: "AI-powered code review"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
value: "copilot",
|
|
207
|
+
label: "GitHub Copilot",
|
|
208
|
+
hint: "GitHub's AI code review"
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
});
|
|
212
|
+
if (p.isCancel(selected)) {
|
|
213
|
+
p.cancel("Setup cancelled.");
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
return selected;
|
|
217
|
+
}
|
|
218
|
+
async function promptCodeowners() {
|
|
219
|
+
const addOwners = await p.confirm({
|
|
220
|
+
message: "Do you want to set up CODEOWNERS for automatic PR reviewers?",
|
|
221
|
+
initialValue: true
|
|
222
|
+
});
|
|
223
|
+
if (p.isCancel(addOwners)) {
|
|
224
|
+
p.cancel("Setup cancelled.");
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}
|
|
227
|
+
if (!addOwners) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
const owners = await p.text({
|
|
231
|
+
message: "Enter GitHub usernames (comma-separated):",
|
|
232
|
+
placeholder: "@username1, @username2",
|
|
233
|
+
validate: (value) => {
|
|
234
|
+
if (!value.trim()) return "At least one username is required";
|
|
235
|
+
return void 0;
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
if (p.isCancel(owners)) {
|
|
239
|
+
p.cancel("Setup cancelled.");
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
return owners.split(",").map((u) => u.trim()).filter(Boolean).map((u) => u.startsWith("@") ? u : `@${u}`);
|
|
243
|
+
}
|
|
244
|
+
async function promptConfirmation(config) {
|
|
245
|
+
console.log();
|
|
246
|
+
p.note(
|
|
247
|
+
[
|
|
248
|
+
`${pc.cyan("Project Type:")} ${getProjectTypeDescription(config.projectType)}`,
|
|
249
|
+
`${pc.cyan("TypeScript:")} ${config.usesTypeScript ? "Yes" : "No"}`,
|
|
250
|
+
`${pc.cyan("ESLint:")} ${config.usesEslint ? "Yes" : "No"}`,
|
|
251
|
+
`${pc.cyan("Prettier:")} ${config.usesPrettier ? "Yes" : "No"}`,
|
|
252
|
+
`${pc.cyan("Asana Integration:")} ${config.asanaBaseUrl ? "Yes" : "No"}`,
|
|
253
|
+
`${pc.cyan("AI Review:")} ${config.aiReviewTool === "none" ? "None" : config.aiReviewTool}`,
|
|
254
|
+
`${pc.cyan("CODEOWNERS:")} ${config.codeowners.length > 0 ? config.codeowners.join(", ") : "None"}`
|
|
255
|
+
].join("\n"),
|
|
256
|
+
"Configuration Summary"
|
|
257
|
+
);
|
|
258
|
+
const confirmed = await p.confirm({
|
|
259
|
+
message: "Generate configuration files?",
|
|
260
|
+
initialValue: true
|
|
261
|
+
});
|
|
262
|
+
if (p.isCancel(confirmed)) {
|
|
263
|
+
p.cancel("Setup cancelled.");
|
|
264
|
+
process.exit(0);
|
|
265
|
+
}
|
|
266
|
+
return confirmed;
|
|
267
|
+
}
|
|
268
|
+
async function collectConfig(targetDir = process.cwd()) {
|
|
269
|
+
showWelcome();
|
|
270
|
+
const detection = await detectProjectType(targetDir);
|
|
271
|
+
const projectType = await promptProjectType(detection);
|
|
272
|
+
const usesTypeScript = await hasTypeScript(targetDir);
|
|
273
|
+
const usesEslint = await hasEslint(targetDir);
|
|
274
|
+
const usesPrettier = await hasPrettier(targetDir);
|
|
275
|
+
const asanaBaseUrl = await promptAsanaConfig();
|
|
276
|
+
const aiReviewTool = await promptAIReview();
|
|
277
|
+
const codeowners = await promptCodeowners();
|
|
278
|
+
const config = {
|
|
279
|
+
projectType,
|
|
280
|
+
asanaBaseUrl,
|
|
281
|
+
aiReviewTool,
|
|
282
|
+
codeowners,
|
|
283
|
+
usesTypeScript,
|
|
284
|
+
usesEslint,
|
|
285
|
+
usesPrettier
|
|
286
|
+
};
|
|
287
|
+
const confirmed = await promptConfirmation(config);
|
|
288
|
+
if (!confirmed) {
|
|
289
|
+
p.cancel("Setup cancelled.");
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return config;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/generators/husky.ts
|
|
296
|
+
import { join as join3 } from "path";
|
|
297
|
+
|
|
298
|
+
// src/utils/file-system.ts
|
|
299
|
+
import { existsSync as existsSync2 } from "fs";
|
|
300
|
+
import {
|
|
301
|
+
mkdir,
|
|
302
|
+
readFile as readFile2,
|
|
303
|
+
writeFile,
|
|
304
|
+
copyFile,
|
|
305
|
+
chmod
|
|
306
|
+
} from "fs/promises";
|
|
307
|
+
import { dirname, join as join2 } from "path";
|
|
308
|
+
async function ensureDir(dirPath) {
|
|
309
|
+
if (!existsSync2(dirPath)) {
|
|
310
|
+
await mkdir(dirPath, { recursive: true });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async function backupFile(filePath) {
|
|
314
|
+
if (!existsSync2(filePath)) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
const backupPath = `${filePath}.backup`;
|
|
318
|
+
await copyFile(filePath, backupPath);
|
|
319
|
+
return backupPath;
|
|
320
|
+
}
|
|
321
|
+
async function writeFileSafe(filePath, content, options = {}) {
|
|
322
|
+
const { backup = true, overwrite = true, executable = false } = options;
|
|
323
|
+
const exists = existsSync2(filePath);
|
|
324
|
+
if (exists && !overwrite) {
|
|
325
|
+
return { created: false, backedUp: null };
|
|
326
|
+
}
|
|
327
|
+
let backedUp = null;
|
|
328
|
+
if (exists && backup) {
|
|
329
|
+
backedUp = await backupFile(filePath);
|
|
330
|
+
}
|
|
331
|
+
await ensureDir(dirname(filePath));
|
|
332
|
+
await writeFile(filePath, content, "utf-8");
|
|
333
|
+
if (executable) {
|
|
334
|
+
await chmod(filePath, 493);
|
|
335
|
+
}
|
|
336
|
+
return { created: true, backedUp };
|
|
337
|
+
}
|
|
338
|
+
function fileExists(filePath) {
|
|
339
|
+
return existsSync2(filePath);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/generators/husky.ts
|
|
343
|
+
function getPreCommitHook(projectType) {
|
|
344
|
+
if (projectType === "nx") {
|
|
345
|
+
return `#!/usr/bin/env sh
|
|
346
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
347
|
+
|
|
348
|
+
npx lint-staged
|
|
349
|
+
`;
|
|
350
|
+
}
|
|
351
|
+
return `#!/usr/bin/env sh
|
|
352
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
353
|
+
|
|
354
|
+
npx lint-staged
|
|
355
|
+
`;
|
|
356
|
+
}
|
|
357
|
+
function getCommitMsgHook() {
|
|
358
|
+
return `#!/usr/bin/env sh
|
|
359
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
360
|
+
|
|
361
|
+
npx --no -- commitlint --edit "$1"
|
|
362
|
+
`;
|
|
363
|
+
}
|
|
364
|
+
function getPrePushHook() {
|
|
365
|
+
return `#!/usr/bin/env sh
|
|
366
|
+
. "$(dirname -- "$0")/_/husky.sh"
|
|
367
|
+
|
|
368
|
+
npx validate-branch-name
|
|
369
|
+
`;
|
|
370
|
+
}
|
|
371
|
+
async function generateHuskyHooks(targetDir, projectType) {
|
|
372
|
+
const result = {
|
|
373
|
+
created: [],
|
|
374
|
+
modified: [],
|
|
375
|
+
skipped: [],
|
|
376
|
+
backedUp: []
|
|
377
|
+
};
|
|
378
|
+
const huskyDir = join3(targetDir, ".husky");
|
|
379
|
+
await ensureDir(huskyDir);
|
|
380
|
+
const preCommitPath = join3(huskyDir, "pre-commit");
|
|
381
|
+
const preCommitResult = await writeFileSafe(
|
|
382
|
+
preCommitPath,
|
|
383
|
+
getPreCommitHook(projectType),
|
|
384
|
+
{ executable: true, backup: true }
|
|
385
|
+
);
|
|
386
|
+
if (preCommitResult.created) {
|
|
387
|
+
result.created.push(".husky/pre-commit");
|
|
388
|
+
if (preCommitResult.backedUp) {
|
|
389
|
+
result.backedUp.push(preCommitResult.backedUp);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const commitMsgPath = join3(huskyDir, "commit-msg");
|
|
393
|
+
const commitMsgResult = await writeFileSafe(
|
|
394
|
+
commitMsgPath,
|
|
395
|
+
getCommitMsgHook(),
|
|
396
|
+
{ executable: true, backup: true }
|
|
397
|
+
);
|
|
398
|
+
if (commitMsgResult.created) {
|
|
399
|
+
result.created.push(".husky/commit-msg");
|
|
400
|
+
if (commitMsgResult.backedUp) {
|
|
401
|
+
result.backedUp.push(commitMsgResult.backedUp);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const prePushPath = join3(huskyDir, "pre-push");
|
|
405
|
+
const prePushResult = await writeFileSafe(prePushPath, getPrePushHook(), {
|
|
406
|
+
executable: true,
|
|
407
|
+
backup: true
|
|
408
|
+
});
|
|
409
|
+
if (prePushResult.created) {
|
|
410
|
+
result.created.push(".husky/pre-push");
|
|
411
|
+
if (prePushResult.backedUp) {
|
|
412
|
+
result.backedUp.push(prePushResult.backedUp);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/generators/commitlint.ts
|
|
419
|
+
import { join as join4 } from "path";
|
|
420
|
+
function getCommitlintConfig(asanaBaseUrl) {
|
|
421
|
+
const baseConfig = `// @ts-check
|
|
422
|
+
|
|
423
|
+
/** @type {import('@commitlint/types').UserConfig} */
|
|
424
|
+
const config = {
|
|
425
|
+
extends: ['@commitlint/config-conventional'],
|
|
426
|
+
rules: {
|
|
427
|
+
// Type must be one of the conventional types
|
|
428
|
+
'type-enum': [
|
|
429
|
+
2,
|
|
430
|
+
'always',
|
|
431
|
+
[
|
|
432
|
+
'feat', // New feature
|
|
433
|
+
'fix', // Bug fix
|
|
434
|
+
'docs', // Documentation changes
|
|
435
|
+
'style', // Code style changes (formatting, etc.)
|
|
436
|
+
'refactor', // Code refactoring
|
|
437
|
+
'perf', // Performance improvements
|
|
438
|
+
'test', // Adding or updating tests
|
|
439
|
+
'build', // Build system changes
|
|
440
|
+
'ci', // CI configuration changes
|
|
441
|
+
'chore', // Other changes (maintenance, etc.)
|
|
442
|
+
'revert', // Reverting changes
|
|
443
|
+
],
|
|
444
|
+
],
|
|
445
|
+
// Subject should not be empty
|
|
446
|
+
'subject-empty': [2, 'never'],
|
|
447
|
+
// Type should not be empty
|
|
448
|
+
'type-empty': [2, 'never'],
|
|
449
|
+
// Subject should be lowercase
|
|
450
|
+
'subject-case': [2, 'always', 'lower-case'],
|
|
451
|
+
// Header max length
|
|
452
|
+
'header-max-length': [2, 'always', 100],
|
|
453
|
+
},`;
|
|
454
|
+
if (asanaBaseUrl) {
|
|
455
|
+
return `${baseConfig}
|
|
456
|
+
plugins: [
|
|
457
|
+
{
|
|
458
|
+
rules: {
|
|
459
|
+
'asana-task-link': ({ body, footer }) => {
|
|
460
|
+
const fullMessage = [body, footer].filter(Boolean).join('\\n');
|
|
461
|
+
const asanaPattern = /https:\\/\\/app\\.asana\\.com\\/\\d+\\/\\d+\\/\\d+/;
|
|
462
|
+
const hasAsanaLink = asanaPattern.test(fullMessage);
|
|
463
|
+
return [
|
|
464
|
+
hasAsanaLink,
|
|
465
|
+
hasAsanaLink
|
|
466
|
+
? null
|
|
467
|
+
: 'Consider adding an Asana task link in the commit body or footer (e.g., Task: https://app.asana.com/0/...)',
|
|
468
|
+
];
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// Enable the Asana task link rule as a WARNING (level 1)
|
|
476
|
+
// Change to level 2 if you want to BLOCK commits without Asana links
|
|
477
|
+
config.rules['asana-task-link'] = [1, 'always'];
|
|
478
|
+
|
|
479
|
+
module.exports = config;
|
|
480
|
+
`;
|
|
481
|
+
}
|
|
482
|
+
return `${baseConfig}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
module.exports = config;
|
|
486
|
+
`;
|
|
487
|
+
}
|
|
488
|
+
async function generateCommitlint(targetDir, asanaBaseUrl) {
|
|
489
|
+
const result = {
|
|
490
|
+
created: [],
|
|
491
|
+
modified: [],
|
|
492
|
+
skipped: [],
|
|
493
|
+
backedUp: []
|
|
494
|
+
};
|
|
495
|
+
const configPath = join4(targetDir, "commitlint.config.js");
|
|
496
|
+
const writeResult = await writeFileSafe(
|
|
497
|
+
configPath,
|
|
498
|
+
getCommitlintConfig(asanaBaseUrl),
|
|
499
|
+
{ backup: true }
|
|
500
|
+
);
|
|
501
|
+
if (writeResult.created) {
|
|
502
|
+
result.created.push("commitlint.config.js");
|
|
503
|
+
if (writeResult.backedUp) {
|
|
504
|
+
result.backedUp.push(writeResult.backedUp);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/generators/cz-git.ts
|
|
511
|
+
import { join as join5 } from "path";
|
|
512
|
+
function getCzGitConfig(asanaBaseUrl) {
|
|
513
|
+
const asanaSection = asanaBaseUrl ? `
|
|
514
|
+
// Asana task reference settings
|
|
515
|
+
allowCustomIssuePrefix: true,
|
|
516
|
+
allowEmptyIssuePrefix: true,
|
|
517
|
+
issuePrefixes: [
|
|
518
|
+
{ value: 'asana', name: 'asana: Link to Asana task' },
|
|
519
|
+
{ value: 'closes', name: 'closes: Close an issue' },
|
|
520
|
+
{ value: 'fixes', name: 'fixes: Fix an issue' },
|
|
521
|
+
],
|
|
522
|
+
customIssuePrefixAlign: 'top',` : `
|
|
523
|
+
allowCustomIssuePrefix: false,
|
|
524
|
+
allowEmptyIssuePrefix: true,`;
|
|
525
|
+
return `// @ts-check
|
|
526
|
+
|
|
527
|
+
/** @type {import('cz-git').UserConfig} */
|
|
528
|
+
module.exports = {
|
|
529
|
+
extends: ['@commitlint/config-conventional'],
|
|
530
|
+
prompt: {
|
|
531
|
+
alias: {
|
|
532
|
+
fd: 'docs: fix typos',
|
|
533
|
+
ur: 'docs: update README',
|
|
534
|
+
},
|
|
535
|
+
messages: {
|
|
536
|
+
type: "Select the type of change you're committing:",
|
|
537
|
+
scope: 'Denote the scope of this change (optional):',
|
|
538
|
+
customScope: 'Denote the scope of this change:',
|
|
539
|
+
subject: 'Write a short, imperative description of the change:\\n',
|
|
540
|
+
body: 'Provide a longer description of the change (optional). Use "|" to break new line:\\n',
|
|
541
|
+
breaking: 'List any BREAKING CHANGES (optional). Use "|" to break new line:\\n',
|
|
542
|
+
footerPrefixSelect: 'Select the ISSUES type of change (optional):',
|
|
543
|
+
customFooterPrefix: 'Input ISSUES prefix:',
|
|
544
|
+
footer: 'List any ISSUES affected by this change (optional). E.g.: #31, #34:\\n',
|
|
545
|
+
confirmCommit: 'Are you sure you want to proceed with the commit above?',
|
|
546
|
+
},
|
|
547
|
+
types: [
|
|
548
|
+
{ value: 'feat', name: 'feat: \u2728 A new feature', emoji: ':sparkles:' },
|
|
549
|
+
{ value: 'fix', name: 'fix: \u{1F41B} A bug fix', emoji: ':bug:' },
|
|
550
|
+
{ value: 'docs', name: 'docs: \u{1F4DD} Documentation changes', emoji: ':memo:' },
|
|
551
|
+
{ value: 'style', name: 'style: \u{1F484} Code style changes', emoji: ':lipstick:' },
|
|
552
|
+
{ value: 'refactor', name: 'refactor: \u267B\uFE0F Code refactoring', emoji: ':recycle:' },
|
|
553
|
+
{ value: 'perf', name: 'perf: \u26A1\uFE0F Performance improvements', emoji: ':zap:' },
|
|
554
|
+
{ value: 'test', name: 'test: \u2705 Adding or updating tests', emoji: ':white_check_mark:' },
|
|
555
|
+
{ value: 'build', name: 'build: \u{1F4E6} Build system changes', emoji: ':package:' },
|
|
556
|
+
{ value: 'ci', name: 'ci: \u{1F3A1} CI configuration changes', emoji: ':ferris_wheel:' },
|
|
557
|
+
{ value: 'chore', name: 'chore: \u{1F527} Other changes', emoji: ':wrench:' },
|
|
558
|
+
{ value: 'revert', name: 'revert: \u23EA Reverting changes', emoji: ':rewind:' },
|
|
559
|
+
],
|
|
560
|
+
useEmoji: true,
|
|
561
|
+
emojiAlign: 'center',
|
|
562
|
+
useAI: false,
|
|
563
|
+
aiNumber: 1,
|
|
564
|
+
themeColorCode: '',
|
|
565
|
+
scopes: [],
|
|
566
|
+
allowCustomScopes: true,
|
|
567
|
+
allowEmptyScopes: true,
|
|
568
|
+
customScopesAlign: 'bottom',
|
|
569
|
+
customScopesAlias: 'custom',
|
|
570
|
+
emptyScopesAlias: 'empty',
|
|
571
|
+
upperCaseSubject: false,
|
|
572
|
+
markBreakingChangeMode: false,
|
|
573
|
+
allowBreakingChanges: ['feat', 'fix'],
|
|
574
|
+
breaklineNumber: 100,
|
|
575
|
+
breaklineChar: '|',
|
|
576
|
+
skipQuestions: [],${asanaSection}
|
|
577
|
+
confirmColorize: true,
|
|
578
|
+
minSubjectLength: 0,
|
|
579
|
+
defaultBody: '',
|
|
580
|
+
defaultIssues: '',
|
|
581
|
+
defaultScope: '',
|
|
582
|
+
defaultSubject: '',
|
|
583
|
+
},
|
|
584
|
+
};
|
|
585
|
+
`;
|
|
586
|
+
}
|
|
587
|
+
async function generateCzGit(targetDir, asanaBaseUrl) {
|
|
588
|
+
const result = {
|
|
589
|
+
created: [],
|
|
590
|
+
modified: [],
|
|
591
|
+
skipped: [],
|
|
592
|
+
backedUp: []
|
|
593
|
+
};
|
|
594
|
+
const configPath = join5(targetDir, ".czrc");
|
|
595
|
+
const writeResult = await writeFileSafe(
|
|
596
|
+
configPath,
|
|
597
|
+
JSON.stringify({ path: "node_modules/cz-git" }, null, 2) + "\n",
|
|
598
|
+
{ backup: true }
|
|
599
|
+
);
|
|
600
|
+
if (writeResult.created) {
|
|
601
|
+
result.created.push(".czrc");
|
|
602
|
+
if (writeResult.backedUp) {
|
|
603
|
+
result.backedUp.push(writeResult.backedUp);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
const czConfigPath = join5(targetDir, "cz.config.js");
|
|
607
|
+
const czConfigResult = await writeFileSafe(
|
|
608
|
+
czConfigPath,
|
|
609
|
+
getCzGitConfig(asanaBaseUrl),
|
|
610
|
+
{ backup: true }
|
|
611
|
+
);
|
|
612
|
+
if (czConfigResult.created) {
|
|
613
|
+
result.created.push("cz.config.js");
|
|
614
|
+
if (czConfigResult.backedUp) {
|
|
615
|
+
result.backedUp.push(czConfigResult.backedUp);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/generators/lint-staged.ts
|
|
622
|
+
import { join as join6 } from "path";
|
|
623
|
+
function getLintStagedConfig(projectType, usesEslint, usesPrettier, usesTypeScript) {
|
|
624
|
+
const rules = {};
|
|
625
|
+
if (usesTypeScript) {
|
|
626
|
+
const tsCommands = [];
|
|
627
|
+
if (usesEslint) {
|
|
628
|
+
tsCommands.push("eslint --fix");
|
|
629
|
+
}
|
|
630
|
+
if (usesPrettier) {
|
|
631
|
+
tsCommands.push("prettier --write");
|
|
632
|
+
}
|
|
633
|
+
if (tsCommands.length > 0) {
|
|
634
|
+
rules["*.{ts,tsx}"] = tsCommands;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const jsCommands = [];
|
|
638
|
+
if (usesEslint) {
|
|
639
|
+
jsCommands.push("eslint --fix");
|
|
640
|
+
}
|
|
641
|
+
if (usesPrettier) {
|
|
642
|
+
jsCommands.push("prettier --write");
|
|
643
|
+
}
|
|
644
|
+
if (jsCommands.length > 0) {
|
|
645
|
+
rules["*.{js,jsx,mjs,cjs}"] = jsCommands;
|
|
646
|
+
}
|
|
647
|
+
if (usesPrettier) {
|
|
648
|
+
rules["*.{json,md,yaml,yml}"] = "prettier --write";
|
|
649
|
+
}
|
|
650
|
+
if (usesPrettier) {
|
|
651
|
+
rules["*.{css,scss,less}"] = "prettier --write";
|
|
652
|
+
}
|
|
653
|
+
if (projectType === "nx") {
|
|
654
|
+
return `// @ts-check
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* @type {import('lint-staged').Config}
|
|
658
|
+
*/
|
|
659
|
+
module.exports = {
|
|
660
|
+
${Object.entries(rules).map(([pattern, commands]) => {
|
|
661
|
+
const cmdStr = Array.isArray(commands) ? JSON.stringify(commands) : JSON.stringify([commands]);
|
|
662
|
+
return ` '${pattern}': ${cmdStr},`;
|
|
663
|
+
}).join("\n")}
|
|
664
|
+
};
|
|
665
|
+
`;
|
|
666
|
+
}
|
|
667
|
+
return `// @ts-check
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* @type {import('lint-staged').Config}
|
|
671
|
+
*/
|
|
672
|
+
module.exports = {
|
|
673
|
+
${Object.entries(rules).map(([pattern, commands]) => {
|
|
674
|
+
const cmdStr = Array.isArray(commands) ? JSON.stringify(commands) : JSON.stringify([commands]);
|
|
675
|
+
return ` '${pattern}': ${cmdStr},`;
|
|
676
|
+
}).join("\n")}
|
|
677
|
+
};
|
|
678
|
+
`;
|
|
679
|
+
}
|
|
680
|
+
async function generateLintStaged(targetDir, projectType, usesEslint, usesPrettier, usesTypeScript) {
|
|
681
|
+
const result = {
|
|
682
|
+
created: [],
|
|
683
|
+
modified: [],
|
|
684
|
+
skipped: [],
|
|
685
|
+
backedUp: []
|
|
686
|
+
};
|
|
687
|
+
const configPath = join6(targetDir, ".lintstagedrc.js");
|
|
688
|
+
const writeResult = await writeFileSafe(
|
|
689
|
+
configPath,
|
|
690
|
+
getLintStagedConfig(projectType, usesEslint, usesPrettier, usesTypeScript),
|
|
691
|
+
{ backup: true }
|
|
692
|
+
);
|
|
693
|
+
if (writeResult.created) {
|
|
694
|
+
result.created.push(".lintstagedrc.js");
|
|
695
|
+
if (writeResult.backedUp) {
|
|
696
|
+
result.backedUp.push(writeResult.backedUp);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return result;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/utils/package-json.ts
|
|
703
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
704
|
+
import { existsSync as existsSync3 } from "fs";
|
|
705
|
+
import { join as join7 } from "path";
|
|
706
|
+
async function readPackageJson(targetDir = process.cwd()) {
|
|
707
|
+
const pkgPath = join7(targetDir, "package.json");
|
|
708
|
+
if (!existsSync3(pkgPath)) {
|
|
709
|
+
throw new Error(`No package.json found in ${targetDir}`);
|
|
710
|
+
}
|
|
711
|
+
const content = await readFile3(pkgPath, "utf-8");
|
|
712
|
+
return JSON.parse(content);
|
|
713
|
+
}
|
|
714
|
+
async function writePackageJson(pkg, targetDir = process.cwd()) {
|
|
715
|
+
const pkgPath = join7(targetDir, "package.json");
|
|
716
|
+
const content = JSON.stringify(pkg, null, 2) + "\n";
|
|
717
|
+
await writeFile2(pkgPath, content, "utf-8");
|
|
718
|
+
}
|
|
719
|
+
function mergeScripts(pkg, scripts, overwrite = false) {
|
|
720
|
+
const existingScripts = pkg.scripts || {};
|
|
721
|
+
const newScripts = { ...existingScripts };
|
|
722
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
723
|
+
if (overwrite || !existingScripts[name]) {
|
|
724
|
+
newScripts[name] = command;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
...pkg,
|
|
729
|
+
scripts: newScripts
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
function mergeDevDependencies(pkg, deps) {
|
|
733
|
+
const existingDeps = pkg.devDependencies || {};
|
|
734
|
+
return {
|
|
735
|
+
...pkg,
|
|
736
|
+
devDependencies: {
|
|
737
|
+
...existingDeps,
|
|
738
|
+
...deps
|
|
739
|
+
}
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
function addPackageJsonConfig(pkg, key, config, overwrite = false) {
|
|
743
|
+
if (!overwrite && pkg[key]) {
|
|
744
|
+
return pkg;
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
...pkg,
|
|
748
|
+
[key]: config
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
var RAFTSTACK_DEV_DEPENDENCIES = {
|
|
752
|
+
"@commitlint/cli": "^18.0.0",
|
|
753
|
+
"@commitlint/config-conventional": "^18.0.0",
|
|
754
|
+
"cz-git": "^1.8.0",
|
|
755
|
+
czg: "^1.8.0",
|
|
756
|
+
husky: "^9.0.0",
|
|
757
|
+
"lint-staged": "^15.0.0",
|
|
758
|
+
"validate-branch-name": "^1.3.0"
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// src/generators/branch-validation.ts
|
|
762
|
+
function getBranchValidationConfig() {
|
|
763
|
+
return {
|
|
764
|
+
pattern: "^(main|master|develop|staging|production)$|^(feature|fix|hotfix|bugfix|release|chore|docs|refactor|test|ci)\\/[a-z0-9._-]+$",
|
|
765
|
+
errorMsg: "Branch name must follow pattern: feature/*, fix/*, hotfix/*, bugfix/*, release/*, chore/*, docs/*, refactor/*, test/*, ci/* or be main/master/develop/staging/production"
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
async function generateBranchValidation(targetDir) {
|
|
769
|
+
const result = {
|
|
770
|
+
created: [],
|
|
771
|
+
modified: [],
|
|
772
|
+
skipped: [],
|
|
773
|
+
backedUp: []
|
|
774
|
+
};
|
|
775
|
+
try {
|
|
776
|
+
const pkg = await readPackageJson(targetDir);
|
|
777
|
+
const config = getBranchValidationConfig();
|
|
778
|
+
const updatedPkg = addPackageJsonConfig(
|
|
779
|
+
pkg,
|
|
780
|
+
"validate-branch-name",
|
|
781
|
+
config,
|
|
782
|
+
false
|
|
783
|
+
// Don't overwrite if exists
|
|
784
|
+
);
|
|
785
|
+
if (JSON.stringify(pkg) !== JSON.stringify(updatedPkg)) {
|
|
786
|
+
await writePackageJson(updatedPkg, targetDir);
|
|
787
|
+
result.modified.push("package.json (validate-branch-name)");
|
|
788
|
+
} else {
|
|
789
|
+
result.skipped.push("validate-branch-name config (already exists)");
|
|
790
|
+
}
|
|
791
|
+
} catch (error) {
|
|
792
|
+
result.skipped.push("validate-branch-name config (no package.json)");
|
|
793
|
+
}
|
|
794
|
+
return result;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/generators/pr-template.ts
|
|
798
|
+
import { join as join8 } from "path";
|
|
799
|
+
function getPRTemplate(hasAsana) {
|
|
800
|
+
const asanaSection = hasAsana ? `## Asana Task
|
|
801
|
+
<!-- Link to the Asana task -->
|
|
802
|
+
- [ ] https://app.asana.com/0/...
|
|
803
|
+
|
|
804
|
+
` : "";
|
|
805
|
+
return `## Description
|
|
806
|
+
<!-- Provide a brief description of the changes in this PR -->
|
|
807
|
+
|
|
808
|
+
## Type of Change
|
|
809
|
+
<!-- Mark the appropriate option with an "x" -->
|
|
810
|
+
- [ ] \u{1F41B} Bug fix (non-breaking change that fixes an issue)
|
|
811
|
+
- [ ] \u2728 New feature (non-breaking change that adds functionality)
|
|
812
|
+
- [ ] \u{1F4A5} Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
|
813
|
+
- [ ] \u{1F4DD} Documentation update
|
|
814
|
+
- [ ] \u{1F527} Configuration change
|
|
815
|
+
- [ ] \u267B\uFE0F Refactoring (no functional changes)
|
|
816
|
+
- [ ] \u2705 Test update
|
|
817
|
+
|
|
818
|
+
${asanaSection}## Changes Made
|
|
819
|
+
<!-- List the specific changes made in this PR -->
|
|
820
|
+
-
|
|
821
|
+
|
|
822
|
+
## Testing
|
|
823
|
+
<!-- Describe how you tested your changes -->
|
|
824
|
+
- [ ] Unit tests added/updated
|
|
825
|
+
- [ ] Integration tests added/updated
|
|
826
|
+
- [ ] Manual testing performed
|
|
827
|
+
|
|
828
|
+
## Screenshots (if applicable)
|
|
829
|
+
<!-- Add screenshots to help explain your changes -->
|
|
830
|
+
|
|
831
|
+
## Checklist
|
|
832
|
+
- [ ] My code follows the project's coding standards
|
|
833
|
+
- [ ] I have performed a self-review of my code
|
|
834
|
+
- [ ] I have commented my code, particularly in hard-to-understand areas
|
|
835
|
+
- [ ] I have made corresponding changes to the documentation
|
|
836
|
+
- [ ] My changes generate no new warnings
|
|
837
|
+
- [ ] Any dependent changes have been merged and published
|
|
838
|
+
|
|
839
|
+
## Additional Notes
|
|
840
|
+
<!-- Add any additional information that reviewers should know -->
|
|
841
|
+
`;
|
|
842
|
+
}
|
|
843
|
+
async function generatePRTemplate(targetDir, hasAsana) {
|
|
844
|
+
const result = {
|
|
845
|
+
created: [],
|
|
846
|
+
modified: [],
|
|
847
|
+
skipped: [],
|
|
848
|
+
backedUp: []
|
|
849
|
+
};
|
|
850
|
+
const githubDir = join8(targetDir, ".github");
|
|
851
|
+
await ensureDir(githubDir);
|
|
852
|
+
const templatePath = join8(githubDir, "PULL_REQUEST_TEMPLATE.md");
|
|
853
|
+
const writeResult = await writeFileSafe(
|
|
854
|
+
templatePath,
|
|
855
|
+
getPRTemplate(hasAsana),
|
|
856
|
+
{ backup: true }
|
|
857
|
+
);
|
|
858
|
+
if (writeResult.created) {
|
|
859
|
+
result.created.push(".github/PULL_REQUEST_TEMPLATE.md");
|
|
860
|
+
if (writeResult.backedUp) {
|
|
861
|
+
result.backedUp.push(writeResult.backedUp);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return result;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/generators/github-workflows.ts
|
|
868
|
+
import { join as join9 } from "path";
|
|
869
|
+
function getPRChecksWorkflow(projectType, usesTypeScript, usesEslint) {
|
|
870
|
+
const steps = [];
|
|
871
|
+
steps.push(` - name: Checkout
|
|
872
|
+
uses: actions/checkout@v4`);
|
|
873
|
+
steps.push(`
|
|
874
|
+
- name: Setup Node.js
|
|
875
|
+
uses: actions/setup-node@v4
|
|
876
|
+
with:
|
|
877
|
+
node-version: '20'`);
|
|
878
|
+
steps.push(`
|
|
879
|
+
- name: Setup pnpm
|
|
880
|
+
uses: pnpm/action-setup@v3
|
|
881
|
+
with:
|
|
882
|
+
version: 9`);
|
|
883
|
+
steps.push(`
|
|
884
|
+
- name: Install dependencies
|
|
885
|
+
run: pnpm install --frozen-lockfile`);
|
|
886
|
+
if (usesTypeScript) {
|
|
887
|
+
steps.push(`
|
|
888
|
+
- name: Type check
|
|
889
|
+
run: pnpm typecheck`);
|
|
890
|
+
}
|
|
891
|
+
if (usesEslint) {
|
|
892
|
+
steps.push(`
|
|
893
|
+
- name: Lint
|
|
894
|
+
run: pnpm lint`);
|
|
895
|
+
}
|
|
896
|
+
if (projectType === "nx") {
|
|
897
|
+
steps.push(`
|
|
898
|
+
- name: Build
|
|
899
|
+
run: pnpm nx affected --target=build --parallel=3`);
|
|
900
|
+
} else if (projectType === "turbo") {
|
|
901
|
+
steps.push(`
|
|
902
|
+
- name: Build
|
|
903
|
+
run: pnpm turbo build`);
|
|
904
|
+
} else {
|
|
905
|
+
steps.push(`
|
|
906
|
+
- name: Build
|
|
907
|
+
run: pnpm build`);
|
|
908
|
+
}
|
|
909
|
+
if (projectType === "nx") {
|
|
910
|
+
steps.push(`
|
|
911
|
+
- name: Test
|
|
912
|
+
run: pnpm nx affected --target=test --parallel=3`);
|
|
913
|
+
} else if (projectType === "turbo") {
|
|
914
|
+
steps.push(`
|
|
915
|
+
- name: Test
|
|
916
|
+
run: pnpm turbo test`);
|
|
917
|
+
} else {
|
|
918
|
+
steps.push(`
|
|
919
|
+
- name: Test
|
|
920
|
+
run: pnpm test`);
|
|
921
|
+
}
|
|
922
|
+
return `name: PR Checks
|
|
923
|
+
|
|
924
|
+
on:
|
|
925
|
+
pull_request:
|
|
926
|
+
branches: [main, master, develop]
|
|
927
|
+
|
|
928
|
+
concurrency:
|
|
929
|
+
group: \${{ github.workflow }}-\${{ github.ref }}
|
|
930
|
+
cancel-in-progress: true
|
|
931
|
+
|
|
932
|
+
jobs:
|
|
933
|
+
check:
|
|
934
|
+
name: Check
|
|
935
|
+
runs-on: ubuntu-latest
|
|
936
|
+
steps:
|
|
937
|
+
${steps.join("\n")}
|
|
938
|
+
`;
|
|
939
|
+
}
|
|
940
|
+
async function generateGitHubWorkflows(targetDir, projectType, usesTypeScript, usesEslint) {
|
|
941
|
+
const result = {
|
|
942
|
+
created: [],
|
|
943
|
+
modified: [],
|
|
944
|
+
skipped: [],
|
|
945
|
+
backedUp: []
|
|
946
|
+
};
|
|
947
|
+
const workflowsDir = join9(targetDir, ".github", "workflows");
|
|
948
|
+
await ensureDir(workflowsDir);
|
|
949
|
+
const prChecksPath = join9(workflowsDir, "pr-checks.yml");
|
|
950
|
+
const writeResult = await writeFileSafe(
|
|
951
|
+
prChecksPath,
|
|
952
|
+
getPRChecksWorkflow(projectType, usesTypeScript, usesEslint),
|
|
953
|
+
{ backup: true }
|
|
954
|
+
);
|
|
955
|
+
if (writeResult.created) {
|
|
956
|
+
result.created.push(".github/workflows/pr-checks.yml");
|
|
957
|
+
if (writeResult.backedUp) {
|
|
958
|
+
result.backedUp.push(writeResult.backedUp);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return result;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/generators/codeowners.ts
|
|
965
|
+
import { join as join10 } from "path";
|
|
966
|
+
function getCodeownersContent(owners) {
|
|
967
|
+
if (owners.length === 0) {
|
|
968
|
+
return `# CODEOWNERS file
|
|
969
|
+
# Learn about CODEOWNERS: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
|
970
|
+
|
|
971
|
+
# Default owners for everything in the repo
|
|
972
|
+
# Uncomment and modify the line below to set default owners
|
|
973
|
+
# * @owner1 @owner2
|
|
974
|
+
`;
|
|
975
|
+
}
|
|
976
|
+
const ownersList = owners.join(" ");
|
|
977
|
+
return `# CODEOWNERS file
|
|
978
|
+
# Learn about CODEOWNERS: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
|
979
|
+
|
|
980
|
+
# Default owners for everything in the repo
|
|
981
|
+
* ${ownersList}
|
|
982
|
+
|
|
983
|
+
# You can also specify owners for specific paths:
|
|
984
|
+
# /docs/ @docs-team
|
|
985
|
+
# /src/api/ @backend-team
|
|
986
|
+
# /src/ui/ @frontend-team
|
|
987
|
+
# *.ts @typescript-team
|
|
988
|
+
`;
|
|
989
|
+
}
|
|
990
|
+
async function generateCodeowners(targetDir, owners) {
|
|
991
|
+
const result = {
|
|
992
|
+
created: [],
|
|
993
|
+
modified: [],
|
|
994
|
+
skipped: [],
|
|
995
|
+
backedUp: []
|
|
996
|
+
};
|
|
997
|
+
const githubDir = join10(targetDir, ".github");
|
|
998
|
+
await ensureDir(githubDir);
|
|
999
|
+
const codeownersPath = join10(githubDir, "CODEOWNERS");
|
|
1000
|
+
const writeResult = await writeFileSafe(
|
|
1001
|
+
codeownersPath,
|
|
1002
|
+
getCodeownersContent(owners),
|
|
1003
|
+
{ backup: true }
|
|
1004
|
+
);
|
|
1005
|
+
if (writeResult.created) {
|
|
1006
|
+
result.created.push(".github/CODEOWNERS");
|
|
1007
|
+
if (writeResult.backedUp) {
|
|
1008
|
+
result.backedUp.push(writeResult.backedUp);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return result;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// src/generators/ai-review.ts
|
|
1015
|
+
import { join as join11 } from "path";
|
|
1016
|
+
function getCodeRabbitConfig() {
|
|
1017
|
+
return `# CodeRabbit Configuration
|
|
1018
|
+
# Learn more: https://docs.coderabbit.ai/guides/configure-coderabbit
|
|
1019
|
+
|
|
1020
|
+
language: "en-US"
|
|
1021
|
+
|
|
1022
|
+
reviews:
|
|
1023
|
+
request_changes_workflow: true
|
|
1024
|
+
high_level_summary: true
|
|
1025
|
+
poem: false
|
|
1026
|
+
review_status: true
|
|
1027
|
+
collapse_walkthrough: false
|
|
1028
|
+
auto_review:
|
|
1029
|
+
enabled: true
|
|
1030
|
+
drafts: false
|
|
1031
|
+
|
|
1032
|
+
chat:
|
|
1033
|
+
auto_reply: true
|
|
1034
|
+
`;
|
|
1035
|
+
}
|
|
1036
|
+
function getCopilotWorkflow() {
|
|
1037
|
+
return `name: Copilot Code Review
|
|
1038
|
+
|
|
1039
|
+
on:
|
|
1040
|
+
pull_request:
|
|
1041
|
+
types: [opened, synchronize, reopened]
|
|
1042
|
+
|
|
1043
|
+
permissions:
|
|
1044
|
+
contents: read
|
|
1045
|
+
pull-requests: write
|
|
1046
|
+
|
|
1047
|
+
jobs:
|
|
1048
|
+
review:
|
|
1049
|
+
name: Copilot Review
|
|
1050
|
+
runs-on: ubuntu-latest
|
|
1051
|
+
steps:
|
|
1052
|
+
- name: Checkout
|
|
1053
|
+
uses: actions/checkout@v4
|
|
1054
|
+
|
|
1055
|
+
# Note: GitHub Copilot code review is automatically enabled
|
|
1056
|
+
# when you have Copilot Enterprise. This workflow is a placeholder
|
|
1057
|
+
# for any additional AI review configuration you might need.
|
|
1058
|
+
|
|
1059
|
+
- name: Add review comment
|
|
1060
|
+
uses: actions/github-script@v7
|
|
1061
|
+
with:
|
|
1062
|
+
script: |
|
|
1063
|
+
// GitHub Copilot will automatically review PRs if enabled
|
|
1064
|
+
// This is a placeholder for additional review logic
|
|
1065
|
+
console.log('Copilot review enabled for this repository');
|
|
1066
|
+
`;
|
|
1067
|
+
}
|
|
1068
|
+
async function generateAIReview(targetDir, tool) {
|
|
1069
|
+
const result = {
|
|
1070
|
+
created: [],
|
|
1071
|
+
modified: [],
|
|
1072
|
+
skipped: [],
|
|
1073
|
+
backedUp: []
|
|
1074
|
+
};
|
|
1075
|
+
if (tool === "none") {
|
|
1076
|
+
return result;
|
|
1077
|
+
}
|
|
1078
|
+
if (tool === "coderabbit") {
|
|
1079
|
+
const configPath = join11(targetDir, ".coderabbit.yaml");
|
|
1080
|
+
const writeResult = await writeFileSafe(configPath, getCodeRabbitConfig(), {
|
|
1081
|
+
backup: true
|
|
1082
|
+
});
|
|
1083
|
+
if (writeResult.created) {
|
|
1084
|
+
result.created.push(".coderabbit.yaml");
|
|
1085
|
+
if (writeResult.backedUp) {
|
|
1086
|
+
result.backedUp.push(writeResult.backedUp);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
if (tool === "copilot") {
|
|
1091
|
+
const workflowsDir = join11(targetDir, ".github", "workflows");
|
|
1092
|
+
await ensureDir(workflowsDir);
|
|
1093
|
+
const workflowPath = join11(workflowsDir, "copilot-review.yml");
|
|
1094
|
+
const writeResult = await writeFileSafe(workflowPath, getCopilotWorkflow(), {
|
|
1095
|
+
backup: true
|
|
1096
|
+
});
|
|
1097
|
+
if (writeResult.created) {
|
|
1098
|
+
result.created.push(".github/workflows/copilot-review.yml");
|
|
1099
|
+
if (writeResult.backedUp) {
|
|
1100
|
+
result.backedUp.push(writeResult.backedUp);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
return result;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/generators/branch-protection.ts
|
|
1108
|
+
import { join as join12 } from "path";
|
|
1109
|
+
function getBranchProtectionDocs() {
|
|
1110
|
+
return `# Branch Protection Setup Guide
|
|
1111
|
+
|
|
1112
|
+
This guide explains how to configure branch protection rules for your repository.
|
|
1113
|
+
|
|
1114
|
+
## Quick Setup
|
|
1115
|
+
|
|
1116
|
+
Run the automated setup command:
|
|
1117
|
+
|
|
1118
|
+
\`\`\`bash
|
|
1119
|
+
raftstack setup-protection
|
|
1120
|
+
\`\`\`
|
|
1121
|
+
|
|
1122
|
+
This command supports:
|
|
1123
|
+
- **Multiple branches**: main, staging, production, development, etc.
|
|
1124
|
+
- **Merge strategies**: Rebase (recommended), squash, or merge commits
|
|
1125
|
+
- **Review requirements**: Configurable number of required approvals
|
|
1126
|
+
|
|
1127
|
+
## Recommended Settings
|
|
1128
|
+
|
|
1129
|
+
### For \`main\` / \`master\` branch:
|
|
1130
|
+
|
|
1131
|
+
1. **Require a pull request before merging**
|
|
1132
|
+
- \u2705 Require approvals: 1 (or more for larger teams)
|
|
1133
|
+
- \u2705 Dismiss stale pull request approvals when new commits are pushed
|
|
1134
|
+
- \u2705 Require review from Code Owners
|
|
1135
|
+
|
|
1136
|
+
2. **Require status checks to pass before merging**
|
|
1137
|
+
- \u2705 Require branches to be up to date before merging
|
|
1138
|
+
- Select required status checks:
|
|
1139
|
+
- \`check\` (from pr-checks.yml workflow)
|
|
1140
|
+
|
|
1141
|
+
3. **Require conversation resolution before merging**
|
|
1142
|
+
- \u2705 All conversations on code must be resolved
|
|
1143
|
+
|
|
1144
|
+
4. **Do not allow bypassing the above settings**
|
|
1145
|
+
- \u2705 Apply rules to administrators
|
|
1146
|
+
|
|
1147
|
+
5. **Restrict who can push to matching branches**
|
|
1148
|
+
- Only allow merges through pull requests
|
|
1149
|
+
|
|
1150
|
+
6. **Block force pushes**
|
|
1151
|
+
- \u2705 Do not allow force pushes
|
|
1152
|
+
|
|
1153
|
+
7. **Block deletions**
|
|
1154
|
+
- \u2705 Do not allow this branch to be deleted
|
|
1155
|
+
|
|
1156
|
+
## Manual Setup via GitHub UI
|
|
1157
|
+
|
|
1158
|
+
1. Go to your repository on GitHub
|
|
1159
|
+
2. Click **Settings** > **Branches**
|
|
1160
|
+
3. Click **Add branch protection rule**
|
|
1161
|
+
4. Enter \`main\` (or \`master\`) as the branch name pattern
|
|
1162
|
+
5. Configure the settings as described above
|
|
1163
|
+
6. Click **Create** or **Save changes**
|
|
1164
|
+
|
|
1165
|
+
## Automated Setup (Recommended)
|
|
1166
|
+
|
|
1167
|
+
Use the \`raftstack setup-protection\` command to configure
|
|
1168
|
+
branch protection rules automatically using the GitHub CLI.
|
|
1169
|
+
|
|
1170
|
+
Requirements:
|
|
1171
|
+
- GitHub CLI (\`gh\`) installed and authenticated
|
|
1172
|
+
- Admin access to the repository
|
|
1173
|
+
|
|
1174
|
+
\`\`\`bash
|
|
1175
|
+
raftstack setup-protection
|
|
1176
|
+
\`\`\`
|
|
1177
|
+
|
|
1178
|
+
### Features
|
|
1179
|
+
|
|
1180
|
+
The setup command will:
|
|
1181
|
+
1. Prompt you to select branches to protect (main, staging, production, etc.)
|
|
1182
|
+
2. Let you choose a merge strategy (rebase, squash, or merge commits)
|
|
1183
|
+
3. Configure required review count
|
|
1184
|
+
4. Apply branch protection rules to all selected branches
|
|
1185
|
+
5. Set repository merge settings
|
|
1186
|
+
|
|
1187
|
+
### Merge Strategy Recommendations
|
|
1188
|
+
|
|
1189
|
+
| Strategy | Use Case |
|
|
1190
|
+
|----------|----------|
|
|
1191
|
+
| **Rebase** (recommended) | Clean linear history, easy to follow |
|
|
1192
|
+
| **Squash** | Single commit per PR, cleaner history |
|
|
1193
|
+
| **Merge commit** | Preserve all commits, show PR merge points |
|
|
1194
|
+
|
|
1195
|
+
## Branch Naming Convention
|
|
1196
|
+
|
|
1197
|
+
This project enforces branch naming conventions via \`validate-branch-name\`.
|
|
1198
|
+
|
|
1199
|
+
Allowed patterns:
|
|
1200
|
+
- \`main\`, \`master\`, \`develop\`, \`staging\`, \`production\`
|
|
1201
|
+
- \`feature/*\` - New features
|
|
1202
|
+
- \`fix/*\` or \`bugfix/*\` - Bug fixes
|
|
1203
|
+
- \`hotfix/*\` - Urgent fixes
|
|
1204
|
+
- \`release/*\` - Release preparation
|
|
1205
|
+
- \`chore/*\` - Maintenance tasks
|
|
1206
|
+
- \`docs/*\` - Documentation updates
|
|
1207
|
+
- \`refactor/*\` - Code refactoring
|
|
1208
|
+
- \`test/*\` - Test additions/updates
|
|
1209
|
+
- \`ci/*\` - CI/CD changes
|
|
1210
|
+
|
|
1211
|
+
Examples:
|
|
1212
|
+
- \`feature/user-authentication\`
|
|
1213
|
+
- \`fix/login-validation\`
|
|
1214
|
+
- \`hotfix/security-patch\`
|
|
1215
|
+
- \`release/v1.2.0\`
|
|
1216
|
+
`;
|
|
1217
|
+
}
|
|
1218
|
+
async function generateBranchProtectionDocs(targetDir) {
|
|
1219
|
+
const result = {
|
|
1220
|
+
created: [],
|
|
1221
|
+
modified: [],
|
|
1222
|
+
skipped: [],
|
|
1223
|
+
backedUp: []
|
|
1224
|
+
};
|
|
1225
|
+
const docsDir = join12(targetDir, ".github");
|
|
1226
|
+
await ensureDir(docsDir);
|
|
1227
|
+
const docsPath = join12(docsDir, "BRANCH_PROTECTION_SETUP.md");
|
|
1228
|
+
const writeResult = await writeFileSafe(docsPath, getBranchProtectionDocs(), {
|
|
1229
|
+
backup: true
|
|
1230
|
+
});
|
|
1231
|
+
if (writeResult.created) {
|
|
1232
|
+
result.created.push(".github/BRANCH_PROTECTION_SETUP.md");
|
|
1233
|
+
if (writeResult.backedUp) {
|
|
1234
|
+
result.backedUp.push(writeResult.backedUp);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
return result;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/generators/contributing.ts
|
|
1241
|
+
import { join as join13 } from "path";
|
|
1242
|
+
function getContributingContent(hasAsana) {
|
|
1243
|
+
const asanaSection = hasAsana ? `
|
|
1244
|
+
## Linking to Asana
|
|
1245
|
+
|
|
1246
|
+
When working on a task:
|
|
1247
|
+
1. Create a branch following the naming convention (e.g., \`feature/task-description\`)
|
|
1248
|
+
2. Include the Asana task link in your commit body or footer
|
|
1249
|
+
3. Reference the Asana task in your PR description
|
|
1250
|
+
|
|
1251
|
+
Example commit:
|
|
1252
|
+
\`\`\`
|
|
1253
|
+
feat(auth): add password reset functionality
|
|
1254
|
+
|
|
1255
|
+
Implement password reset flow with email verification.
|
|
1256
|
+
|
|
1257
|
+
Asana: https://app.asana.com/0/workspace/task-id
|
|
1258
|
+
\`\`\`
|
|
1259
|
+
` : "";
|
|
1260
|
+
return `# Contributing Guide
|
|
1261
|
+
|
|
1262
|
+
Thank you for your interest in contributing! This document outlines our development workflow and standards.
|
|
1263
|
+
|
|
1264
|
+
## Getting Started
|
|
1265
|
+
|
|
1266
|
+
1. Clone the repository
|
|
1267
|
+
2. Install dependencies: \`pnpm install\`
|
|
1268
|
+
3. Create a new branch following our naming convention
|
|
1269
|
+
|
|
1270
|
+
## Branch Naming Convention
|
|
1271
|
+
|
|
1272
|
+
We use structured branch names to keep our repository organized:
|
|
1273
|
+
|
|
1274
|
+
| Prefix | Purpose | Example |
|
|
1275
|
+
|--------|---------|---------|
|
|
1276
|
+
| \`feature/\` | New features | \`feature/user-authentication\` |
|
|
1277
|
+
| \`fix/\` | Bug fixes | \`fix/login-validation\` |
|
|
1278
|
+
| \`hotfix/\` | Urgent fixes | \`hotfix/security-patch\` |
|
|
1279
|
+
| \`bugfix/\` | Bug fixes (alternative) | \`bugfix/form-submission\` |
|
|
1280
|
+
| \`release/\` | Release preparation | \`release/v1.2.0\` |
|
|
1281
|
+
| \`chore/\` | Maintenance tasks | \`chore/update-dependencies\` |
|
|
1282
|
+
| \`docs/\` | Documentation | \`docs/api-reference\` |
|
|
1283
|
+
| \`refactor/\` | Code refactoring | \`refactor/auth-module\` |
|
|
1284
|
+
| \`test/\` | Test additions | \`test/user-service\` |
|
|
1285
|
+
| \`ci/\` | CI/CD changes | \`ci/github-actions\` |
|
|
1286
|
+
|
|
1287
|
+
## Commit Convention
|
|
1288
|
+
|
|
1289
|
+
We follow [Conventional Commits](https://www.conventionalcommits.org/). Use the interactive commit tool:
|
|
1290
|
+
|
|
1291
|
+
\`\`\`bash
|
|
1292
|
+
pnpm commit
|
|
1293
|
+
\`\`\`
|
|
1294
|
+
|
|
1295
|
+
### Commit Types
|
|
1296
|
+
|
|
1297
|
+
| Type | Description |
|
|
1298
|
+
|------|-------------|
|
|
1299
|
+
| \`feat\` | New feature |
|
|
1300
|
+
| \`fix\` | Bug fix |
|
|
1301
|
+
| \`docs\` | Documentation changes |
|
|
1302
|
+
| \`style\` | Code style changes (formatting, etc.) |
|
|
1303
|
+
| \`refactor\` | Code refactoring |
|
|
1304
|
+
| \`perf\` | Performance improvements |
|
|
1305
|
+
| \`test\` | Adding or updating tests |
|
|
1306
|
+
| \`build\` | Build system changes |
|
|
1307
|
+
| \`ci\` | CI configuration changes |
|
|
1308
|
+
| \`chore\` | Other changes |
|
|
1309
|
+
| \`revert\` | Reverting changes |
|
|
1310
|
+
|
|
1311
|
+
### Commit Message Format
|
|
1312
|
+
|
|
1313
|
+
\`\`\`
|
|
1314
|
+
<type>(<scope>): <subject>
|
|
1315
|
+
|
|
1316
|
+
<body>
|
|
1317
|
+
|
|
1318
|
+
<footer>
|
|
1319
|
+
\`\`\`
|
|
1320
|
+
|
|
1321
|
+
Example:
|
|
1322
|
+
\`\`\`
|
|
1323
|
+
feat(auth): add social login support
|
|
1324
|
+
|
|
1325
|
+
Implement OAuth2 login for Google and GitHub providers.
|
|
1326
|
+
Includes user profile sync and token refresh.
|
|
1327
|
+
|
|
1328
|
+
Closes #123
|
|
1329
|
+
\`\`\`
|
|
1330
|
+
${asanaSection}
|
|
1331
|
+
## Pull Request Process
|
|
1332
|
+
|
|
1333
|
+
1. Ensure your branch is up to date with \`main\`/\`master\`
|
|
1334
|
+
2. Run tests and linting locally
|
|
1335
|
+
3. Create a pull request using the provided template
|
|
1336
|
+
4. Request review from code owners
|
|
1337
|
+
5. Address any feedback
|
|
1338
|
+
6. Merge once approved and all checks pass
|
|
1339
|
+
|
|
1340
|
+
### PR Size Guidelines
|
|
1341
|
+
|
|
1342
|
+
Keep pull requests small and focused for faster reviews:
|
|
1343
|
+
|
|
1344
|
+
| Size | Lines Changed | Review Time |
|
|
1345
|
+
|------|---------------|-------------|
|
|
1346
|
+
| XS | 0-10 lines | Minutes |
|
|
1347
|
+
| S | 11-50 lines | < 30 min |
|
|
1348
|
+
| M | 51-200 lines | < 1 hour |
|
|
1349
|
+
| L | 201-400 lines | 1-2 hours |
|
|
1350
|
+
| XL | 400+ lines | Needs justification |
|
|
1351
|
+
|
|
1352
|
+
**Target: < 400 lines per PR**
|
|
1353
|
+
|
|
1354
|
+
If your PR is large:
|
|
1355
|
+
- Consider splitting it into smaller, logical PRs
|
|
1356
|
+
- Explain in the description why it can't be split
|
|
1357
|
+
|
|
1358
|
+
## Code Quality
|
|
1359
|
+
|
|
1360
|
+
Before committing, the following checks run automatically:
|
|
1361
|
+
|
|
1362
|
+
- **Linting**: ESLint checks for code quality
|
|
1363
|
+
- **Formatting**: Prettier ensures consistent style
|
|
1364
|
+
- **Type checking**: TypeScript validates types
|
|
1365
|
+
- **Commit messages**: Commitlint validates format
|
|
1366
|
+
- **Branch names**: validate-branch-name checks naming
|
|
1367
|
+
|
|
1368
|
+
## Questions?
|
|
1369
|
+
|
|
1370
|
+
If you have questions, please open an issue or reach out to the maintainers.
|
|
1371
|
+
`;
|
|
1372
|
+
}
|
|
1373
|
+
async function generateContributing(targetDir, hasAsana) {
|
|
1374
|
+
const result = {
|
|
1375
|
+
created: [],
|
|
1376
|
+
modified: [],
|
|
1377
|
+
skipped: [],
|
|
1378
|
+
backedUp: []
|
|
1379
|
+
};
|
|
1380
|
+
const contributingPath = join13(targetDir, "CONTRIBUTING.md");
|
|
1381
|
+
const writeResult = await writeFileSafe(
|
|
1382
|
+
contributingPath,
|
|
1383
|
+
getContributingContent(hasAsana),
|
|
1384
|
+
{ backup: true }
|
|
1385
|
+
);
|
|
1386
|
+
if (writeResult.created) {
|
|
1387
|
+
result.created.push("CONTRIBUTING.md");
|
|
1388
|
+
if (writeResult.backedUp) {
|
|
1389
|
+
result.backedUp.push(writeResult.backedUp);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
return result;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// src/generators/prettier.ts
|
|
1396
|
+
import { join as join14 } from "path";
|
|
1397
|
+
function getPrettierConfig() {
|
|
1398
|
+
return JSON.stringify(
|
|
1399
|
+
{
|
|
1400
|
+
semi: true,
|
|
1401
|
+
singleQuote: true,
|
|
1402
|
+
tabWidth: 2,
|
|
1403
|
+
trailingComma: "es5",
|
|
1404
|
+
printWidth: 100,
|
|
1405
|
+
bracketSpacing: true,
|
|
1406
|
+
arrowParens: "always",
|
|
1407
|
+
endOfLine: "lf"
|
|
1408
|
+
},
|
|
1409
|
+
null,
|
|
1410
|
+
2
|
|
1411
|
+
) + "\n";
|
|
1412
|
+
}
|
|
1413
|
+
function getPrettierIgnore() {
|
|
1414
|
+
return `# Dependencies
|
|
1415
|
+
node_modules/
|
|
1416
|
+
|
|
1417
|
+
# Build output
|
|
1418
|
+
dist/
|
|
1419
|
+
build/
|
|
1420
|
+
.next/
|
|
1421
|
+
out/
|
|
1422
|
+
|
|
1423
|
+
# Coverage
|
|
1424
|
+
coverage/
|
|
1425
|
+
|
|
1426
|
+
# IDE
|
|
1427
|
+
.idea/
|
|
1428
|
+
.vscode/
|
|
1429
|
+
|
|
1430
|
+
# Generated files
|
|
1431
|
+
*.min.js
|
|
1432
|
+
*.min.css
|
|
1433
|
+
package-lock.json
|
|
1434
|
+
pnpm-lock.yaml
|
|
1435
|
+
yarn.lock
|
|
1436
|
+
|
|
1437
|
+
# Other
|
|
1438
|
+
.git/
|
|
1439
|
+
`;
|
|
1440
|
+
}
|
|
1441
|
+
function hasPrettierConfig(targetDir) {
|
|
1442
|
+
const prettierFiles = [
|
|
1443
|
+
".prettierrc",
|
|
1444
|
+
".prettierrc.js",
|
|
1445
|
+
".prettierrc.cjs",
|
|
1446
|
+
".prettierrc.json",
|
|
1447
|
+
".prettierrc.yaml",
|
|
1448
|
+
".prettierrc.yml",
|
|
1449
|
+
".prettierrc.toml",
|
|
1450
|
+
"prettier.config.js",
|
|
1451
|
+
"prettier.config.cjs",
|
|
1452
|
+
"prettier.config.mjs"
|
|
1453
|
+
];
|
|
1454
|
+
return prettierFiles.some((file) => fileExists(join14(targetDir, file)));
|
|
1455
|
+
}
|
|
1456
|
+
async function generatePrettier(targetDir) {
|
|
1457
|
+
const result = {
|
|
1458
|
+
created: [],
|
|
1459
|
+
modified: [],
|
|
1460
|
+
skipped: [],
|
|
1461
|
+
backedUp: []
|
|
1462
|
+
};
|
|
1463
|
+
if (hasPrettierConfig(targetDir)) {
|
|
1464
|
+
result.skipped.push(".prettierrc (already exists)");
|
|
1465
|
+
return result;
|
|
1466
|
+
}
|
|
1467
|
+
const configPath = join14(targetDir, ".prettierrc");
|
|
1468
|
+
const configResult = await writeFileSafe(configPath, getPrettierConfig(), {
|
|
1469
|
+
backup: true
|
|
1470
|
+
});
|
|
1471
|
+
if (configResult.created) {
|
|
1472
|
+
result.created.push(".prettierrc");
|
|
1473
|
+
if (configResult.backedUp) {
|
|
1474
|
+
result.backedUp.push(configResult.backedUp);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
const ignorePath = join14(targetDir, ".prettierignore");
|
|
1478
|
+
const ignoreResult = await writeFileSafe(ignorePath, getPrettierIgnore(), {
|
|
1479
|
+
backup: true,
|
|
1480
|
+
overwrite: false
|
|
1481
|
+
// Don't overwrite existing ignore file
|
|
1482
|
+
});
|
|
1483
|
+
if (ignoreResult.created) {
|
|
1484
|
+
result.created.push(".prettierignore");
|
|
1485
|
+
if (ignoreResult.backedUp) {
|
|
1486
|
+
result.backedUp.push(ignoreResult.backedUp);
|
|
1487
|
+
}
|
|
1488
|
+
} else {
|
|
1489
|
+
result.skipped.push(".prettierignore (already exists)");
|
|
1490
|
+
}
|
|
1491
|
+
return result;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// src/generators/claude-skills.ts
|
|
1495
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1496
|
+
import { readdir, copyFile as copyFile2 } from "fs/promises";
|
|
1497
|
+
import { join as join15, dirname as dirname2 } from "path";
|
|
1498
|
+
import { fileURLToPath } from "url";
|
|
1499
|
+
function getPackageSkillsDir() {
|
|
1500
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
1501
|
+
const packageRoot = join15(dirname2(currentFilePath), "..", "..");
|
|
1502
|
+
return join15(packageRoot, ".claude", "skills");
|
|
1503
|
+
}
|
|
1504
|
+
async function copyDirectory(srcDir, destDir, result, baseDir) {
|
|
1505
|
+
await ensureDir(destDir);
|
|
1506
|
+
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
1507
|
+
for (const entry of entries) {
|
|
1508
|
+
const srcPath = join15(srcDir, entry.name);
|
|
1509
|
+
const destPath = join15(destDir, entry.name);
|
|
1510
|
+
const relativePath = destPath.replace(baseDir + "/", "");
|
|
1511
|
+
if (entry.isDirectory()) {
|
|
1512
|
+
await copyDirectory(srcPath, destPath, result, baseDir);
|
|
1513
|
+
} else {
|
|
1514
|
+
if (existsSync4(destPath)) {
|
|
1515
|
+
const backupPath = await backupFile(destPath);
|
|
1516
|
+
if (backupPath) {
|
|
1517
|
+
result.backedUp.push(relativePath);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
await copyFile2(srcPath, destPath);
|
|
1521
|
+
result.created.push(relativePath);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
async function generateClaudeSkills(targetDir) {
|
|
1526
|
+
const result = {
|
|
1527
|
+
created: [],
|
|
1528
|
+
modified: [],
|
|
1529
|
+
skipped: [],
|
|
1530
|
+
backedUp: []
|
|
1531
|
+
};
|
|
1532
|
+
const packageSkillsDir = getPackageSkillsDir();
|
|
1533
|
+
const targetSkillsDir = join15(targetDir, ".claude", "skills");
|
|
1534
|
+
if (!existsSync4(packageSkillsDir)) {
|
|
1535
|
+
console.warn(
|
|
1536
|
+
"Warning: Skills directory not found in package. Skipping skills generation."
|
|
1537
|
+
);
|
|
1538
|
+
return result;
|
|
1539
|
+
}
|
|
1540
|
+
await ensureDir(join15(targetDir, ".claude"));
|
|
1541
|
+
await copyDirectory(packageSkillsDir, targetSkillsDir, result, targetDir);
|
|
1542
|
+
return result;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// src/generators/eslint.ts
|
|
1546
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1547
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
1548
|
+
import { join as join16 } from "path";
|
|
1549
|
+
|
|
1550
|
+
// src/utils/git.ts
|
|
1551
|
+
import { execa } from "execa";
|
|
1552
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1553
|
+
import { join as join17 } from "path";
|
|
1554
|
+
async function isGitRepo(targetDir = process.cwd()) {
|
|
1555
|
+
if (existsSync6(join17(targetDir, ".git"))) {
|
|
1556
|
+
return true;
|
|
1557
|
+
}
|
|
1558
|
+
try {
|
|
1559
|
+
await execa("git", ["rev-parse", "--git-dir"], { cwd: targetDir });
|
|
1560
|
+
return true;
|
|
1561
|
+
} catch {
|
|
1562
|
+
return false;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
async function isGhCliAvailable() {
|
|
1566
|
+
try {
|
|
1567
|
+
await execa("gh", ["auth", "status"]);
|
|
1568
|
+
return true;
|
|
1569
|
+
} catch {
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
async function getGitHubRepoInfo(targetDir = process.cwd()) {
|
|
1574
|
+
try {
|
|
1575
|
+
const { stdout } = await execa("gh", ["repo", "view", "--json", "owner,name"], {
|
|
1576
|
+
cwd: targetDir
|
|
1577
|
+
});
|
|
1578
|
+
const data = JSON.parse(stdout);
|
|
1579
|
+
return {
|
|
1580
|
+
owner: data.owner.login,
|
|
1581
|
+
repo: data.name
|
|
1582
|
+
};
|
|
1583
|
+
} catch {
|
|
1584
|
+
return null;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// src/commands/init.ts
|
|
1589
|
+
function mergeResults(results) {
|
|
1590
|
+
return results.reduce(
|
|
1591
|
+
(acc, result) => ({
|
|
1592
|
+
created: [...acc.created, ...result.created],
|
|
1593
|
+
modified: [...acc.modified, ...result.modified],
|
|
1594
|
+
skipped: [...acc.skipped, ...result.skipped],
|
|
1595
|
+
backedUp: [...acc.backedUp, ...result.backedUp]
|
|
1596
|
+
}),
|
|
1597
|
+
{ created: [], modified: [], skipped: [], backedUp: [] }
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
async function updateProjectPackageJson(targetDir, _config) {
|
|
1601
|
+
const result = {
|
|
1602
|
+
created: [],
|
|
1603
|
+
modified: [],
|
|
1604
|
+
skipped: [],
|
|
1605
|
+
backedUp: []
|
|
1606
|
+
};
|
|
1607
|
+
try {
|
|
1608
|
+
let pkg = await readPackageJson(targetDir);
|
|
1609
|
+
const scripts = {
|
|
1610
|
+
prepare: "husky",
|
|
1611
|
+
commit: "czg"
|
|
1612
|
+
};
|
|
1613
|
+
pkg = mergeScripts(pkg, scripts, false);
|
|
1614
|
+
pkg = mergeDevDependencies(pkg, RAFTSTACK_DEV_DEPENDENCIES);
|
|
1615
|
+
await writePackageJson(pkg, targetDir);
|
|
1616
|
+
result.modified.push("package.json");
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
result.skipped.push("package.json (error updating)");
|
|
1619
|
+
}
|
|
1620
|
+
return result;
|
|
1621
|
+
}
|
|
1622
|
+
async function runInit(targetDir = process.cwd()) {
|
|
1623
|
+
const isRepo = await isGitRepo(targetDir);
|
|
1624
|
+
if (!isRepo) {
|
|
1625
|
+
p2.log.warn(
|
|
1626
|
+
pc2.yellow(
|
|
1627
|
+
"This directory is not a git repository. Some features may not work correctly."
|
|
1628
|
+
)
|
|
1629
|
+
);
|
|
1630
|
+
const proceed = await p2.confirm({
|
|
1631
|
+
message: "Continue anyway?",
|
|
1632
|
+
initialValue: false
|
|
1633
|
+
});
|
|
1634
|
+
if (p2.isCancel(proceed) || !proceed) {
|
|
1635
|
+
p2.cancel("Setup cancelled.");
|
|
1636
|
+
process.exit(0);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
const config = await collectConfig(targetDir);
|
|
1640
|
+
if (!config) {
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
const spinner3 = p2.spinner();
|
|
1644
|
+
spinner3.start("Generating configuration files...");
|
|
1645
|
+
const results = [];
|
|
1646
|
+
try {
|
|
1647
|
+
results.push(await generateHuskyHooks(targetDir, config.projectType));
|
|
1648
|
+
results.push(await generateCommitlint(targetDir, config.asanaBaseUrl));
|
|
1649
|
+
results.push(await generateCzGit(targetDir, config.asanaBaseUrl));
|
|
1650
|
+
results.push(
|
|
1651
|
+
await generateLintStaged(
|
|
1652
|
+
targetDir,
|
|
1653
|
+
config.projectType,
|
|
1654
|
+
config.usesEslint,
|
|
1655
|
+
config.usesPrettier,
|
|
1656
|
+
config.usesTypeScript
|
|
1657
|
+
)
|
|
1658
|
+
);
|
|
1659
|
+
results.push(await generateBranchValidation(targetDir));
|
|
1660
|
+
if (!config.usesPrettier) {
|
|
1661
|
+
results.push(await generatePrettier(targetDir));
|
|
1662
|
+
}
|
|
1663
|
+
results.push(await generatePRTemplate(targetDir, !!config.asanaBaseUrl));
|
|
1664
|
+
results.push(
|
|
1665
|
+
await generateGitHubWorkflows(
|
|
1666
|
+
targetDir,
|
|
1667
|
+
config.projectType,
|
|
1668
|
+
config.usesTypeScript,
|
|
1669
|
+
config.usesEslint
|
|
1670
|
+
)
|
|
1671
|
+
);
|
|
1672
|
+
results.push(await generateCodeowners(targetDir, config.codeowners));
|
|
1673
|
+
results.push(await generateAIReview(targetDir, config.aiReviewTool));
|
|
1674
|
+
results.push(await generateBranchProtectionDocs(targetDir));
|
|
1675
|
+
results.push(await generateContributing(targetDir, !!config.asanaBaseUrl));
|
|
1676
|
+
results.push(await generateClaudeSkills(targetDir));
|
|
1677
|
+
results.push(await updateProjectPackageJson(targetDir, config));
|
|
1678
|
+
spinner3.stop("Configuration files generated!");
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
spinner3.stop("Error generating files");
|
|
1681
|
+
p2.log.error(
|
|
1682
|
+
pc2.red(
|
|
1683
|
+
`Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1684
|
+
)
|
|
1685
|
+
);
|
|
1686
|
+
process.exit(1);
|
|
1687
|
+
}
|
|
1688
|
+
const finalResult = mergeResults(results);
|
|
1689
|
+
console.log();
|
|
1690
|
+
if (finalResult.created.length > 0) {
|
|
1691
|
+
p2.log.success(pc2.green("Created files:"));
|
|
1692
|
+
for (const file of finalResult.created) {
|
|
1693
|
+
console.log(` ${pc2.dim("+")} ${file}`);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
if (finalResult.modified.length > 0) {
|
|
1697
|
+
console.log();
|
|
1698
|
+
p2.log.info(pc2.blue("Modified files:"));
|
|
1699
|
+
for (const file of finalResult.modified) {
|
|
1700
|
+
console.log(` ${pc2.dim("~")} ${file}`);
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (finalResult.skipped.length > 0) {
|
|
1704
|
+
console.log();
|
|
1705
|
+
p2.log.warn(pc2.yellow("Skipped (already exist):"));
|
|
1706
|
+
for (const file of finalResult.skipped) {
|
|
1707
|
+
console.log(` ${pc2.dim("-")} ${file}`);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (finalResult.backedUp.length > 0) {
|
|
1711
|
+
console.log();
|
|
1712
|
+
p2.log.info(pc2.dim("Backed up files:"));
|
|
1713
|
+
for (const file of finalResult.backedUp) {
|
|
1714
|
+
console.log(` ${pc2.dim("\u2192")} ${file}`);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
console.log();
|
|
1718
|
+
p2.note(
|
|
1719
|
+
[
|
|
1720
|
+
`${pc2.cyan("1.")} Run ${pc2.yellow("pnpm install")} to install dependencies`,
|
|
1721
|
+
`${pc2.cyan("2.")} Review the generated configuration files`,
|
|
1722
|
+
`${pc2.cyan("3.")} Use ${pc2.yellow("pnpm commit")} for interactive commits`,
|
|
1723
|
+
`${pc2.cyan("4.")} Set up branch protection rules (see .github/BRANCH_PROTECTION_SETUP.md)`
|
|
1724
|
+
].join("\n"),
|
|
1725
|
+
"Next Steps"
|
|
1726
|
+
);
|
|
1727
|
+
p2.outro(pc2.green("RaftStack setup complete! Happy coding! \u{1F680}"));
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/commands/setup-protection.ts
|
|
1731
|
+
import * as p3 from "@clack/prompts";
|
|
1732
|
+
import pc3 from "picocolors";
|
|
1733
|
+
import { execa as execa2 } from "execa";
|
|
1734
|
+
function getDefaultSettings(branch) {
|
|
1735
|
+
return {
|
|
1736
|
+
branch,
|
|
1737
|
+
requiredReviews: 1,
|
|
1738
|
+
dismissStaleReviews: true,
|
|
1739
|
+
requireCodeOwners: true,
|
|
1740
|
+
requireStatusChecks: true,
|
|
1741
|
+
statusChecks: ["check"],
|
|
1742
|
+
requireConversationResolution: true,
|
|
1743
|
+
restrictPushes: false,
|
|
1744
|
+
blockForcePushes: true,
|
|
1745
|
+
blockDeletions: true
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
function getMergeStrategySettings(strategy) {
|
|
1749
|
+
switch (strategy) {
|
|
1750
|
+
case "rebase":
|
|
1751
|
+
return {
|
|
1752
|
+
allowMergeCommit: false,
|
|
1753
|
+
allowSquashMerge: false,
|
|
1754
|
+
allowRebaseMerge: true,
|
|
1755
|
+
deleteBranchOnMerge: true
|
|
1756
|
+
};
|
|
1757
|
+
case "squash":
|
|
1758
|
+
return {
|
|
1759
|
+
allowMergeCommit: false,
|
|
1760
|
+
allowSquashMerge: true,
|
|
1761
|
+
allowRebaseMerge: false,
|
|
1762
|
+
deleteBranchOnMerge: true
|
|
1763
|
+
};
|
|
1764
|
+
case "merge":
|
|
1765
|
+
return {
|
|
1766
|
+
allowMergeCommit: true,
|
|
1767
|
+
allowSquashMerge: false,
|
|
1768
|
+
allowRebaseMerge: false,
|
|
1769
|
+
deleteBranchOnMerge: true
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
async function applyBranchProtection(owner, repo, settings) {
|
|
1774
|
+
const args = [
|
|
1775
|
+
"api",
|
|
1776
|
+
"-X",
|
|
1777
|
+
"PUT",
|
|
1778
|
+
`/repos/${owner}/${repo}/branches/${settings.branch}/protection`,
|
|
1779
|
+
"-f",
|
|
1780
|
+
`required_pull_request_reviews[required_approving_review_count]=${settings.requiredReviews}`,
|
|
1781
|
+
"-f",
|
|
1782
|
+
`required_pull_request_reviews[dismiss_stale_reviews]=${settings.dismissStaleReviews}`,
|
|
1783
|
+
"-f",
|
|
1784
|
+
`required_pull_request_reviews[require_code_owner_reviews]=${settings.requireCodeOwners}`,
|
|
1785
|
+
"-f",
|
|
1786
|
+
`required_status_checks[strict]=true`,
|
|
1787
|
+
"-f",
|
|
1788
|
+
`enforce_admins=true`,
|
|
1789
|
+
"-f",
|
|
1790
|
+
`allow_force_pushes=${!settings.blockForcePushes}`,
|
|
1791
|
+
"-f",
|
|
1792
|
+
`allow_deletions=${!settings.blockDeletions}`,
|
|
1793
|
+
"-f",
|
|
1794
|
+
`required_conversation_resolution=${settings.requireConversationResolution}`
|
|
1795
|
+
];
|
|
1796
|
+
if (settings.requireStatusChecks && settings.statusChecks.length > 0) {
|
|
1797
|
+
for (const check of settings.statusChecks) {
|
|
1798
|
+
args.push("-f", `required_status_checks[contexts][]=${check}`);
|
|
1799
|
+
}
|
|
1800
|
+
} else {
|
|
1801
|
+
args.push("-F", "required_status_checks=null");
|
|
1802
|
+
}
|
|
1803
|
+
args.push("-F", "restrictions=null");
|
|
1804
|
+
await execa2("gh", args);
|
|
1805
|
+
}
|
|
1806
|
+
async function applyMergeStrategy(owner, repo, settings) {
|
|
1807
|
+
const args = [
|
|
1808
|
+
"api",
|
|
1809
|
+
"-X",
|
|
1810
|
+
"PATCH",
|
|
1811
|
+
`/repos/${owner}/${repo}`,
|
|
1812
|
+
"-f",
|
|
1813
|
+
`allow_merge_commit=${settings.allowMergeCommit}`,
|
|
1814
|
+
"-f",
|
|
1815
|
+
`allow_squash_merge=${settings.allowSquashMerge}`,
|
|
1816
|
+
"-f",
|
|
1817
|
+
`allow_rebase_merge=${settings.allowRebaseMerge}`,
|
|
1818
|
+
"-f",
|
|
1819
|
+
`delete_branch_on_merge=${settings.deleteBranchOnMerge}`
|
|
1820
|
+
];
|
|
1821
|
+
await execa2("gh", args);
|
|
1822
|
+
}
|
|
1823
|
+
async function runSetupProtection(targetDir = process.cwd()) {
|
|
1824
|
+
console.log();
|
|
1825
|
+
p3.intro(pc3.bgCyan(pc3.black(" Branch Protection Setup ")));
|
|
1826
|
+
const spinner3 = p3.spinner();
|
|
1827
|
+
spinner3.start("Checking GitHub CLI...");
|
|
1828
|
+
const ghAvailable = await isGhCliAvailable();
|
|
1829
|
+
if (!ghAvailable) {
|
|
1830
|
+
spinner3.stop("GitHub CLI not found or not authenticated");
|
|
1831
|
+
console.log();
|
|
1832
|
+
p3.log.error(pc3.red("The GitHub CLI (gh) is required for this command."));
|
|
1833
|
+
p3.log.info("Install it from: https://cli.github.com/");
|
|
1834
|
+
p3.log.info("Then run: gh auth login");
|
|
1835
|
+
console.log();
|
|
1836
|
+
p3.log.info(
|
|
1837
|
+
pc3.dim(
|
|
1838
|
+
"Alternatively, see .github/BRANCH_PROTECTION_SETUP.md for manual instructions."
|
|
1839
|
+
)
|
|
1840
|
+
);
|
|
1841
|
+
process.exit(1);
|
|
1842
|
+
}
|
|
1843
|
+
spinner3.stop("GitHub CLI ready");
|
|
1844
|
+
spinner3.start("Getting repository info...");
|
|
1845
|
+
const repoInfo = await getGitHubRepoInfo(targetDir);
|
|
1846
|
+
if (!repoInfo) {
|
|
1847
|
+
spinner3.stop("Could not determine repository");
|
|
1848
|
+
p3.log.error(
|
|
1849
|
+
pc3.red("Could not determine the GitHub repository for this directory.")
|
|
1850
|
+
);
|
|
1851
|
+
p3.log.info("Make sure you're in a git repository with a GitHub remote.");
|
|
1852
|
+
process.exit(1);
|
|
1853
|
+
}
|
|
1854
|
+
spinner3.stop(`Repository: ${pc3.cyan(`${repoInfo.owner}/${repoInfo.repo}`)}`);
|
|
1855
|
+
const branches = await p3.multiselect({
|
|
1856
|
+
message: "Which branches need protection?",
|
|
1857
|
+
options: [
|
|
1858
|
+
{ value: "main", label: "main", hint: "recommended" },
|
|
1859
|
+
{ value: "master", label: "master", hint: "legacy default" },
|
|
1860
|
+
{ value: "staging", label: "staging", hint: "staging environment" },
|
|
1861
|
+
{ value: "production", label: "production", hint: "production environment" },
|
|
1862
|
+
{ value: "development", label: "development", hint: "development branch" },
|
|
1863
|
+
{ value: "develop", label: "develop", hint: "alternative dev branch" }
|
|
1864
|
+
],
|
|
1865
|
+
required: true,
|
|
1866
|
+
initialValues: ["main"]
|
|
1867
|
+
});
|
|
1868
|
+
if (p3.isCancel(branches)) {
|
|
1869
|
+
p3.cancel("Setup cancelled.");
|
|
1870
|
+
process.exit(0);
|
|
1871
|
+
}
|
|
1872
|
+
const mergeStrategy = await p3.select({
|
|
1873
|
+
message: "Default merge strategy for PRs?",
|
|
1874
|
+
options: [
|
|
1875
|
+
{
|
|
1876
|
+
value: "rebase",
|
|
1877
|
+
label: "Rebase merge",
|
|
1878
|
+
hint: "recommended - clean linear history"
|
|
1879
|
+
},
|
|
1880
|
+
{
|
|
1881
|
+
value: "squash",
|
|
1882
|
+
label: "Squash merge",
|
|
1883
|
+
hint: "single commit per PR"
|
|
1884
|
+
},
|
|
1885
|
+
{
|
|
1886
|
+
value: "merge",
|
|
1887
|
+
label: "Merge commit",
|
|
1888
|
+
hint: "preserve all commits with merge commit"
|
|
1889
|
+
}
|
|
1890
|
+
],
|
|
1891
|
+
initialValue: "rebase"
|
|
1892
|
+
});
|
|
1893
|
+
if (p3.isCancel(mergeStrategy)) {
|
|
1894
|
+
p3.cancel("Setup cancelled.");
|
|
1895
|
+
process.exit(0);
|
|
1896
|
+
}
|
|
1897
|
+
const reviews = await p3.text({
|
|
1898
|
+
message: "How many approving reviews are required?",
|
|
1899
|
+
placeholder: "1",
|
|
1900
|
+
initialValue: "1",
|
|
1901
|
+
validate: (value) => {
|
|
1902
|
+
const num = parseInt(value, 10);
|
|
1903
|
+
if (isNaN(num) || num < 0 || num > 6) {
|
|
1904
|
+
return "Must be a number between 0 and 6";
|
|
1905
|
+
}
|
|
1906
|
+
return void 0;
|
|
1907
|
+
}
|
|
1908
|
+
});
|
|
1909
|
+
if (p3.isCancel(reviews)) {
|
|
1910
|
+
p3.cancel("Setup cancelled.");
|
|
1911
|
+
process.exit(0);
|
|
1912
|
+
}
|
|
1913
|
+
const requiredReviews = parseInt(reviews, 10);
|
|
1914
|
+
const mergeStrategyLabels = {
|
|
1915
|
+
rebase: "Rebase merge",
|
|
1916
|
+
squash: "Squash merge",
|
|
1917
|
+
merge: "Merge commit"
|
|
1918
|
+
};
|
|
1919
|
+
console.log();
|
|
1920
|
+
p3.note(
|
|
1921
|
+
[
|
|
1922
|
+
`${pc3.cyan("Repository:")} ${repoInfo.owner}/${repoInfo.repo}`,
|
|
1923
|
+
`${pc3.cyan("Protected branches:")} ${branches.join(", ")}`,
|
|
1924
|
+
`${pc3.cyan("Merge strategy:")} ${mergeStrategyLabels[mergeStrategy]}`,
|
|
1925
|
+
`${pc3.cyan("Required reviews:")} ${requiredReviews}`,
|
|
1926
|
+
`${pc3.cyan("Dismiss stale reviews:")} Yes`,
|
|
1927
|
+
`${pc3.cyan("Require code owners:")} Yes`,
|
|
1928
|
+
`${pc3.cyan("Require status checks:")} Yes`,
|
|
1929
|
+
`${pc3.cyan("Block force pushes:")} Yes`,
|
|
1930
|
+
`${pc3.cyan("Block deletions:")} Yes`,
|
|
1931
|
+
`${pc3.cyan("Delete branch on merge:")} Yes`
|
|
1932
|
+
].join("\n"),
|
|
1933
|
+
"Branch Protection Settings"
|
|
1934
|
+
);
|
|
1935
|
+
const confirmed = await p3.confirm({
|
|
1936
|
+
message: "Apply these branch protection rules?",
|
|
1937
|
+
initialValue: true
|
|
1938
|
+
});
|
|
1939
|
+
if (p3.isCancel(confirmed) || !confirmed) {
|
|
1940
|
+
p3.cancel("Setup cancelled.");
|
|
1941
|
+
process.exit(0);
|
|
1942
|
+
}
|
|
1943
|
+
spinner3.start("Configuring merge strategy...");
|
|
1944
|
+
try {
|
|
1945
|
+
const repoSettings = getMergeStrategySettings(mergeStrategy);
|
|
1946
|
+
await applyMergeStrategy(repoInfo.owner, repoInfo.repo, repoSettings);
|
|
1947
|
+
spinner3.stop("Merge strategy configured!");
|
|
1948
|
+
} catch (error) {
|
|
1949
|
+
spinner3.stop("Failed to configure merge strategy");
|
|
1950
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
1951
|
+
p3.log.warn(pc3.yellow(`Warning: Could not set merge strategy: ${errorMsg}`));
|
|
1952
|
+
p3.log.info(pc3.dim("Continuing with branch protection..."));
|
|
1953
|
+
}
|
|
1954
|
+
const protectedBranches = [];
|
|
1955
|
+
const failedBranches = [];
|
|
1956
|
+
for (const branch of branches) {
|
|
1957
|
+
spinner3.start(`Protecting branch: ${branch}...`);
|
|
1958
|
+
try {
|
|
1959
|
+
const settings = getDefaultSettings(branch);
|
|
1960
|
+
settings.requiredReviews = requiredReviews;
|
|
1961
|
+
await applyBranchProtection(repoInfo.owner, repoInfo.repo, settings);
|
|
1962
|
+
protectedBranches.push(branch);
|
|
1963
|
+
spinner3.stop(`Protected: ${pc3.green(branch)}`);
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
failedBranches.push(branch);
|
|
1966
|
+
spinner3.stop(`Failed: ${pc3.red(branch)}`);
|
|
1967
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
1968
|
+
p3.log.warn(
|
|
1969
|
+
pc3.yellow(
|
|
1970
|
+
`Could not protect ${branch}: ${errorMsg.includes("Branch not found") ? "branch does not exist" : errorMsg}`
|
|
1971
|
+
)
|
|
1972
|
+
);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
console.log();
|
|
1976
|
+
if (protectedBranches.length > 0) {
|
|
1977
|
+
p3.log.success(
|
|
1978
|
+
pc3.green(`Branch protection enabled for: ${pc3.cyan(protectedBranches.join(", "))}`)
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
if (failedBranches.length > 0) {
|
|
1982
|
+
p3.log.warn(
|
|
1983
|
+
pc3.yellow(
|
|
1984
|
+
`Could not protect: ${pc3.red(failedBranches.join(", "))} (branches may not exist yet)`
|
|
1985
|
+
)
|
|
1986
|
+
);
|
|
1987
|
+
p3.log.info(pc3.dim("Create these branches first, then run this command again."));
|
|
1988
|
+
}
|
|
1989
|
+
if (protectedBranches.length > 0) {
|
|
1990
|
+
p3.outro(pc3.green("Setup complete!"));
|
|
1991
|
+
} else {
|
|
1992
|
+
p3.outro(pc3.yellow("No branches were protected."));
|
|
1993
|
+
process.exit(1);
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// src/cli.ts
|
|
1998
|
+
var program = new Command();
|
|
1999
|
+
program.name("raftstack").description(
|
|
2000
|
+
"CLI tool for setting up Git hooks, commit conventions, and GitHub integration"
|
|
2001
|
+
).version("1.0.0");
|
|
2002
|
+
program.command("init").description("Initialize RaftStack configuration in your project").action(async () => {
|
|
2003
|
+
await runInit(process.cwd());
|
|
2004
|
+
});
|
|
2005
|
+
program.command("setup-protection").description("Configure GitHub branch protection rules via API").action(async () => {
|
|
2006
|
+
await runSetupProtection(process.cwd());
|
|
2007
|
+
});
|
|
2008
|
+
program.parse();
|
|
2009
|
+
//# sourceMappingURL=cli.js.map
|