@raftlabs/raftstack 1.9.3 → 1.10.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 CHANGED
@@ -215,6 +215,72 @@ async function hasPrettier(targetDir = process.cwd()) {
215
215
  }
216
216
  return false;
217
217
  }
218
+ async function hasReact(targetDir = process.cwd()) {
219
+ try {
220
+ const pkgPath = join2(targetDir, "package.json");
221
+ if (existsSync2(pkgPath)) {
222
+ const content = await readFile(pkgPath, "utf-8");
223
+ const pkg = JSON.parse(content);
224
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
225
+ return "react" in deps || "react-dom" in deps;
226
+ }
227
+ } catch {
228
+ }
229
+ return false;
230
+ }
231
+ async function detectTestFramework(targetDir = process.cwd()) {
232
+ const vitestConfigFiles = [
233
+ "vitest.config.ts",
234
+ "vitest.config.js",
235
+ "vitest.config.mjs",
236
+ "vitest.config.cjs"
237
+ ];
238
+ for (const file of vitestConfigFiles) {
239
+ if (existsSync2(join2(targetDir, file))) {
240
+ return "vitest";
241
+ }
242
+ }
243
+ const jestConfigFiles = [
244
+ "jest.config.ts",
245
+ "jest.config.js",
246
+ "jest.config.mjs",
247
+ "jest.config.cjs",
248
+ "jest.config.json"
249
+ ];
250
+ for (const file of jestConfigFiles) {
251
+ if (existsSync2(join2(targetDir, file))) {
252
+ return "jest";
253
+ }
254
+ }
255
+ try {
256
+ const pkgPath = join2(targetDir, "package.json");
257
+ if (existsSync2(pkgPath)) {
258
+ const content = await readFile(pkgPath, "utf-8");
259
+ const pkg = JSON.parse(content);
260
+ if (pkg.jest) {
261
+ return "jest";
262
+ }
263
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
264
+ if ("vitest" in deps) {
265
+ return "vitest";
266
+ }
267
+ if ("jest" in deps || "@jest/core" in deps) {
268
+ return "jest";
269
+ }
270
+ if (pkg.scripts?.test) {
271
+ const testScript = pkg.scripts.test;
272
+ if (testScript.includes("vitest")) {
273
+ return "vitest";
274
+ }
275
+ if (testScript.includes("jest")) {
276
+ return "jest";
277
+ }
278
+ }
279
+ }
280
+ } catch {
281
+ }
282
+ return null;
283
+ }
218
284
  function getProjectTypeDescription(type) {
219
285
  switch (type) {
220
286
  case "nx":
@@ -386,6 +452,34 @@ async function promptPackageManager(targetDir) {
386
452
  }
387
453
  return getPackageManagerInfo(selected);
388
454
  }
455
+ async function promptTestFramework(targetDir) {
456
+ const detected = await detectTestFramework(targetDir);
457
+ if (detected) {
458
+ p.log.info(`Detected ${pc.cyan(detected)} test framework`);
459
+ return detected;
460
+ }
461
+ p.log.warn("No test framework detected");
462
+ const selected = await p.select({
463
+ message: "Select a test framework:",
464
+ options: [
465
+ {
466
+ value: "vitest",
467
+ label: "Vitest",
468
+ hint: "Recommended for modern projects (fast, ESM-native)"
469
+ },
470
+ {
471
+ value: "jest",
472
+ label: "Jest",
473
+ hint: "Mature ecosystem, widely used"
474
+ }
475
+ ]
476
+ });
477
+ if (p.isCancel(selected)) {
478
+ p.cancel("Setup cancelled.");
479
+ process.exit(0);
480
+ }
481
+ return selected;
482
+ }
389
483
  async function promptConfirmation(config) {
390
484
  console.log();
391
485
  p.note(
@@ -395,6 +489,7 @@ async function promptConfirmation(config) {
395
489
  `${pc.cyan("TypeScript:")} ${config.usesTypeScript ? "Yes" : "No"}`,
396
490
  `${pc.cyan("ESLint:")} ${config.usesEslint ? "Yes" : "No"}`,
397
491
  `${pc.cyan("Prettier:")} ${config.usesPrettier ? "Yes" : "No"}`,
492
+ `${pc.cyan("Test Framework:")} ${config.testFramework || "None"}`,
398
493
  `${pc.cyan("Asana Integration:")} ${config.asanaBaseUrl ? "Yes" : "No"}`,
399
494
  `${pc.cyan("AI Review:")} ${config.aiReviewTool === "none" ? "None" : config.aiReviewTool}`,
400
495
  `${pc.cyan("CODEOWNERS:")} ${config.codeowners.length > 0 ? config.codeowners.join(", ") : "None"}`
@@ -419,6 +514,16 @@ async function collectConfig(targetDir = process.cwd()) {
419
514
  const usesTypeScript = await hasTypeScript(targetDir);
420
515
  const usesEslint = await hasEslint(targetDir);
421
516
  const usesPrettier = await hasPrettier(targetDir);
517
+ const testFramework = await promptTestFramework(targetDir);
518
+ if (testFramework) {
519
+ p.note(
520
+ `${pc.green("\u2713")} TDD enforcement enabled:
521
+ \u2022 Tests run on commit (related tests)
522
+ \u2022 Full test suite + 80% coverage required on push
523
+ \u2022 Use ${pc.cyan("--no-verify")} to bypass (not recommended)`,
524
+ "Test-Driven Development"
525
+ );
526
+ }
422
527
  const asanaBaseUrl = await promptAsanaConfig();
423
528
  const aiReviewTool = await promptAIReview();
424
529
  const codeowners = await promptCodeowners();
@@ -430,7 +535,8 @@ async function collectConfig(targetDir = process.cwd()) {
430
535
  codeowners,
431
536
  usesTypeScript,
432
537
  usesEslint,
433
- usesPrettier
538
+ usesPrettier,
539
+ testFramework
434
540
  };
435
541
  const confirmed = await promptConfirmation(config);
436
542
  if (!confirmed) {
@@ -508,17 +614,30 @@ function getBuildCommand(projectType) {
508
614
  return "pnpm build";
509
615
  }
510
616
  }
511
- function getPrePushHook(projectType) {
617
+ function getPrePushHook(projectType, testFramework) {
512
618
  const buildCommand = getBuildCommand(projectType);
513
- return `# Validate branch naming convention
619
+ let hook = `# Validate branch naming convention
514
620
  validate-branch-name
515
-
621
+ `;
622
+ if (testFramework) {
623
+ hook += `
624
+ # Run full test suite with coverage (STRICT - blocks on failure)
625
+ echo "\u{1F9EA} Running full test suite with coverage..."
626
+ npm run test:coverage || {
627
+ echo "\u274C Tests failed or coverage below 80%. Push blocked."
628
+ echo "\u{1F4A1} Fix failing tests or use 'git push --no-verify' to bypass (not recommended)."
629
+ exit 1
630
+ }
631
+ `;
632
+ }
633
+ hook += `
516
634
  # Build all packages - push only succeeds if builds pass
517
635
  echo "\u{1F528} Building..."
518
636
  ${buildCommand}
519
637
  `;
638
+ return hook;
520
639
  }
521
- async function generateHuskyHooks(targetDir, projectType, _pm) {
640
+ async function generateHuskyHooks(targetDir, projectType, _pm, testFramework) {
522
641
  const result = {
523
642
  created: [],
524
643
  modified: [],
@@ -552,10 +671,14 @@ async function generateHuskyHooks(targetDir, projectType, _pm) {
552
671
  }
553
672
  }
554
673
  const prePushPath = join4(huskyDir, "pre-push");
555
- const prePushResult = await writeFileSafe(prePushPath, getPrePushHook(projectType), {
556
- executable: true,
557
- backup: true
558
- });
674
+ const prePushResult = await writeFileSafe(
675
+ prePushPath,
676
+ getPrePushHook(projectType, testFramework),
677
+ {
678
+ executable: true,
679
+ backup: true
680
+ }
681
+ );
559
682
  if (prePushResult.created) {
560
683
  result.created.push(".husky/pre-push");
561
684
  if (prePushResult.backedUp) {
@@ -727,7 +850,7 @@ async function generateCzGit(targetDir, _asanaBaseUrl) {
727
850
  }
728
851
 
729
852
  // src/generators/lint-staged.ts
730
- function getLintStagedConfig(usesEslint, usesPrettier, usesTypeScript) {
853
+ function getLintStagedConfig(usesEslint, usesPrettier, usesTypeScript, testFramework) {
731
854
  const config = {};
732
855
  const codePatterns = [];
733
856
  if (usesTypeScript) {
@@ -741,6 +864,13 @@ function getLintStagedConfig(usesEslint, usesPrettier, usesTypeScript) {
741
864
  if (usesPrettier) {
742
865
  codeCommands.push("prettier --write");
743
866
  }
867
+ if (testFramework) {
868
+ if (testFramework === "vitest") {
869
+ codeCommands.push("vitest related --run --passWithNoTests");
870
+ } else {
871
+ codeCommands.push("jest --bail --findRelatedTests --passWithNoTests");
872
+ }
873
+ }
744
874
  if (codeCommands.length > 0) {
745
875
  const pattern = `*.{${codePatterns.join(",")}}`;
746
876
  config[pattern] = codeCommands;
@@ -933,11 +1063,18 @@ ${asanaSection}## Changes Made
933
1063
  <!-- List the specific changes made in this PR -->
934
1064
  -
935
1065
 
1066
+ ## Development Process
1067
+ <!-- Mark completed items with an "x" -->
1068
+ - [ ] Tests written BEFORE implementation (TDD)
1069
+ - [ ] All tests pass locally (\`npm run test\`)
1070
+ - [ ] Coverage maintained/improved (\`npm run test:coverage\`)
1071
+
936
1072
  ## Testing
937
1073
  <!-- Describe how you tested your changes -->
938
1074
  - [ ] Unit tests added/updated
939
1075
  - [ ] Integration tests added/updated
940
1076
  - [ ] Manual testing performed
1077
+ - [ ] Coverage: [X]% (target: 80%+)
941
1078
 
942
1079
  ## Screenshots (if applicable)
943
1080
  <!-- Add screenshots to help explain your changes -->
@@ -980,7 +1117,7 @@ async function generatePRTemplate(targetDir, hasAsana) {
980
1117
 
981
1118
  // src/generators/github-workflows.ts
982
1119
  import { join as join9 } from "path";
983
- function getPRChecksWorkflow(projectType, usesTypeScript, usesEslint, pm) {
1120
+ function getPRChecksWorkflow(projectType, usesTypeScript, usesEslint, pm, testFramework) {
984
1121
  const steps = [];
985
1122
  steps.push(` - name: Checkout
986
1123
  uses: actions/checkout@v4`);
@@ -1022,18 +1159,38 @@ function getPRChecksWorkflow(projectType, usesTypeScript, usesEslint, pm) {
1022
1159
  - name: Build
1023
1160
  run: ${pm.run} build`);
1024
1161
  }
1025
- if (projectType === "nx") {
1162
+ if (testFramework) {
1163
+ if (projectType === "nx") {
1164
+ steps.push(`
1165
+ - name: Test with Coverage
1166
+ run: ${pm.run} nx affected --target=test --parallel=3 --coverage`);
1167
+ } else if (projectType === "turbo") {
1168
+ steps.push(`
1169
+ - name: Test with Coverage
1170
+ run: ${pm.run} turbo test:coverage`);
1171
+ } else {
1172
+ steps.push(`
1173
+ - name: Test with Coverage
1174
+ run: ${pm.run} test:coverage`);
1175
+ }
1026
1176
  steps.push(`
1177
+ - name: Check Coverage Threshold
1178
+ run: |
1179
+ echo "\u2705 Coverage check passed (80% threshold enforced by test config)"`);
1180
+ } else {
1181
+ if (projectType === "nx") {
1182
+ steps.push(`
1027
1183
  - name: Test
1028
1184
  run: ${pm.run} nx affected --target=test --parallel=3`);
1029
- } else if (projectType === "turbo") {
1030
- steps.push(`
1185
+ } else if (projectType === "turbo") {
1186
+ steps.push(`
1031
1187
  - name: Test
1032
1188
  run: ${pm.run} turbo test`);
1033
- } else {
1034
- steps.push(`
1189
+ } else {
1190
+ steps.push(`
1035
1191
  - name: Test
1036
1192
  run: ${pm.run} test`);
1193
+ }
1037
1194
  }
1038
1195
  return `name: PR Checks
1039
1196
 
@@ -1053,7 +1210,7 @@ jobs:
1053
1210
  ${steps.join("\n")}
1054
1211
  `;
1055
1212
  }
1056
- async function generateGitHubWorkflows(targetDir, projectType, usesTypeScript, usesEslint, pm) {
1213
+ async function generateGitHubWorkflows(targetDir, projectType, usesTypeScript, usesEslint, pm, testFramework) {
1057
1214
  const result = {
1058
1215
  created: [],
1059
1216
  modified: [],
@@ -1065,7 +1222,13 @@ async function generateGitHubWorkflows(targetDir, projectType, usesTypeScript, u
1065
1222
  const prChecksPath = join9(workflowsDir, "pr-checks.yml");
1066
1223
  const writeResult = await writeFileSafe(
1067
1224
  prChecksPath,
1068
- getPRChecksWorkflow(projectType, usesTypeScript, usesEslint, pm),
1225
+ getPRChecksWorkflow(
1226
+ projectType,
1227
+ usesTypeScript,
1228
+ usesEslint,
1229
+ pm,
1230
+ testFramework
1231
+ ),
1069
1232
  { backup: true }
1070
1233
  );
1071
1234
  if (writeResult.created) {
@@ -1444,14 +1607,53 @@ Includes user profile sync and token refresh.
1444
1607
  Closes #123
1445
1608
  \`\`\`
1446
1609
  ${asanaSection}
1610
+ ## Test-Driven Development (TDD)
1611
+
1612
+ We enforce TDD to ensure code quality and maintainability. **Write tests before implementation.**
1613
+
1614
+ ### TDD Workflow
1615
+
1616
+ 1. **Write a failing test** - Define expected behavior
1617
+ 2. **Write minimal code to pass** - Implement just enough to make it green
1618
+ 3. **Refactor** - Improve code while tests stay green
1619
+
1620
+ ### Running Tests
1621
+
1622
+ \`\`\`bash
1623
+ # Watch mode (during development)
1624
+ ${pm.run} test:watch
1625
+
1626
+ # Run all tests
1627
+ ${pm.run} test
1628
+
1629
+ # Check coverage
1630
+ ${pm.run} test:coverage
1631
+ \`\`\`
1632
+
1633
+ ### Coverage Requirements
1634
+
1635
+ - **Minimum:** 80% coverage (lines, functions, branches, statements)
1636
+ - **Critical paths:** Aim for 100% coverage
1637
+ - **New features:** Must include tests before merging
1638
+
1639
+ ### Git Hooks
1640
+
1641
+ Tests run automatically:
1642
+ - **Pre-commit:** Related tests for changed files
1643
+ - **Pre-push:** Full test suite with coverage check
1644
+
1645
+ To bypass (not recommended): \`git push --no-verify\`
1646
+
1447
1647
  ## Pull Request Process
1448
1648
 
1449
1649
  1. Ensure your branch is up to date with \`main\`/\`master\`
1450
- 2. Run tests and linting locally
1451
- 3. Create a pull request using the provided template
1452
- 4. Request review from code owners
1453
- 5. Address any feedback
1454
- 6. Merge once approved and all checks pass
1650
+ 2. **Write tests first (TDD)**
1651
+ 3. Run tests and linting locally
1652
+ 4. **Verify coverage** (\`${pm.run} test:coverage\`)
1653
+ 5. Create a pull request using the provided template
1654
+ 6. Request review from code owners
1655
+ 7. Address any feedback
1656
+ 8. Merge once approved and all checks pass
1455
1657
 
1456
1658
  ### PR Size Guidelines
1457
1659
 
@@ -1475,9 +1677,11 @@ If your PR is large:
1475
1677
 
1476
1678
  Before committing, the following checks run automatically:
1477
1679
 
1680
+ - **Testing**: Related tests run on commit, full suite on push
1478
1681
  - **Linting**: ESLint checks for code quality
1479
1682
  - **Formatting**: Prettier ensures consistent style
1480
1683
  - **Type checking**: TypeScript validates types
1684
+ - **Coverage**: 80% minimum enforced on push
1481
1685
  - **Commit messages**: Commitlint validates format
1482
1686
  - **Branch names**: validate-branch-name checks naming
1483
1687
 
@@ -1708,7 +1912,8 @@ import { join as join17, dirname as dirname4 } from "path";
1708
1912
  import { fileURLToPath as fileURLToPath3 } from "url";
1709
1913
  function getPackageConfigDir() {
1710
1914
  const currentFilePath = fileURLToPath3(import.meta.url);
1711
- const packageRoot = join17(dirname4(currentFilePath), "..");
1915
+ const currentDir = dirname4(currentFilePath);
1916
+ const packageRoot = currentDir.endsWith("dist") ? join17(currentDir, "..") : join17(currentDir, "..", "..");
1712
1917
  return join17(packageRoot, ".claude");
1713
1918
  }
1714
1919
  async function copyDirectory3(srcDir, destDir, result, baseDir) {
@@ -1742,17 +1947,22 @@ async function generateClaudeConfig(targetDir) {
1742
1947
  const packageConfigDir = getPackageConfigDir();
1743
1948
  const targetClaudeDir = join17(targetDir, ".claude");
1744
1949
  await ensureDir(targetClaudeDir);
1745
- const settingsContent = JSON.stringify({ model: "opusplan" }, null, 2) + "\n";
1746
- const settingsResult = await writeFileSafe(
1747
- join17(targetClaudeDir, "settings.json"),
1748
- settingsContent,
1749
- { backup: true }
1750
- );
1751
- if (settingsResult.created) {
1752
- result.created.push(".claude/settings.json");
1753
- if (settingsResult.backedUp) {
1754
- result.backedUp.push(settingsResult.backedUp);
1950
+ const packageSettingsPath = join17(packageConfigDir, "settings.json");
1951
+ const targetSettingsPath = join17(targetClaudeDir, "settings.json");
1952
+ if (existsSync7(packageSettingsPath)) {
1953
+ if (existsSync7(targetSettingsPath)) {
1954
+ const backupPath = await backupFile(targetSettingsPath);
1955
+ if (backupPath) {
1956
+ const relativePath = backupPath.replace(targetDir + "/", "");
1957
+ result.backedUp.push(relativePath);
1958
+ }
1755
1959
  }
1960
+ await copyFile4(packageSettingsPath, targetSettingsPath);
1961
+ result.created.push(".claude/settings.json");
1962
+ } else {
1963
+ console.warn(
1964
+ "Warning: settings.json not found in package. Skipping settings generation."
1965
+ );
1756
1966
  }
1757
1967
  const packageSubagentsDir = join17(packageConfigDir, "subagents");
1758
1968
  const targetSubagentsDir = join17(targetClaudeDir, "subagents");
@@ -1770,7 +1980,7 @@ async function generateClaudeConfig(targetDir) {
1770
1980
  import { existsSync as existsSync8 } from "fs";
1771
1981
  import { readFile as readFile4 } from "fs/promises";
1772
1982
  import { join as join18 } from "path";
1773
- async function hasReact(targetDir) {
1983
+ async function hasReact2(targetDir) {
1774
1984
  try {
1775
1985
  const pkgPath = join18(targetDir, "package.json");
1776
1986
  if (existsSync8(pkgPath)) {
@@ -2035,7 +2245,7 @@ export default [
2035
2245
  `;
2036
2246
  }
2037
2247
  async function detectReact(targetDir) {
2038
- return hasReact(targetDir);
2248
+ return hasReact2(targetDir);
2039
2249
  }
2040
2250
  async function detectNextJs(targetDir) {
2041
2251
  return hasNextJs(targetDir);
@@ -2052,7 +2262,7 @@ async function generateEslint(targetDir, usesTypeScript, force = false) {
2052
2262
  return result;
2053
2263
  }
2054
2264
  const usesNextJs = await hasNextJs(targetDir);
2055
- const usesReact = await hasReact(targetDir);
2265
+ const usesReact = await hasReact2(targetDir);
2056
2266
  let config;
2057
2267
  if (usesNextJs && usesTypeScript) {
2058
2268
  config = generateNextJsConfig();
@@ -2136,6 +2346,41 @@ git push -u origin feature/my-feature
2136
2346
 
2137
2347
  ---
2138
2348
 
2349
+ ## \u{1F9EA} Test-Driven Development (TDD)
2350
+
2351
+ ### TDD Workflow
2352
+
2353
+ 1. **Write the test first** (red phase)
2354
+ 2. **Write minimal code to pass** (green phase)
2355
+ 3. **Refactor** with confidence (tests stay green)
2356
+
2357
+ ### Test Commands
2358
+
2359
+ \`\`\`bash
2360
+ # Run tests in watch mode (during development)
2361
+ ${pm.run} test:watch
2362
+
2363
+ # Run all tests once
2364
+ ${pm.run} test
2365
+
2366
+ # Run tests with coverage report
2367
+ ${pm.run} test:coverage
2368
+ \`\`\`
2369
+
2370
+ ### Git Hooks with Testing
2371
+
2372
+ - **Pre-commit:** Runs related tests for changed files
2373
+ - **Pre-push:** Runs full test suite with 80% coverage requirement
2374
+ - **Bypass (not recommended):** \`git push --no-verify\`
2375
+
2376
+ ### Coverage Requirements
2377
+
2378
+ - **Minimum:** 80% coverage for lines, functions, branches, statements
2379
+ - **Critical paths:** Aim for 100%
2380
+ - **New features:** Must include tests before merging
2381
+
2382
+ ---
2383
+
2139
2384
  ## \u{1F4DD} Commit Types
2140
2385
 
2141
2386
  | Type | Emoji | Use For |
@@ -2158,7 +2403,9 @@ git push -u origin feature/my-feature
2158
2403
  Before submitting:
2159
2404
  - [ ] Branch name follows convention
2160
2405
  - [ ] All commits have task links
2161
- - [ ] Tests pass locally
2406
+ - [ ] **Tests written before implementation (TDD)**
2407
+ - [ ] **All tests pass** (\`${pm.run} test\`)
2408
+ - [ ] **Coverage \u2265 80%** (\`${pm.run} test:coverage\`)
2162
2409
  - [ ] PR description filled out
2163
2410
  - [ ] Size is reasonable (< 400 lines)
2164
2411
 
@@ -2178,6 +2425,12 @@ ${pm.run} lint
2178
2425
 
2179
2426
  # Run tests
2180
2427
  ${pm.run} test
2428
+
2429
+ # Run tests with coverage
2430
+ ${pm.run} test:coverage
2431
+
2432
+ # Run tests in watch mode
2433
+ ${pm.run} test:watch
2181
2434
  \`\`\`
2182
2435
 
2183
2436
  ---
@@ -2523,12 +2776,200 @@ async function generateSharedConfigs(targetDir, projectType) {
2523
2776
  return result;
2524
2777
  }
2525
2778
 
2779
+ // src/generators/testing.ts
2780
+ import { join as join21 } from "path";
2781
+ var TEST_DEPENDENCIES = {
2782
+ vitest: [
2783
+ "vitest",
2784
+ // ^2.1.0
2785
+ "@vitest/coverage-v8"
2786
+ // ^2.1.0
2787
+ ],
2788
+ jest: [
2789
+ "jest",
2790
+ // ^29.7.0
2791
+ "@types/jest",
2792
+ // ^29.5.0
2793
+ "ts-jest"
2794
+ // ^29.2.0
2795
+ ]
2796
+ };
2797
+ var REACT_TEST_PACKAGES = [
2798
+ "@testing-library/react",
2799
+ // ^16.1.0
2800
+ "@testing-library/jest-dom",
2801
+ // ^6.6.0
2802
+ "@testing-library/user-event"
2803
+ // ^14.6.0
2804
+ ];
2805
+ function getVitestConfig(projectType) {
2806
+ if (projectType !== "single") {
2807
+ return `import { defineConfig } from "vitest/config";
2808
+
2809
+ export default defineConfig({
2810
+ test: {
2811
+ globals: true,
2812
+ environment: "node",
2813
+ coverage: {
2814
+ provider: "v8",
2815
+ reporter: ["text", "json", "html", "lcov"],
2816
+ thresholds: {
2817
+ lines: 80,
2818
+ functions: 80,
2819
+ branches: 80,
2820
+ statements: 80,
2821
+ },
2822
+ },
2823
+ },
2824
+ });
2825
+ `;
2826
+ }
2827
+ return `import { defineConfig } from "vitest/config";
2828
+
2829
+ export default defineConfig({
2830
+ test: {
2831
+ globals: true,
2832
+ environment: "node",
2833
+ coverage: {
2834
+ provider: "v8",
2835
+ reporter: ["text", "json", "html", "lcov"],
2836
+ thresholds: {
2837
+ lines: 80,
2838
+ functions: 80,
2839
+ branches: 80,
2840
+ statements: 80,
2841
+ },
2842
+ exclude: [
2843
+ "node_modules/**",
2844
+ "dist/**",
2845
+ "build/**",
2846
+ "**/*.config.{js,ts,mjs,cjs}",
2847
+ "**/*.test.{js,ts,jsx,tsx}",
2848
+ "**/__tests__/**",
2849
+ ],
2850
+ },
2851
+ },
2852
+ });
2853
+ `;
2854
+ }
2855
+ function getJestConfig(projectType) {
2856
+ const isMonorepo2 = projectType !== "single";
2857
+ return `/** @type {import('jest').Config} */
2858
+ const config = {
2859
+ preset: "ts-jest",
2860
+ testEnvironment: "node",
2861
+ ${isMonorepo2 ? "" : 'roots: ["<rootDir>/src"],\n '}testMatch: ["**/__tests__/**/*.test.ts", "**/*.test.ts"],
2862
+ collectCoverageFrom: [
2863
+ "src/**/*.{ts,tsx}",
2864
+ "!src/**/*.d.ts",
2865
+ "!src/**/*.config.{ts,js}",
2866
+ ],
2867
+ coverageThresholds: {
2868
+ global: {
2869
+ lines: 80,
2870
+ functions: 80,
2871
+ branches: 80,
2872
+ statements: 80,
2873
+ },
2874
+ },
2875
+ coverageReporters: ["text", "json", "html", "lcov"],
2876
+ };
2877
+
2878
+ module.exports = config;
2879
+ `;
2880
+ }
2881
+ function getTestSetupFile(framework) {
2882
+ if (framework === "vitest") {
2883
+ return `import "@testing-library/jest-dom/vitest";
2884
+ `;
2885
+ }
2886
+ return `import "@testing-library/jest-dom";
2887
+ `;
2888
+ }
2889
+ function getTestScripts(framework) {
2890
+ if (framework === "vitest") {
2891
+ return {
2892
+ test: "vitest run",
2893
+ "test:watch": "vitest",
2894
+ "test:coverage": "vitest run --coverage",
2895
+ "test:ui": "vitest --ui"
2896
+ };
2897
+ }
2898
+ return {
2899
+ test: "jest",
2900
+ "test:watch": "jest --watch",
2901
+ "test:coverage": "jest --coverage"
2902
+ };
2903
+ }
2904
+ async function generateTestingSetup(targetDir, framework, projectType) {
2905
+ const result = {
2906
+ created: [],
2907
+ modified: [],
2908
+ skipped: [],
2909
+ backedUp: []
2910
+ };
2911
+ const configFileName = framework === "vitest" ? "vitest.config.ts" : "jest.config.js";
2912
+ const configPath = join21(targetDir, configFileName);
2913
+ const configContent = framework === "vitest" ? getVitestConfig(projectType) : getJestConfig(projectType);
2914
+ const configResult = await writeFileSafe(configPath, configContent, {
2915
+ backup: true
2916
+ });
2917
+ if (configResult.created) {
2918
+ result.created.push(configPath);
2919
+ } else if (configResult.modified) {
2920
+ result.modified.push(configPath);
2921
+ if (configResult.backedUp) result.backedUp.push(configPath);
2922
+ } else {
2923
+ result.skipped.push(configPath);
2924
+ }
2925
+ const isReactProject = await hasReact(targetDir);
2926
+ if (isReactProject) {
2927
+ const setupFileName = framework === "vitest" ? "vitest.setup.ts" : "jest.setup.ts";
2928
+ const setupPath = join21(targetDir, setupFileName);
2929
+ const setupContent = getTestSetupFile(framework);
2930
+ const setupResult = await writeFileSafe(setupPath, setupContent, {
2931
+ backup: true
2932
+ });
2933
+ if (setupResult.created) {
2934
+ result.created.push(setupPath);
2935
+ } else if (setupResult.modified) {
2936
+ result.modified.push(setupPath);
2937
+ if (setupResult.backedUp) result.backedUp.push(setupPath);
2938
+ } else {
2939
+ result.skipped.push(setupPath);
2940
+ }
2941
+ if (framework === "vitest") {
2942
+ const updatedConfig = getVitestConfig(projectType).replace(
2943
+ 'environment: "node",',
2944
+ `environment: "jsdom",
2945
+ setupFiles: ["./vitest.setup.ts"],`
2946
+ );
2947
+ await writeFileSafe(configPath, updatedConfig, { backup: true });
2948
+ } else {
2949
+ const updatedConfig = getJestConfig(projectType).replace(
2950
+ 'testEnvironment: "node",',
2951
+ `testEnvironment: "jsdom",
2952
+ setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],`
2953
+ );
2954
+ await writeFileSafe(configPath, updatedConfig, { backup: true });
2955
+ }
2956
+ }
2957
+ return result;
2958
+ }
2959
+ function getTestDependencies(framework, includeReact) {
2960
+ const baseDeps = TEST_DEPENDENCIES[framework];
2961
+ if (includeReact) {
2962
+ return [...baseDeps, ...REACT_TEST_PACKAGES];
2963
+ }
2964
+ return baseDeps;
2965
+ }
2966
+
2526
2967
  // src/utils/git.ts
2527
2968
  import { execa } from "execa";
2528
2969
  import { existsSync as existsSync9 } from "fs";
2529
- import { join as join21 } from "path";
2970
+ import { join as join22 } from "path";
2530
2971
  async function isGitRepo(targetDir = process.cwd()) {
2531
- if (existsSync9(join21(targetDir, ".git"))) {
2972
+ if (existsSync9(join22(targetDir, ".git"))) {
2532
2973
  return true;
2533
2974
  }
2534
2975
  try {
@@ -2595,13 +3036,18 @@ async function updateProjectPackageJson(targetDir, config) {
2595
3036
  if (config.usesTypeScript) {
2596
3037
  scripts.typecheck = "tsc --noEmit";
2597
3038
  }
3039
+ if (config.testFramework) {
3040
+ const testScripts = getTestScripts(config.testFramework);
3041
+ Object.assign(scripts, testScripts);
3042
+ }
2598
3043
  pkg = mergeScripts(pkg, scripts, false);
2599
3044
  const lintStagedConfig = getLintStagedConfig(
2600
3045
  true,
2601
3046
  // usesEslint - always true now since we install it
2602
3047
  true,
2603
3048
  // usesPrettier - always true now since we install it
2604
- config.usesTypeScript
3049
+ config.usesTypeScript,
3050
+ config.testFramework
2605
3051
  );
2606
3052
  pkg = addPackageJsonConfig(pkg, "lint-staged", lintStagedConfig, true);
2607
3053
  await writePackageJson(pkg, targetDir);
@@ -2641,6 +3087,10 @@ async function runInit(targetDir = process.cwd()) {
2641
3087
  } else if (usesReact) {
2642
3088
  packagesToInstall = [...packagesToInstall, ...REACT_ESLINT_PACKAGES];
2643
3089
  }
3090
+ if (config.testFramework) {
3091
+ const testDeps = getTestDependencies(config.testFramework, usesReact);
3092
+ packagesToInstall = [...packagesToInstall, ...testDeps];
3093
+ }
2644
3094
  installSpinner.start("Installing dependencies...");
2645
3095
  const installResult = await installPackages(
2646
3096
  config.packageManager,
@@ -2669,13 +3119,23 @@ async function runInit(targetDir = process.cwd()) {
2669
3119
  const results = [];
2670
3120
  try {
2671
3121
  results.push(
2672
- await generateHuskyHooks(targetDir, config.projectType, config.packageManager)
3122
+ await generateHuskyHooks(
3123
+ targetDir,
3124
+ config.projectType,
3125
+ config.packageManager,
3126
+ config.testFramework
3127
+ )
2673
3128
  );
2674
3129
  results.push(await generateCommitlint(targetDir, config.asanaBaseUrl));
2675
3130
  results.push(await generateCzGit(targetDir, config.asanaBaseUrl));
2676
3131
  results.push(await generateBranchValidation(targetDir));
2677
3132
  results.push(await generateEslint(targetDir, config.usesTypeScript, false));
2678
3133
  results.push(await generatePrettier(targetDir));
3134
+ if (config.testFramework) {
3135
+ results.push(
3136
+ await generateTestingSetup(targetDir, config.testFramework, config.projectType)
3137
+ );
3138
+ }
2679
3139
  if (isMonorepo(config.projectType)) {
2680
3140
  results.push(await generateSharedConfigs(targetDir, config.projectType));
2681
3141
  }
@@ -2687,7 +3147,8 @@ async function runInit(targetDir = process.cwd()) {
2687
3147
  config.usesTypeScript,
2688
3148
  true,
2689
3149
  // usesEslint - always true now
2690
- config.packageManager
3150
+ config.packageManager,
3151
+ config.testFramework
2691
3152
  )
2692
3153
  );
2693
3154
  results.push(await generateCodeowners(targetDir, config.codeowners));
@@ -3699,7 +4160,7 @@ async function runInstallCommands(targetDir = process.cwd()) {
3699
4160
  // package.json
3700
4161
  var package_default = {
3701
4162
  name: "@raftlabs/raftstack",
3702
- version: "1.9.3",
4163
+ version: "1.10.0",
3703
4164
  description: "CLI tool for setting up Git hooks, commit conventions, and GitHub integration",
3704
4165
  type: "module",
3705
4166
  main: "./dist/index.js",