create-krispya 0.5.3 → 0.6.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.
@@ -1,4 +1,7 @@
1
1
  import color from 'chalk';
2
+ import { readFile, access } from 'fs/promises';
3
+ import { constants } from 'fs';
4
+ import { join } from 'path';
2
5
 
3
6
  const HtmlContent = `<!DOCTYPE html>
4
7
  <html lang="en">
@@ -90,6 +93,34 @@ const defaultFormatterConfig = {
90
93
  bracketSpacing: true,
91
94
  arrowParens: "always"
92
95
  };
96
+ const defaultPrettierConfig = {
97
+ $schema: "https://json.schemastore.org/prettierrc",
98
+ ...defaultFormatterConfig,
99
+ overrides: [
100
+ {
101
+ files: ["*.json", "**/*.json"],
102
+ options: { tabWidth: 2 }
103
+ },
104
+ {
105
+ files: ["*.md", "**/*.md"],
106
+ options: { tabWidth: 2, semi: false }
107
+ },
108
+ {
109
+ files: ["*.yml", "*.yaml", "**/*.yml", "**/*.yaml"],
110
+ options: { tabWidth: 2, semi: false }
111
+ }
112
+ ]
113
+ };
114
+ const defaultOxfmtConfig = {
115
+ printWidth: defaultFormatterConfig.printWidth,
116
+ tabWidth: defaultFormatterConfig.tabWidth,
117
+ useTabs: defaultFormatterConfig.useTabs,
118
+ semi: defaultFormatterConfig.semi,
119
+ singleQuote: defaultFormatterConfig.singleQuote,
120
+ trailingComma: defaultFormatterConfig.trailingComma,
121
+ bracketSpacing: defaultFormatterConfig.bracketSpacing,
122
+ arrowParens: defaultFormatterConfig.arrowParens
123
+ };
93
124
  const defaultLinterConfig = {
94
125
  ignorePatterns: ["dist"],
95
126
  rules: {
@@ -106,9 +137,88 @@ const defaultLinterConfig = {
106
137
  }
107
138
  };
108
139
 
140
+ const ALL_AI_PLATFORMS = ["agents", "claude"];
141
+ const AI_PLATFORM_LABELS = {
142
+ agents: "AGENTS.md",
143
+ claude: "CLAUDE.md"
144
+ };
145
+ const AI_PLATFORM_HINTS = {
146
+ agents: "OpenAI, Cursor, Windsurf, etc.",
147
+ claude: "Claude Code"
148
+ };
149
+ function generateAiFiles(files, params) {
150
+ const { platforms, isMonorepo, configStrategy, ...rest } = params;
151
+ if (platforms.length === 0) return;
152
+ files[".ai/workspace.md"] = {
153
+ type: "text",
154
+ content: generateWorkspace({
155
+ ...rest,
156
+ isMonorepo: !!isMonorepo,
157
+ configStrategy: configStrategy ?? "stealth"
158
+ })
159
+ };
160
+ const pointer = "See `.ai/` for project rules.\n";
161
+ for (const platform of platforms) {
162
+ const path = platform === "agents" ? "AGENTS.md" : "CLAUDE.md";
163
+ files[path] = { type: "text", content: pointer };
164
+ }
165
+ }
166
+ function generateWorkspace(ctx) {
167
+ const { name, packageManager, linter, formatter, isMonorepo, configStrategy } = ctx;
168
+ const sections = [
169
+ `# ${name}`,
170
+ "",
171
+ `- **Type:** ${isMonorepo ? "pnpm monorepo" : "standalone project"}`,
172
+ `- **Package Manager:** ${packageManager}`,
173
+ `- **Linter:** ${linter}`,
174
+ `- **Formatter:** ${formatter}`,
175
+ "",
176
+ "## Commands",
177
+ "",
178
+ `- \`${packageManager} test\` \u2014 run tests`,
179
+ `- \`${packageManager} build\` \u2014 build`,
180
+ `- \`${packageManager} lint\` and \`${packageManager} format\` \u2014 run before committing`
181
+ ];
182
+ if (isMonorepo) {
183
+ sections.push(
184
+ "",
185
+ "## Structure",
186
+ "",
187
+ "- `apps/` \u2014 applications",
188
+ "- `packages/` \u2014 shared libraries",
189
+ "- `.config/` \u2014 shared config packages",
190
+ "",
191
+ "## Monorepo",
192
+ "",
193
+ "- Use `workspace:*` for internal dependencies",
194
+ `- New packages: \`${packageManager} create krispya <name> --workspace\``
195
+ );
196
+ } else if (configStrategy === "root") {
197
+ sections.push(
198
+ "",
199
+ "## Structure",
200
+ "",
201
+ "- `src/` \u2014 source code",
202
+ "- `dist/` \u2014 generated, don't edit",
203
+ "- Config files (`tsconfig.json`, etc.) are at project root"
204
+ );
205
+ } else {
206
+ sections.push(
207
+ "",
208
+ "## Structure",
209
+ "",
210
+ "- `src/` \u2014 source code",
211
+ "- `.config/` \u2014 configs, don't move",
212
+ "- `dist/` \u2014 generated, don't edit"
213
+ );
214
+ }
215
+ sections.push("");
216
+ return sections.join("\n");
217
+ }
218
+
109
219
  function generateTypescriptConfig(baseTemplateOrParams) {
110
220
  const params = typeof baseTemplateOrParams === "string" ? { baseTemplate: baseTemplateOrParams } : baseTemplateOrParams;
111
- const { baseTemplate, useConfigPackage } = params;
221
+ const { baseTemplate, useConfigPackage, configStrategy = "stealth" } = params;
112
222
  const isReact = baseTemplate === "react";
113
223
  const isR3f = baseTemplate === "r3f";
114
224
  const files = {};
@@ -137,58 +247,113 @@ function generateTypescriptConfig(baseTemplateOrParams) {
137
247
  };
138
248
  return { files, devDependencies };
139
249
  }
140
- const tsConfig = {
141
- $schema: "https://json.schemastore.org/tsconfig",
142
- files: [],
143
- references: [{ path: "./.config/tsconfig.app.json" }, { path: "./.config/tsconfig.node.json" }]
144
- };
145
- files["tsconfig.json"] = {
146
- type: "text",
147
- content: JSON.stringify(tsConfig, null, 2)
148
- };
149
- const tsConfigApp = {
150
- $schema: "https://json.schemastore.org/tsconfig",
151
- compilerOptions: {
152
- target: "ESNext",
153
- module: "ESNext",
154
- moduleResolution: "bundler",
155
- lib: ["DOM", "DOM.Iterable", "ESNext"],
156
- esModuleInterop: true,
157
- allowSyntheticDefaultImports: true,
158
- strict: true,
159
- skipLibCheck: true,
160
- composite: true,
161
- rewriteRelativeImportExtensions: true,
162
- erasableSyntaxOnly: true,
163
- ...isReact || isR3f ? { jsx: "react-jsx" } : {}
164
- },
165
- include: ["../src", "../tests"]
166
- };
167
- files[".config/tsconfig.app.json"] = {
168
- type: "text",
169
- content: JSON.stringify(tsConfigApp, null, 2)
170
- };
171
- const tsConfigNode = {
172
- $schema: "https://json.schemastore.org/tsconfig",
173
- compilerOptions: {
174
- target: "ESNext",
175
- module: "ESNext",
176
- moduleResolution: "bundler",
177
- lib: ["ESNext"],
178
- esModuleInterop: true,
179
- allowSyntheticDefaultImports: true,
180
- strict: true,
181
- skipLibCheck: true,
182
- composite: true,
183
- rewriteRelativeImportExtensions: true,
184
- erasableSyntaxOnly: true
185
- },
186
- include: ["../*.config.ts", "./*.ts"]
187
- };
188
- files[".config/tsconfig.node.json"] = {
189
- type: "text",
190
- content: JSON.stringify(tsConfigNode, null, 2)
191
- };
250
+ if (configStrategy === "stealth") {
251
+ const tsConfig = {
252
+ $schema: "https://json.schemastore.org/tsconfig",
253
+ files: [],
254
+ references: [{ path: "./.config/tsconfig.app.json" }, { path: "./.config/tsconfig.node.json" }]
255
+ };
256
+ files["tsconfig.json"] = {
257
+ type: "text",
258
+ content: JSON.stringify(tsConfig, null, 2)
259
+ };
260
+ const tsConfigApp = {
261
+ $schema: "https://json.schemastore.org/tsconfig",
262
+ compilerOptions: {
263
+ target: "ESNext",
264
+ module: "ESNext",
265
+ moduleResolution: "bundler",
266
+ lib: ["DOM", "DOM.Iterable", "ESNext"],
267
+ esModuleInterop: true,
268
+ allowSyntheticDefaultImports: true,
269
+ strict: true,
270
+ skipLibCheck: true,
271
+ composite: true,
272
+ rewriteRelativeImportExtensions: true,
273
+ erasableSyntaxOnly: true,
274
+ ...isReact || isR3f ? { jsx: "react-jsx" } : {}
275
+ },
276
+ include: ["../src", "../tests"]
277
+ };
278
+ files[".config/tsconfig.app.json"] = {
279
+ type: "text",
280
+ content: JSON.stringify(tsConfigApp, null, 2)
281
+ };
282
+ const tsConfigNode = {
283
+ $schema: "https://json.schemastore.org/tsconfig",
284
+ compilerOptions: {
285
+ target: "ESNext",
286
+ module: "ESNext",
287
+ moduleResolution: "bundler",
288
+ lib: ["ESNext"],
289
+ esModuleInterop: true,
290
+ allowSyntheticDefaultImports: true,
291
+ strict: true,
292
+ skipLibCheck: true,
293
+ composite: true,
294
+ rewriteRelativeImportExtensions: true,
295
+ erasableSyntaxOnly: true
296
+ },
297
+ include: ["../*.config.ts", "./*.ts"]
298
+ };
299
+ files[".config/tsconfig.node.json"] = {
300
+ type: "text",
301
+ content: JSON.stringify(tsConfigNode, null, 2)
302
+ };
303
+ } else {
304
+ const tsConfig = {
305
+ $schema: "https://json.schemastore.org/tsconfig",
306
+ files: [],
307
+ references: [{ path: "./tsconfig.app.json" }, { path: "./tsconfig.node.json" }]
308
+ };
309
+ files["tsconfig.json"] = {
310
+ type: "text",
311
+ content: JSON.stringify(tsConfig, null, 2)
312
+ };
313
+ const tsConfigApp = {
314
+ $schema: "https://json.schemastore.org/tsconfig",
315
+ compilerOptions: {
316
+ target: "ESNext",
317
+ module: "ESNext",
318
+ moduleResolution: "bundler",
319
+ lib: ["DOM", "DOM.Iterable", "ESNext"],
320
+ esModuleInterop: true,
321
+ allowSyntheticDefaultImports: true,
322
+ strict: true,
323
+ skipLibCheck: true,
324
+ composite: true,
325
+ rewriteRelativeImportExtensions: true,
326
+ erasableSyntaxOnly: true,
327
+ ...isReact || isR3f ? { jsx: "react-jsx" } : {}
328
+ },
329
+ include: ["src", "tests"]
330
+ };
331
+ files["tsconfig.app.json"] = {
332
+ type: "text",
333
+ content: JSON.stringify(tsConfigApp, null, 2)
334
+ };
335
+ const tsConfigNode = {
336
+ $schema: "https://json.schemastore.org/tsconfig",
337
+ compilerOptions: {
338
+ target: "ESNext",
339
+ module: "ESNext",
340
+ moduleResolution: "bundler",
341
+ lib: ["ESNext"],
342
+ esModuleInterop: true,
343
+ allowSyntheticDefaultImports: true,
344
+ strict: true,
345
+ skipLibCheck: true,
346
+ composite: true,
347
+ rewriteRelativeImportExtensions: true,
348
+ erasableSyntaxOnly: true
349
+ },
350
+ include: ["*.config.ts"]
351
+ };
352
+ files["tsconfig.node.json"] = {
353
+ type: "text",
354
+ content: JSON.stringify(tsConfigNode, null, 2)
355
+ };
356
+ }
192
357
  return { files, devDependencies };
193
358
  }
194
359
 
@@ -598,9 +763,12 @@ function generateVscodeFiles$1(params) {
598
763
  };
599
764
  }
600
765
  if (Object.keys(vscodeSettings).length > 0) {
766
+ const sortedSettings = Object.fromEntries(
767
+ Object.entries(vscodeSettings).sort(([a], [b]) => a.localeCompare(b))
768
+ );
601
769
  files[".vscode/settings.json"] = {
602
770
  type: "text",
603
- content: JSON.stringify(vscodeSettings, null, " ")
771
+ content: JSON.stringify(sortedSettings, null, " ")
604
772
  };
605
773
  }
606
774
  return files;
@@ -653,128 +821,6 @@ function generateViteConfig(params) {
653
821
  return { type: "text", content: viteConfigContent };
654
822
  }
655
823
 
656
- function generateAiFiles(files, params) {
657
- const { name, packageManager, linter, formatter, aiFiles } = params;
658
- const content = getAiInstructionsContent({
659
- name,
660
- packageManager,
661
- linter,
662
- formatter
663
- });
664
- for (const fileChoice of aiFiles) {
665
- switch (fileChoice) {
666
- case "cursor-rules":
667
- files[".cursor/rules"] = { type: "text", content };
668
- break;
669
- case "agents-md":
670
- files["AGENTS.md"] = { type: "text", content };
671
- break;
672
- case "claude-md":
673
- files["CLAUDE.md"] = { type: "text", content };
674
- break;
675
- case "copilot-md":
676
- files[".github/copilot-instructions.md"] = { type: "text", content };
677
- break;
678
- }
679
- }
680
- }
681
- function getConfigPackagesDescription(linter, formatter) {
682
- const packages = ["`@config/typescript`"];
683
- if (linter !== "biome") {
684
- packages.push(`\`@config/${linter}\``);
685
- }
686
- if (formatter !== "biome" && formatter !== linter) {
687
- packages.push(`\`@config/${formatter}\``);
688
- }
689
- let description = `- \`.config/\`: shared config packages (${packages.join(", ")})`;
690
- if (linter === "biome" || formatter === "biome") {
691
- description += "\n- `biome.json`: Biome configuration (root level)";
692
- }
693
- return description;
694
- }
695
- function getAiInstructionsContent(params) {
696
- const { name, packageManager, linter, formatter } = params;
697
- const configDescription = getConfigPackagesDescription(linter, formatter);
698
- return `# ${name}
699
-
700
- This is a pnpm monorepo workspace generated with \`create-krispya\`.
701
-
702
- ## Most important rule (package creation)
703
-
704
- If you need a new app/package for any reason, **ALWAYS** create it with \`create-krispya\` (do not hand-create folders/package.json).
705
-
706
- ### Non-interactive (preferred for agents)
707
-
708
- \`\`\`bash
709
- ${packageManager} create krispya <name> --workspace [options]
710
- \`\`\`
711
-
712
- - The package directory will be \`apps/<name>\` (apps) or \`packages/<name>\` (libraries), unless \`--dir\` is provided.
713
- - Package names default to \`@${name}/<name>\` but you can pass any name (scoped or unscoped).
714
-
715
- ### Workspace maintenance (non-interactive)
716
-
717
- \`\`\`bash
718
- ${packageManager} create krispya --check # validate workspace
719
- ${packageManager} create krispya --fix --linter ${linter} --formatter ${formatter} # fix missing .config packages
720
- \`\`\`
721
-
722
- For non-interactive \`--fix\`, you MUST provide both \`--linter\` and \`--formatter\` flags.
723
-
724
- ## Package creation options (CLI truth)
725
-
726
- | Option | Values | Notes |
727
- |--------|--------|-------|
728
- | \`--type\` | app, library | default: app |
729
- | \`--template\` | vanilla, vanilla-js, react, react-js, r3f, r3f-js | default: vanilla |
730
- | \`--dir\` | any directory | requires \`--workspace\`; default: \`apps/\` or \`packages/\` |
731
- | \`--bundler\` | unbuild, tsdown | libraries only; default: unbuild |
732
-
733
- ### R3F flags (r3f templates only)
734
-
735
- \`--drei\` \`--handle\` \`--leva\` \`--postprocessing\` \`--rapier\` \`--xr\` \`--uikit\` \`--offscreen\` \`--zustand\` \`--koota\` \`--triplex\` \`--viverse\`
736
-
737
- ### Examples
738
-
739
- \`\`\`bash
740
- # React library (@${name}/ui) in packages/ui
741
- ${packageManager} create krispya ui --workspace --type library --template react
742
-
743
- # R3F app with physics + controls (@${name}/game) in apps/game
744
- ${packageManager} create krispya game --workspace --type app --template r3f --drei --rapier --leva
745
-
746
- # App in a custom directory
747
- ${packageManager} create krispya demo --workspace --type app --template react --dir examples
748
- \`\`\`
749
-
750
- ## After creating a package
751
-
752
- \`\`\`bash
753
- ${packageManager} install
754
- \`\`\`
755
-
756
- - Use \`"workspace:*"\` for internal deps (e.g. \`"@${name}/ui": "workspace:*"\`).
757
-
758
- ## Workspace commands
759
-
760
- \`\`\`bash
761
- ${packageManager} install # Install all dependencies
762
- ${packageManager} run dev # Run all apps in dev mode
763
- ${packageManager} run build # Build packages then apps
764
- ${packageManager} run test # Run all tests
765
- ${packageManager} run lint # Lint with ${linter}
766
- ${packageManager} run format # Format with ${formatter}
767
- \`\`\`
768
-
769
- ## Structure + conventions
770
-
771
- - \`apps/\`: applications (\`--type app\`)
772
- - \`packages/\`: libraries (\`--type library\`)
773
- ${configDescription}
774
- - TS configs extend \`@config/typescript/*\` (base/app/node/react)
775
- `;
776
- }
777
-
778
824
  function generateTypescriptConfigPackage(files) {
779
825
  const basePath = ".config/typescript";
780
826
  files[`${basePath}/package.json`] = {
@@ -1126,20 +1172,7 @@ Or in \`.prettierrc.json\`:
1126
1172
  };
1127
1173
  files[`${basePath}/base.json`] = {
1128
1174
  type: "text",
1129
- content: JSON.stringify(
1130
- {
1131
- printWidth: defaultFormatterConfig.printWidth,
1132
- tabWidth: defaultFormatterConfig.tabWidth,
1133
- useTabs: defaultFormatterConfig.useTabs,
1134
- semi: defaultFormatterConfig.semi,
1135
- singleQuote: defaultFormatterConfig.singleQuote,
1136
- trailingComma: defaultFormatterConfig.trailingComma,
1137
- bracketSpacing: defaultFormatterConfig.bracketSpacing,
1138
- arrowParens: defaultFormatterConfig.arrowParens
1139
- },
1140
- null,
1141
- 2
1142
- )
1175
+ content: JSON.stringify(defaultPrettierConfig, null, 2)
1143
1176
  };
1144
1177
  }
1145
1178
  function generateOxfmtConfigPackage(files) {
@@ -1178,20 +1211,7 @@ oxfmt -c node_modules/@config/oxfmt/base.json --write .
1178
1211
  };
1179
1212
  files[`${basePath}/base.json`] = {
1180
1213
  type: "text",
1181
- content: JSON.stringify(
1182
- {
1183
- printWidth: defaultFormatterConfig.printWidth,
1184
- tabWidth: defaultFormatterConfig.tabWidth,
1185
- useTabs: defaultFormatterConfig.useTabs,
1186
- semi: defaultFormatterConfig.semi,
1187
- singleQuote: defaultFormatterConfig.singleQuote,
1188
- trailingComma: defaultFormatterConfig.trailingComma,
1189
- bracketSpacing: defaultFormatterConfig.bracketSpacing,
1190
- arrowParens: defaultFormatterConfig.arrowParens
1191
- },
1192
- null,
1193
- 2
1194
- )
1214
+ content: JSON.stringify(defaultOxfmtConfig, null, 2)
1195
1215
  };
1196
1216
  }
1197
1217
 
@@ -1204,7 +1224,7 @@ function generateMonorepo(params) {
1204
1224
  pnpmVersion,
1205
1225
  pnpmManageVersions,
1206
1226
  nodeVersion,
1207
- aiFiles
1227
+ aiPlatforms
1208
1228
  } = params;
1209
1229
  const files = {};
1210
1230
  const isPnpm = packageManager === "pnpm";
@@ -1231,7 +1251,7 @@ function generateMonorepo(params) {
1231
1251
  build: "pnpm --filter './packages/*' run build && pnpm --filter './apps/*' run build",
1232
1252
  test: "pnpm -r run test",
1233
1253
  lint: linter === "oxlint" ? "oxlint ." : linter === "biome" ? "biome check ." : "eslint .",
1234
- format: formatter === "oxfmt" ? "oxfmt ." : formatter === "biome" ? "biome format . --write" : "prettier --write ."
1254
+ format: formatter === "oxfmt" ? "oxfmt -c .config/oxfmt/base.json ." : formatter === "biome" ? "biome format . --write" : "prettier --config .config/prettier/base.json --write ."
1235
1255
  },
1236
1256
  devDependencies
1237
1257
  };
@@ -1270,11 +1290,43 @@ function generateMonorepo(params) {
1270
1290
  content: workspaceLines.join("\n")
1271
1291
  };
1272
1292
  }
1293
+ files["tsconfig.json"] = {
1294
+ type: "text",
1295
+ content: JSON.stringify(
1296
+ {
1297
+ extends: "@config/typescript/base.json",
1298
+ compilerOptions: {
1299
+ noEmit: true
1300
+ },
1301
+ references: []
1302
+ },
1303
+ null,
1304
+ 2
1305
+ )
1306
+ };
1273
1307
  generateTypescriptConfigPackage(files);
1274
1308
  if (linter === "oxlint") {
1275
1309
  generateOxlintConfigPackage(files);
1310
+ files["oxlint.json"] = {
1311
+ type: "text",
1312
+ content: JSON.stringify(
1313
+ {
1314
+ $schema: "./node_modules/oxlint/configuration_schema.json",
1315
+ extends: ["@config/oxlint/base.json"]
1316
+ },
1317
+ null,
1318
+ 2
1319
+ )
1320
+ };
1276
1321
  } else if (linter === "eslint") {
1277
1322
  generateEslintConfigPackage(files);
1323
+ files["eslint.config.js"] = {
1324
+ type: "text",
1325
+ content: `import base from "@config/eslint/base";
1326
+
1327
+ export default [...base];
1328
+ `
1329
+ };
1278
1330
  } else if (linter === "biome") {
1279
1331
  const biomeConfig = {
1280
1332
  $schema: "https://biomejs.dev/schemas/1.9.4/schema.json",
@@ -1341,13 +1393,14 @@ This monorepo workspace was generated with create-krispya.
1341
1393
  To add a new package to this workspace, run create-krispya from this directory and it will detect the monorepo.
1342
1394
  `
1343
1395
  };
1344
- if (aiFiles && aiFiles.length > 0) {
1396
+ if (aiPlatforms && aiPlatforms.length > 0) {
1345
1397
  generateAiFiles(files, {
1346
1398
  name,
1347
1399
  packageManager,
1348
1400
  linter,
1349
1401
  formatter,
1350
- aiFiles
1402
+ isMonorepo: true,
1403
+ platforms: aiPlatforms
1351
1404
  });
1352
1405
  }
1353
1406
  return { files };
@@ -1419,14 +1472,11 @@ function generateBiome(generator, options) {
1419
1472
  if (options == null || !options.linter && !options.formatter) {
1420
1473
  return;
1421
1474
  }
1422
- const version = generator.versions.biome ?? "1.9.4";
1475
+ const version = generator.versions.biome ?? "2.0.0";
1423
1476
  generator.addDevDependency("@biomejs/biome", `^${version}`);
1424
1477
  const { rules } = defaultLinterConfig;
1425
1478
  const biomeConfig = {
1426
- $schema: "https://biomejs.dev/schemas/1.9.4/schema.json",
1427
- files: {
1428
- ignore: defaultLinterConfig.ignorePatterns
1429
- }
1479
+ $schema: `https://biomejs.dev/schemas/${version}/schema.json`
1430
1480
  };
1431
1481
  if (options.linter) {
1432
1482
  biomeConfig.linter = {
@@ -1459,27 +1509,52 @@ function generateBiome(generator, options) {
1459
1509
  arrowParentheses: "always"
1460
1510
  }
1461
1511
  };
1512
+ biomeConfig.json = {
1513
+ formatter: {
1514
+ indentWidth: 2
1515
+ }
1516
+ };
1462
1517
  } else {
1463
1518
  biomeConfig.formatter = {
1464
1519
  enabled: false
1465
1520
  };
1466
1521
  }
1467
- generator.addFile("biome.json", {
1468
- type: "text",
1469
- content: JSON.stringify(biomeConfig, null, 2)
1470
- });
1471
- if (options.linter) {
1472
- generator.addScript("lint", "biome lint .");
1473
- }
1474
- if (options.formatter) {
1475
- generator.addScript("format", "biome format --write .");
1522
+ const isStealth = generator.isStealthConfig();
1523
+ if (isStealth) {
1524
+ generator.addFile(".config/biome.json", {
1525
+ type: "text",
1526
+ content: JSON.stringify(biomeConfig, null, 2)
1527
+ });
1528
+ if (options.linter) {
1529
+ generator.addScript("lint", "biome lint --config-path .config .");
1530
+ }
1531
+ if (options.formatter) {
1532
+ generator.addScript(
1533
+ "format",
1534
+ "biome format --config-path .config --write ."
1535
+ );
1536
+ }
1537
+ generator.addVscodeSetting("biome.linter.configPath", ".config/biome.json");
1538
+ } else {
1539
+ generator.addFile("biome.json", {
1540
+ type: "text",
1541
+ content: JSON.stringify(biomeConfig, null, 2)
1542
+ });
1543
+ if (options.linter) {
1544
+ generator.addScript("lint", "biome lint .");
1545
+ }
1546
+ if (options.formatter) {
1547
+ generator.addScript("format", "biome format --write .");
1548
+ }
1476
1549
  }
1477
1550
  const roles = [];
1478
1551
  if (options.linter) roles.push("linter");
1479
1552
  if (options.formatter) roles.push("formatter");
1480
1553
  generator.inject(
1481
1554
  "readme-tools",
1482
- `[Biome](https://biomejs.dev/) - Fast ${roles.join(" and ")} for JavaScript and TypeScript`
1555
+ `[Biome](https://biomejs.dev/) - Fast ${roles.join(
1556
+ " and "
1557
+ )} for JavaScript and TypeScript`
1483
1558
  );
1484
1559
  generator.inject("vscode-extension-suggestion", "biomejs.biome");
1485
1560
  generator.addVscodeSetting("biome.enabled", true);
@@ -1564,11 +1639,23 @@ function generateEslint(generator, options) {
1564
1639
  },`,
1565
1640
  "]"
1566
1641
  ].filter(Boolean).join("\n");
1567
- generator.addFile("eslint.config.js", {
1568
- type: "text",
1569
- content: configContent
1570
- });
1571
- generator.addScript("lint", "eslint .");
1642
+ const isStealth = generator.isStealthConfig();
1643
+ if (isStealth) {
1644
+ generator.addFile(".config/eslint.config.js", {
1645
+ type: "text",
1646
+ content: configContent
1647
+ });
1648
+ generator.addScript("lint", "eslint --config .config/eslint.config.js .");
1649
+ generator.addVscodeSetting("eslint.options", {
1650
+ overrideConfigFile: ".config/eslint.config.js"
1651
+ });
1652
+ } else {
1653
+ generator.addFile("eslint.config.js", {
1654
+ type: "text",
1655
+ content: configContent
1656
+ });
1657
+ generator.addScript("lint", "eslint .");
1658
+ }
1572
1659
  generator.inject(
1573
1660
  "readme-tools",
1574
1661
  "[ESLint](https://eslint.org/) - Linter for JavaScript and TypeScript"
@@ -1738,22 +1825,21 @@ function generateOxfmt(generator, options) {
1738
1825
  } else {
1739
1826
  const version = generator.versions.oxfmt ?? "0.1.0";
1740
1827
  generator.addDevDependency("oxfmt", `^${version}`);
1741
- const oxfmtConfig = {
1742
- printWidth: defaultFormatterConfig.printWidth,
1743
- tabWidth: defaultFormatterConfig.tabWidth,
1744
- useTabs: defaultFormatterConfig.useTabs,
1745
- semi: defaultFormatterConfig.semi,
1746
- singleQuote: defaultFormatterConfig.singleQuote,
1747
- trailingComma: defaultFormatterConfig.trailingComma,
1748
- bracketSpacing: defaultFormatterConfig.bracketSpacing,
1749
- arrowParens: defaultFormatterConfig.arrowParens
1750
- };
1751
- generator.addFile(".config/oxfmt.json", {
1752
- type: "text",
1753
- content: JSON.stringify(oxfmtConfig, null, 2)
1754
- });
1755
- generator.addScript("format", "oxfmt -c .config/oxfmt.json --write .");
1756
- generator.addVscodeSetting("oxc.fmt.configPath", ".config/oxfmt.json");
1828
+ const isStealth = generator.isStealthConfig();
1829
+ if (isStealth) {
1830
+ generator.addFile(".config/oxfmt.json", {
1831
+ type: "text",
1832
+ content: JSON.stringify(defaultOxfmtConfig, null, 2)
1833
+ });
1834
+ generator.addScript("format", "oxfmt -c .config/oxfmt.json --write .");
1835
+ generator.addVscodeSetting("oxc.fmt.configPath", ".config/oxfmt.json");
1836
+ } else {
1837
+ generator.addFile("oxfmt.json", {
1838
+ type: "text",
1839
+ content: JSON.stringify(defaultOxfmtConfig, null, 2)
1840
+ });
1841
+ generator.addScript("format", "oxfmt -c oxfmt.json --write .");
1842
+ }
1757
1843
  }
1758
1844
  generator.inject(
1759
1845
  "readme-tools",
@@ -1767,6 +1853,12 @@ function generateOxfmt(generator, options) {
1767
1853
  generator.addVscodeSetting("[jsonc]", {
1768
1854
  "editor.defaultFormatter": "vscode.json-language-features"
1769
1855
  });
1856
+ generator.addVscodeSetting("[markdown]", {
1857
+ "editor.defaultFormatter": "vscode.markdown-language-features"
1858
+ });
1859
+ generator.addVscodeSetting("[yaml]", {
1860
+ "editor.defaultFormatter": "redhat.vscode-yaml"
1861
+ });
1770
1862
  }
1771
1863
 
1772
1864
  function toOxlintLevel(level) {
@@ -1785,13 +1877,14 @@ function generateOxlint(generator, options) {
1785
1877
  } else {
1786
1878
  const version = generator.versions.oxlint ?? "0.16.0";
1787
1879
  generator.addDevDependency("oxlint", `^${version}`);
1880
+ const isStealth = generator.isStealthConfig();
1788
1881
  const { rules } = defaultLinterConfig;
1789
1882
  const plugins = ["unicorn", "typescript", "oxc"];
1790
1883
  if (isReact) {
1791
1884
  plugins.push("react");
1792
1885
  }
1793
1886
  const oxlintConfig = {
1794
- $schema: "../node_modules/oxlint/configuration_schema.json",
1887
+ $schema: isStealth ? "../node_modules/oxlint/configuration_schema.json" : "./node_modules/oxlint/configuration_schema.json",
1795
1888
  plugins,
1796
1889
  rules: {
1797
1890
  "no-unused-vars": [
@@ -1810,12 +1903,20 @@ function generateOxlint(generator, options) {
1810
1903
  },
1811
1904
  ignorePatterns: defaultLinterConfig.ignorePatterns
1812
1905
  };
1813
- generator.addFile(".config/oxlint.json", {
1814
- type: "text",
1815
- content: JSON.stringify(oxlintConfig, null, 2)
1816
- });
1817
- generator.addScript("lint", "oxlint -c .config/oxlint.json");
1818
- generator.addVscodeSetting("oxc.configPath", ".config/oxlint.json");
1906
+ if (isStealth) {
1907
+ generator.addFile(".config/oxlint.json", {
1908
+ type: "text",
1909
+ content: JSON.stringify(oxlintConfig, null, 2)
1910
+ });
1911
+ generator.addScript("lint", "oxlint -c .config/oxlint.json");
1912
+ generator.addVscodeSetting("oxc.configPath", ".config/oxlint.json");
1913
+ } else {
1914
+ generator.addFile("oxlint.json", {
1915
+ type: "text",
1916
+ content: JSON.stringify(oxlintConfig, null, 2)
1917
+ });
1918
+ generator.addScript("lint", "oxlint");
1919
+ }
1819
1920
  }
1820
1921
  generator.inject(
1821
1922
  "readme-tools",
@@ -1846,25 +1947,33 @@ function generatePostprocessing(generator, options) {
1846
1947
  function generatePrettier(generator, options) {
1847
1948
  const version = generator.versions.prettier ?? "3.4.2";
1848
1949
  generator.addDevDependency("prettier", `^${version}`);
1849
- const prettierConfig = {
1850
- $schema: "https://json.schemastore.org/prettierrc",
1851
- printWidth: defaultFormatterConfig.printWidth,
1852
- tabWidth: defaultFormatterConfig.tabWidth,
1853
- useTabs: defaultFormatterConfig.useTabs,
1854
- semi: defaultFormatterConfig.semi,
1855
- singleQuote: defaultFormatterConfig.singleQuote,
1856
- trailingComma: defaultFormatterConfig.trailingComma,
1857
- bracketSpacing: defaultFormatterConfig.bracketSpacing,
1858
- arrowParens: defaultFormatterConfig.arrowParens
1859
- };
1860
- generator.addFile(".prettierrc", {
1861
- type: "text",
1862
- content: JSON.stringify(prettierConfig, null, 2)
1863
- });
1864
- generator.addScript("format", "prettier --write .");
1865
- generator.inject("readme-tools", "[Prettier](https://prettier.io/) - Opinionated code formatter");
1950
+ const isStealth = generator.isStealthConfig();
1951
+ if (isStealth) {
1952
+ generator.addFile(".config/prettier.json", {
1953
+ type: "text",
1954
+ content: JSON.stringify(defaultPrettierConfig, null, 2)
1955
+ });
1956
+ generator.addScript(
1957
+ "format",
1958
+ "prettier --config .config/prettier.json --write ."
1959
+ );
1960
+ generator.addVscodeSetting("prettier.configPath", ".config/prettier.json");
1961
+ } else {
1962
+ generator.addFile(".prettierrc", {
1963
+ type: "text",
1964
+ content: JSON.stringify(defaultPrettierConfig, null, 2)
1965
+ });
1966
+ generator.addScript("format", "prettier --write .");
1967
+ }
1968
+ generator.inject(
1969
+ "readme-tools",
1970
+ "[Prettier](https://prettier.io/) - Opinionated code formatter"
1971
+ );
1866
1972
  generator.inject("vscode-extension-suggestion", "esbenp.prettier-vscode");
1867
- generator.addVscodeSetting("editor.defaultFormatter", "esbenp.prettier-vscode");
1973
+ generator.addVscodeSetting(
1974
+ "editor.defaultFormatter",
1975
+ "esbenp.prettier-vscode"
1976
+ );
1868
1977
  }
1869
1978
 
1870
1979
  function generateRapier(generator, options) {
@@ -2114,18 +2223,19 @@ function generateUnbuild(generator) {
2114
2223
  }
2115
2224
  buildConfigLines.push(` },`);
2116
2225
  buildConfigLines.push(`})`);
2117
- if (isMonorepo) {
2118
- generator.addFile(`build.config.${ext}`, {
2226
+ const isStealth = generator.isStealthConfig() && !isMonorepo;
2227
+ if (isStealth) {
2228
+ generator.addFile(`.config/build.config.${ext}`, {
2119
2229
  type: "text",
2120
2230
  content: buildConfigLines.join("\n")
2121
2231
  });
2122
- generator.addScript("build", "unbuild");
2232
+ generator.addScript("build", `unbuild --config .config/build.config.${ext}`);
2123
2233
  } else {
2124
- generator.addFile(`.config/build.config.${ext}`, {
2234
+ generator.addFile(`build.config.${ext}`, {
2125
2235
  type: "text",
2126
2236
  content: buildConfigLines.join("\n")
2127
2237
  });
2128
- generator.addScript("build", `unbuild --config .config/build.config.${ext}`);
2238
+ generator.addScript("build", "unbuild");
2129
2239
  }
2130
2240
  generator.inject(
2131
2241
  "readme-libraries",
@@ -2433,6 +2543,82 @@ function parseWorkspaceYamlContent(content) {
2433
2543
  }
2434
2544
  return directories;
2435
2545
  }
2546
+ async function pathExists(path) {
2547
+ try {
2548
+ await access(path, constants.F_OK);
2549
+ return true;
2550
+ } catch {
2551
+ return false;
2552
+ }
2553
+ }
2554
+ function detectLinterFromScript(script) {
2555
+ if (!script) return void 0;
2556
+ if (script.includes("oxlint")) return "oxlint";
2557
+ if (script.includes("eslint")) return "eslint";
2558
+ if (script.includes("biome check") || script.includes("biome lint")) return "biome";
2559
+ return void 0;
2560
+ }
2561
+ function detectFormatterFromScript(script) {
2562
+ if (!script) return void 0;
2563
+ if (script.includes("prettier")) return "prettier";
2564
+ if (script.includes("oxfmt")) return "oxfmt";
2565
+ if (script.includes("biome format")) return "biome";
2566
+ return void 0;
2567
+ }
2568
+ async function detectLinterFromConfig(root) {
2569
+ if (await pathExists(join(root, ".config/oxlint"))) return "oxlint";
2570
+ if (await pathExists(join(root, ".config/eslint"))) return "eslint";
2571
+ if (await pathExists(join(root, "biome.json"))) {
2572
+ try {
2573
+ const content = await readFile(join(root, "biome.json"), "utf-8");
2574
+ const config = JSON.parse(content);
2575
+ if (config.linter?.enabled !== false) return "biome";
2576
+ } catch {
2577
+ return "biome";
2578
+ }
2579
+ }
2580
+ return void 0;
2581
+ }
2582
+ async function detectFormatterFromConfig(root) {
2583
+ if (await pathExists(join(root, ".config/prettier"))) return "prettier";
2584
+ if (await pathExists(join(root, ".config/oxfmt"))) return "oxfmt";
2585
+ if (await pathExists(join(root, "biome.json"))) {
2586
+ try {
2587
+ const content = await readFile(join(root, "biome.json"), "utf-8");
2588
+ const config = JSON.parse(content);
2589
+ if (config.formatter?.enabled !== false) return "biome";
2590
+ } catch {
2591
+ return "biome";
2592
+ }
2593
+ }
2594
+ return void 0;
2595
+ }
2596
+ function detectLinterFromDeps(devDeps) {
2597
+ if (!devDeps) return void 0;
2598
+ if (devDeps["@biomejs/biome"]) return "biome";
2599
+ if (devDeps.eslint) return "eslint";
2600
+ if (devDeps.oxlint) return "oxlint";
2601
+ return void 0;
2602
+ }
2603
+ function detectFormatterFromDeps(devDeps) {
2604
+ if (!devDeps) return void 0;
2605
+ if (devDeps["@biomejs/biome"]) return "biome";
2606
+ if (devDeps.prettier) return "prettier";
2607
+ if (devDeps.oxfmt) return "oxfmt";
2608
+ return void 0;
2609
+ }
2610
+ async function detectTooling(root) {
2611
+ try {
2612
+ const pkgPath = join(root, "package.json");
2613
+ const content = await readFile(pkgPath, "utf-8");
2614
+ const pkg = JSON.parse(content);
2615
+ const linter = detectLinterFromScript(pkg.scripts?.lint) ?? await detectLinterFromConfig(root) ?? detectLinterFromDeps(pkg.devDependencies);
2616
+ const formatter = detectFormatterFromScript(pkg.scripts?.format) ?? await detectFormatterFromConfig(root) ?? detectFormatterFromDeps(pkg.devDependencies);
2617
+ return { linter, formatter };
2618
+ } catch {
2619
+ return { linter: void 0, formatter: void 0 };
2620
+ }
2621
+ }
2436
2622
  function generateRandomName() {
2437
2623
  const adjectives = [
2438
2624
  "red",
@@ -2554,7 +2740,8 @@ function generate(options) {
2554
2740
  if (language === "typescript") {
2555
2741
  const tsResult = generateTypescriptConfig({
2556
2742
  baseTemplate,
2557
- useConfigPackage: clonedOptions.workspaceRoot != null
2743
+ useConfigPackage: clonedOptions.workspaceRoot != null,
2744
+ configStrategy: clonedOptions.configStrategy
2558
2745
  });
2559
2746
  Object.assign(files, tsResult.files);
2560
2747
  Object.assign(devDependencies, tsResult.devDependencies);
@@ -2566,7 +2753,9 @@ function generate(options) {
2566
2753
  build: "vite build"
2567
2754
  };
2568
2755
  if (!isLibrary && (isReact || isR3f)) {
2569
- codeSnippets["vite-config-import"] = ["import react from '@vitejs/plugin-react'"];
2756
+ codeSnippets["vite-config-import"] = [
2757
+ "import react from '@vitejs/plugin-react'"
2758
+ ];
2570
2759
  }
2571
2760
  if (!isLibrary && isR3f) {
2572
2761
  codeSnippets["import"] = [`import { Canvas } from "@react-three/fiber"`];
@@ -2586,6 +2775,9 @@ function generate(options) {
2586
2775
  const generator = {
2587
2776
  options: clonedOptions,
2588
2777
  versions,
2778
+ isStealthConfig() {
2779
+ return (clonedOptions.configStrategy ?? "stealth") === "stealth";
2780
+ },
2589
2781
  addDependency(name2, semver) {
2590
2782
  dependencies[name2];
2591
2783
  dependencies[name2] = semver;
@@ -2642,7 +2834,10 @@ function generate(options) {
2642
2834
  generateTsdown(generator);
2643
2835
  }
2644
2836
  const packageManager2 = clonedOptions.packageManager ?? "pnpm";
2645
- generator.addScript("release", `${packageManager2} run build && ${packageManager2} publish`);
2837
+ generator.addScript(
2838
+ "release",
2839
+ `${packageManager2} run build && ${packageManager2} publish`
2840
+ );
2646
2841
  }
2647
2842
  const testing = clonedOptions.testing ?? (isLibrary ? "vitest" : "none");
2648
2843
  if (testing === "vitest") {
@@ -2735,7 +2930,18 @@ function generate(options) {
2735
2930
  };
2736
2931
  files[".gitattributes"] = { type: "text", content: GitAttributes };
2737
2932
  }
2933
+ if (!isMonorepoPackage && clonedOptions.aiPlatforms?.length) {
2934
+ generateAiFiles(files, {
2935
+ name,
2936
+ packageManager: clonedOptions.packageManager ?? "pnpm",
2937
+ linter: clonedOptions.linter ?? "oxlint",
2938
+ formatter: clonedOptions.formatter ?? "prettier",
2939
+ isMonorepo: false,
2940
+ configStrategy: clonedOptions.configStrategy,
2941
+ platforms: clonedOptions.aiPlatforms
2942
+ });
2943
+ }
2738
2944
  return files;
2739
2945
  }
2740
2946
 
2741
- export { getLanguageFromTemplate as a, generateRandomName as b, generateTypescriptConfigPackage as c, generateOxlintConfigPackage as d, generateEslintConfigPackage as e, generateOxfmtConfigPackage as f, getBaseTemplate as g, generatePrettierConfigPackage as h, generateVscodeFiles as i, generateAiFiles as j, getLatestNpmVersion as k, generate as l, getLatestPnpmVersion as m, getLatestYarnVersion as n, getLatestNpmCliVersion as o, getLatestNodeVersion as p, parseWorkspaceYamlContent as q, generateMonorepo as r, monorepo as s, validatePackageName as v };
2947
+ export { ALL_AI_PLATFORMS as A, getLanguageFromTemplate as a, generateRandomName as b, generateAiFiles as c, detectTooling as d, generateVscodeFiles as e, generateTypescriptConfigPackage as f, getBaseTemplate as g, generateOxlintConfigPackage as h, generateEslintConfigPackage as i, generateOxfmtConfigPackage as j, generatePrettierConfigPackage as k, getLatestNpmVersion as l, generate as m, getLatestPnpmVersion as n, getLatestYarnVersion as o, parseWorkspaceYamlContent as p, getLatestNpmCliVersion as q, getLatestNodeVersion as r, AI_PLATFORM_LABELS as s, AI_PLATFORM_HINTS as t, generateMonorepo as u, validatePackageName as v, monorepo as w };