@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/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