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,6 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  const color = require('chalk');
4
+ const promises = require('fs/promises');
5
+ const fs = require('fs');
6
+ const path = require('path');
4
7
 
5
8
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
6
9
 
@@ -96,6 +99,34 @@ const defaultFormatterConfig = {
96
99
  bracketSpacing: true,
97
100
  arrowParens: "always"
98
101
  };
102
+ const defaultPrettierConfig = {
103
+ $schema: "https://json.schemastore.org/prettierrc",
104
+ ...defaultFormatterConfig,
105
+ overrides: [
106
+ {
107
+ files: ["*.json", "**/*.json"],
108
+ options: { tabWidth: 2 }
109
+ },
110
+ {
111
+ files: ["*.md", "**/*.md"],
112
+ options: { tabWidth: 2, semi: false }
113
+ },
114
+ {
115
+ files: ["*.yml", "*.yaml", "**/*.yml", "**/*.yaml"],
116
+ options: { tabWidth: 2, semi: false }
117
+ }
118
+ ]
119
+ };
120
+ const defaultOxfmtConfig = {
121
+ printWidth: defaultFormatterConfig.printWidth,
122
+ tabWidth: defaultFormatterConfig.tabWidth,
123
+ useTabs: defaultFormatterConfig.useTabs,
124
+ semi: defaultFormatterConfig.semi,
125
+ singleQuote: defaultFormatterConfig.singleQuote,
126
+ trailingComma: defaultFormatterConfig.trailingComma,
127
+ bracketSpacing: defaultFormatterConfig.bracketSpacing,
128
+ arrowParens: defaultFormatterConfig.arrowParens
129
+ };
99
130
  const defaultLinterConfig = {
100
131
  ignorePatterns: ["dist"],
101
132
  rules: {
@@ -112,9 +143,88 @@ const defaultLinterConfig = {
112
143
  }
113
144
  };
114
145
 
146
+ const ALL_AI_PLATFORMS = ["agents", "claude"];
147
+ const AI_PLATFORM_LABELS = {
148
+ agents: "AGENTS.md",
149
+ claude: "CLAUDE.md"
150
+ };
151
+ const AI_PLATFORM_HINTS = {
152
+ agents: "OpenAI, Cursor, Windsurf, etc.",
153
+ claude: "Claude Code"
154
+ };
155
+ function generateAiFiles(files, params) {
156
+ const { platforms, isMonorepo, configStrategy, ...rest } = params;
157
+ if (platforms.length === 0) return;
158
+ files[".ai/workspace.md"] = {
159
+ type: "text",
160
+ content: generateWorkspace({
161
+ ...rest,
162
+ isMonorepo: !!isMonorepo,
163
+ configStrategy: configStrategy ?? "stealth"
164
+ })
165
+ };
166
+ const pointer = "See `.ai/` for project rules.\n";
167
+ for (const platform of platforms) {
168
+ const path = platform === "agents" ? "AGENTS.md" : "CLAUDE.md";
169
+ files[path] = { type: "text", content: pointer };
170
+ }
171
+ }
172
+ function generateWorkspace(ctx) {
173
+ const { name, packageManager, linter, formatter, isMonorepo, configStrategy } = ctx;
174
+ const sections = [
175
+ `# ${name}`,
176
+ "",
177
+ `- **Type:** ${isMonorepo ? "pnpm monorepo" : "standalone project"}`,
178
+ `- **Package Manager:** ${packageManager}`,
179
+ `- **Linter:** ${linter}`,
180
+ `- **Formatter:** ${formatter}`,
181
+ "",
182
+ "## Commands",
183
+ "",
184
+ `- \`${packageManager} test\` \u2014 run tests`,
185
+ `- \`${packageManager} build\` \u2014 build`,
186
+ `- \`${packageManager} lint\` and \`${packageManager} format\` \u2014 run before committing`
187
+ ];
188
+ if (isMonorepo) {
189
+ sections.push(
190
+ "",
191
+ "## Structure",
192
+ "",
193
+ "- `apps/` \u2014 applications",
194
+ "- `packages/` \u2014 shared libraries",
195
+ "- `.config/` \u2014 shared config packages",
196
+ "",
197
+ "## Monorepo",
198
+ "",
199
+ "- Use `workspace:*` for internal dependencies",
200
+ `- New packages: \`${packageManager} create krispya <name> --workspace\``
201
+ );
202
+ } else if (configStrategy === "root") {
203
+ sections.push(
204
+ "",
205
+ "## Structure",
206
+ "",
207
+ "- `src/` \u2014 source code",
208
+ "- `dist/` \u2014 generated, don't edit",
209
+ "- Config files (`tsconfig.json`, etc.) are at project root"
210
+ );
211
+ } else {
212
+ sections.push(
213
+ "",
214
+ "## Structure",
215
+ "",
216
+ "- `src/` \u2014 source code",
217
+ "- `.config/` \u2014 configs, don't move",
218
+ "- `dist/` \u2014 generated, don't edit"
219
+ );
220
+ }
221
+ sections.push("");
222
+ return sections.join("\n");
223
+ }
224
+
115
225
  function generateTypescriptConfig(baseTemplateOrParams) {
116
226
  const params = typeof baseTemplateOrParams === "string" ? { baseTemplate: baseTemplateOrParams } : baseTemplateOrParams;
117
- const { baseTemplate, useConfigPackage } = params;
227
+ const { baseTemplate, useConfigPackage, configStrategy = "stealth" } = params;
118
228
  const isReact = baseTemplate === "react";
119
229
  const isR3f = baseTemplate === "r3f";
120
230
  const files = {};
@@ -143,58 +253,113 @@ function generateTypescriptConfig(baseTemplateOrParams) {
143
253
  };
144
254
  return { files, devDependencies };
145
255
  }
146
- const tsConfig = {
147
- $schema: "https://json.schemastore.org/tsconfig",
148
- files: [],
149
- references: [{ path: "./.config/tsconfig.app.json" }, { path: "./.config/tsconfig.node.json" }]
150
- };
151
- files["tsconfig.json"] = {
152
- type: "text",
153
- content: JSON.stringify(tsConfig, null, 2)
154
- };
155
- const tsConfigApp = {
156
- $schema: "https://json.schemastore.org/tsconfig",
157
- compilerOptions: {
158
- target: "ESNext",
159
- module: "ESNext",
160
- moduleResolution: "bundler",
161
- lib: ["DOM", "DOM.Iterable", "ESNext"],
162
- esModuleInterop: true,
163
- allowSyntheticDefaultImports: true,
164
- strict: true,
165
- skipLibCheck: true,
166
- composite: true,
167
- rewriteRelativeImportExtensions: true,
168
- erasableSyntaxOnly: true,
169
- ...isReact || isR3f ? { jsx: "react-jsx" } : {}
170
- },
171
- include: ["../src", "../tests"]
172
- };
173
- files[".config/tsconfig.app.json"] = {
174
- type: "text",
175
- content: JSON.stringify(tsConfigApp, null, 2)
176
- };
177
- const tsConfigNode = {
178
- $schema: "https://json.schemastore.org/tsconfig",
179
- compilerOptions: {
180
- target: "ESNext",
181
- module: "ESNext",
182
- moduleResolution: "bundler",
183
- lib: ["ESNext"],
184
- esModuleInterop: true,
185
- allowSyntheticDefaultImports: true,
186
- strict: true,
187
- skipLibCheck: true,
188
- composite: true,
189
- rewriteRelativeImportExtensions: true,
190
- erasableSyntaxOnly: true
191
- },
192
- include: ["../*.config.ts", "./*.ts"]
193
- };
194
- files[".config/tsconfig.node.json"] = {
195
- type: "text",
196
- content: JSON.stringify(tsConfigNode, null, 2)
197
- };
256
+ if (configStrategy === "stealth") {
257
+ const tsConfig = {
258
+ $schema: "https://json.schemastore.org/tsconfig",
259
+ files: [],
260
+ references: [{ path: "./.config/tsconfig.app.json" }, { path: "./.config/tsconfig.node.json" }]
261
+ };
262
+ files["tsconfig.json"] = {
263
+ type: "text",
264
+ content: JSON.stringify(tsConfig, null, 2)
265
+ };
266
+ const tsConfigApp = {
267
+ $schema: "https://json.schemastore.org/tsconfig",
268
+ compilerOptions: {
269
+ target: "ESNext",
270
+ module: "ESNext",
271
+ moduleResolution: "bundler",
272
+ lib: ["DOM", "DOM.Iterable", "ESNext"],
273
+ esModuleInterop: true,
274
+ allowSyntheticDefaultImports: true,
275
+ strict: true,
276
+ skipLibCheck: true,
277
+ composite: true,
278
+ rewriteRelativeImportExtensions: true,
279
+ erasableSyntaxOnly: true,
280
+ ...isReact || isR3f ? { jsx: "react-jsx" } : {}
281
+ },
282
+ include: ["../src", "../tests"]
283
+ };
284
+ files[".config/tsconfig.app.json"] = {
285
+ type: "text",
286
+ content: JSON.stringify(tsConfigApp, null, 2)
287
+ };
288
+ const tsConfigNode = {
289
+ $schema: "https://json.schemastore.org/tsconfig",
290
+ compilerOptions: {
291
+ target: "ESNext",
292
+ module: "ESNext",
293
+ moduleResolution: "bundler",
294
+ lib: ["ESNext"],
295
+ esModuleInterop: true,
296
+ allowSyntheticDefaultImports: true,
297
+ strict: true,
298
+ skipLibCheck: true,
299
+ composite: true,
300
+ rewriteRelativeImportExtensions: true,
301
+ erasableSyntaxOnly: true
302
+ },
303
+ include: ["../*.config.ts", "./*.ts"]
304
+ };
305
+ files[".config/tsconfig.node.json"] = {
306
+ type: "text",
307
+ content: JSON.stringify(tsConfigNode, null, 2)
308
+ };
309
+ } else {
310
+ const tsConfig = {
311
+ $schema: "https://json.schemastore.org/tsconfig",
312
+ files: [],
313
+ references: [{ path: "./tsconfig.app.json" }, { path: "./tsconfig.node.json" }]
314
+ };
315
+ files["tsconfig.json"] = {
316
+ type: "text",
317
+ content: JSON.stringify(tsConfig, null, 2)
318
+ };
319
+ const tsConfigApp = {
320
+ $schema: "https://json.schemastore.org/tsconfig",
321
+ compilerOptions: {
322
+ target: "ESNext",
323
+ module: "ESNext",
324
+ moduleResolution: "bundler",
325
+ lib: ["DOM", "DOM.Iterable", "ESNext"],
326
+ esModuleInterop: true,
327
+ allowSyntheticDefaultImports: true,
328
+ strict: true,
329
+ skipLibCheck: true,
330
+ composite: true,
331
+ rewriteRelativeImportExtensions: true,
332
+ erasableSyntaxOnly: true,
333
+ ...isReact || isR3f ? { jsx: "react-jsx" } : {}
334
+ },
335
+ include: ["src", "tests"]
336
+ };
337
+ files["tsconfig.app.json"] = {
338
+ type: "text",
339
+ content: JSON.stringify(tsConfigApp, null, 2)
340
+ };
341
+ const tsConfigNode = {
342
+ $schema: "https://json.schemastore.org/tsconfig",
343
+ compilerOptions: {
344
+ target: "ESNext",
345
+ module: "ESNext",
346
+ moduleResolution: "bundler",
347
+ lib: ["ESNext"],
348
+ esModuleInterop: true,
349
+ allowSyntheticDefaultImports: true,
350
+ strict: true,
351
+ skipLibCheck: true,
352
+ composite: true,
353
+ rewriteRelativeImportExtensions: true,
354
+ erasableSyntaxOnly: true
355
+ },
356
+ include: ["*.config.ts"]
357
+ };
358
+ files["tsconfig.node.json"] = {
359
+ type: "text",
360
+ content: JSON.stringify(tsConfigNode, null, 2)
361
+ };
362
+ }
198
363
  return { files, devDependencies };
199
364
  }
200
365
 
@@ -604,9 +769,12 @@ function generateVscodeFiles$1(params) {
604
769
  };
605
770
  }
606
771
  if (Object.keys(vscodeSettings).length > 0) {
772
+ const sortedSettings = Object.fromEntries(
773
+ Object.entries(vscodeSettings).sort(([a], [b]) => a.localeCompare(b))
774
+ );
607
775
  files[".vscode/settings.json"] = {
608
776
  type: "text",
609
- content: JSON.stringify(vscodeSettings, null, " ")
777
+ content: JSON.stringify(sortedSettings, null, " ")
610
778
  };
611
779
  }
612
780
  return files;
@@ -659,128 +827,6 @@ function generateViteConfig(params) {
659
827
  return { type: "text", content: viteConfigContent };
660
828
  }
661
829
 
662
- function generateAiFiles(files, params) {
663
- const { name, packageManager, linter, formatter, aiFiles } = params;
664
- const content = getAiInstructionsContent({
665
- name,
666
- packageManager,
667
- linter,
668
- formatter
669
- });
670
- for (const fileChoice of aiFiles) {
671
- switch (fileChoice) {
672
- case "cursor-rules":
673
- files[".cursor/rules"] = { type: "text", content };
674
- break;
675
- case "agents-md":
676
- files["AGENTS.md"] = { type: "text", content };
677
- break;
678
- case "claude-md":
679
- files["CLAUDE.md"] = { type: "text", content };
680
- break;
681
- case "copilot-md":
682
- files[".github/copilot-instructions.md"] = { type: "text", content };
683
- break;
684
- }
685
- }
686
- }
687
- function getConfigPackagesDescription(linter, formatter) {
688
- const packages = ["`@config/typescript`"];
689
- if (linter !== "biome") {
690
- packages.push(`\`@config/${linter}\``);
691
- }
692
- if (formatter !== "biome" && formatter !== linter) {
693
- packages.push(`\`@config/${formatter}\``);
694
- }
695
- let description = `- \`.config/\`: shared config packages (${packages.join(", ")})`;
696
- if (linter === "biome" || formatter === "biome") {
697
- description += "\n- `biome.json`: Biome configuration (root level)";
698
- }
699
- return description;
700
- }
701
- function getAiInstructionsContent(params) {
702
- const { name, packageManager, linter, formatter } = params;
703
- const configDescription = getConfigPackagesDescription(linter, formatter);
704
- return `# ${name}
705
-
706
- This is a pnpm monorepo workspace generated with \`create-krispya\`.
707
-
708
- ## Most important rule (package creation)
709
-
710
- If you need a new app/package for any reason, **ALWAYS** create it with \`create-krispya\` (do not hand-create folders/package.json).
711
-
712
- ### Non-interactive (preferred for agents)
713
-
714
- \`\`\`bash
715
- ${packageManager} create krispya <name> --workspace [options]
716
- \`\`\`
717
-
718
- - The package directory will be \`apps/<name>\` (apps) or \`packages/<name>\` (libraries), unless \`--dir\` is provided.
719
- - Package names default to \`@${name}/<name>\` but you can pass any name (scoped or unscoped).
720
-
721
- ### Workspace maintenance (non-interactive)
722
-
723
- \`\`\`bash
724
- ${packageManager} create krispya --check # validate workspace
725
- ${packageManager} create krispya --fix --linter ${linter} --formatter ${formatter} # fix missing .config packages
726
- \`\`\`
727
-
728
- For non-interactive \`--fix\`, you MUST provide both \`--linter\` and \`--formatter\` flags.
729
-
730
- ## Package creation options (CLI truth)
731
-
732
- | Option | Values | Notes |
733
- |--------|--------|-------|
734
- | \`--type\` | app, library | default: app |
735
- | \`--template\` | vanilla, vanilla-js, react, react-js, r3f, r3f-js | default: vanilla |
736
- | \`--dir\` | any directory | requires \`--workspace\`; default: \`apps/\` or \`packages/\` |
737
- | \`--bundler\` | unbuild, tsdown | libraries only; default: unbuild |
738
-
739
- ### R3F flags (r3f templates only)
740
-
741
- \`--drei\` \`--handle\` \`--leva\` \`--postprocessing\` \`--rapier\` \`--xr\` \`--uikit\` \`--offscreen\` \`--zustand\` \`--koota\` \`--triplex\` \`--viverse\`
742
-
743
- ### Examples
744
-
745
- \`\`\`bash
746
- # React library (@${name}/ui) in packages/ui
747
- ${packageManager} create krispya ui --workspace --type library --template react
748
-
749
- # R3F app with physics + controls (@${name}/game) in apps/game
750
- ${packageManager} create krispya game --workspace --type app --template r3f --drei --rapier --leva
751
-
752
- # App in a custom directory
753
- ${packageManager} create krispya demo --workspace --type app --template react --dir examples
754
- \`\`\`
755
-
756
- ## After creating a package
757
-
758
- \`\`\`bash
759
- ${packageManager} install
760
- \`\`\`
761
-
762
- - Use \`"workspace:*"\` for internal deps (e.g. \`"@${name}/ui": "workspace:*"\`).
763
-
764
- ## Workspace commands
765
-
766
- \`\`\`bash
767
- ${packageManager} install # Install all dependencies
768
- ${packageManager} run dev # Run all apps in dev mode
769
- ${packageManager} run build # Build packages then apps
770
- ${packageManager} run test # Run all tests
771
- ${packageManager} run lint # Lint with ${linter}
772
- ${packageManager} run format # Format with ${formatter}
773
- \`\`\`
774
-
775
- ## Structure + conventions
776
-
777
- - \`apps/\`: applications (\`--type app\`)
778
- - \`packages/\`: libraries (\`--type library\`)
779
- ${configDescription}
780
- - TS configs extend \`@config/typescript/*\` (base/app/node/react)
781
- `;
782
- }
783
-
784
830
  function generateTypescriptConfigPackage(files) {
785
831
  const basePath = ".config/typescript";
786
832
  files[`${basePath}/package.json`] = {
@@ -1132,20 +1178,7 @@ Or in \`.prettierrc.json\`:
1132
1178
  };
1133
1179
  files[`${basePath}/base.json`] = {
1134
1180
  type: "text",
1135
- content: JSON.stringify(
1136
- {
1137
- printWidth: defaultFormatterConfig.printWidth,
1138
- tabWidth: defaultFormatterConfig.tabWidth,
1139
- useTabs: defaultFormatterConfig.useTabs,
1140
- semi: defaultFormatterConfig.semi,
1141
- singleQuote: defaultFormatterConfig.singleQuote,
1142
- trailingComma: defaultFormatterConfig.trailingComma,
1143
- bracketSpacing: defaultFormatterConfig.bracketSpacing,
1144
- arrowParens: defaultFormatterConfig.arrowParens
1145
- },
1146
- null,
1147
- 2
1148
- )
1181
+ content: JSON.stringify(defaultPrettierConfig, null, 2)
1149
1182
  };
1150
1183
  }
1151
1184
  function generateOxfmtConfigPackage(files) {
@@ -1184,20 +1217,7 @@ oxfmt -c node_modules/@config/oxfmt/base.json --write .
1184
1217
  };
1185
1218
  files[`${basePath}/base.json`] = {
1186
1219
  type: "text",
1187
- content: JSON.stringify(
1188
- {
1189
- printWidth: defaultFormatterConfig.printWidth,
1190
- tabWidth: defaultFormatterConfig.tabWidth,
1191
- useTabs: defaultFormatterConfig.useTabs,
1192
- semi: defaultFormatterConfig.semi,
1193
- singleQuote: defaultFormatterConfig.singleQuote,
1194
- trailingComma: defaultFormatterConfig.trailingComma,
1195
- bracketSpacing: defaultFormatterConfig.bracketSpacing,
1196
- arrowParens: defaultFormatterConfig.arrowParens
1197
- },
1198
- null,
1199
- 2
1200
- )
1220
+ content: JSON.stringify(defaultOxfmtConfig, null, 2)
1201
1221
  };
1202
1222
  }
1203
1223
 
@@ -1210,7 +1230,7 @@ function generateMonorepo(params) {
1210
1230
  pnpmVersion,
1211
1231
  pnpmManageVersions,
1212
1232
  nodeVersion,
1213
- aiFiles
1233
+ aiPlatforms
1214
1234
  } = params;
1215
1235
  const files = {};
1216
1236
  const isPnpm = packageManager === "pnpm";
@@ -1237,7 +1257,7 @@ function generateMonorepo(params) {
1237
1257
  build: "pnpm --filter './packages/*' run build && pnpm --filter './apps/*' run build",
1238
1258
  test: "pnpm -r run test",
1239
1259
  lint: linter === "oxlint" ? "oxlint ." : linter === "biome" ? "biome check ." : "eslint .",
1240
- format: formatter === "oxfmt" ? "oxfmt ." : formatter === "biome" ? "biome format . --write" : "prettier --write ."
1260
+ format: formatter === "oxfmt" ? "oxfmt -c .config/oxfmt/base.json ." : formatter === "biome" ? "biome format . --write" : "prettier --config .config/prettier/base.json --write ."
1241
1261
  },
1242
1262
  devDependencies
1243
1263
  };
@@ -1276,11 +1296,43 @@ function generateMonorepo(params) {
1276
1296
  content: workspaceLines.join("\n")
1277
1297
  };
1278
1298
  }
1299
+ files["tsconfig.json"] = {
1300
+ type: "text",
1301
+ content: JSON.stringify(
1302
+ {
1303
+ extends: "@config/typescript/base.json",
1304
+ compilerOptions: {
1305
+ noEmit: true
1306
+ },
1307
+ references: []
1308
+ },
1309
+ null,
1310
+ 2
1311
+ )
1312
+ };
1279
1313
  generateTypescriptConfigPackage(files);
1280
1314
  if (linter === "oxlint") {
1281
1315
  generateOxlintConfigPackage(files);
1316
+ files["oxlint.json"] = {
1317
+ type: "text",
1318
+ content: JSON.stringify(
1319
+ {
1320
+ $schema: "./node_modules/oxlint/configuration_schema.json",
1321
+ extends: ["@config/oxlint/base.json"]
1322
+ },
1323
+ null,
1324
+ 2
1325
+ )
1326
+ };
1282
1327
  } else if (linter === "eslint") {
1283
1328
  generateEslintConfigPackage(files);
1329
+ files["eslint.config.js"] = {
1330
+ type: "text",
1331
+ content: `import base from "@config/eslint/base";
1332
+
1333
+ export default [...base];
1334
+ `
1335
+ };
1284
1336
  } else if (linter === "biome") {
1285
1337
  const biomeConfig = {
1286
1338
  $schema: "https://biomejs.dev/schemas/1.9.4/schema.json",
@@ -1347,13 +1399,14 @@ This monorepo workspace was generated with create-krispya.
1347
1399
  To add a new package to this workspace, run create-krispya from this directory and it will detect the monorepo.
1348
1400
  `
1349
1401
  };
1350
- if (aiFiles && aiFiles.length > 0) {
1402
+ if (aiPlatforms && aiPlatforms.length > 0) {
1351
1403
  generateAiFiles(files, {
1352
1404
  name,
1353
1405
  packageManager,
1354
1406
  linter,
1355
1407
  formatter,
1356
- aiFiles
1408
+ isMonorepo: true,
1409
+ platforms: aiPlatforms
1357
1410
  });
1358
1411
  }
1359
1412
  return { files };
@@ -1425,14 +1478,11 @@ function generateBiome(generator, options) {
1425
1478
  if (options == null || !options.linter && !options.formatter) {
1426
1479
  return;
1427
1480
  }
1428
- const version = generator.versions.biome ?? "1.9.4";
1481
+ const version = generator.versions.biome ?? "2.0.0";
1429
1482
  generator.addDevDependency("@biomejs/biome", `^${version}`);
1430
1483
  const { rules } = defaultLinterConfig;
1431
1484
  const biomeConfig = {
1432
- $schema: "https://biomejs.dev/schemas/1.9.4/schema.json",
1433
- files: {
1434
- ignore: defaultLinterConfig.ignorePatterns
1435
- }
1485
+ $schema: `https://biomejs.dev/schemas/${version}/schema.json`
1436
1486
  };
1437
1487
  if (options.linter) {
1438
1488
  biomeConfig.linter = {
@@ -1465,27 +1515,52 @@ function generateBiome(generator, options) {
1465
1515
  arrowParentheses: "always"
1466
1516
  }
1467
1517
  };
1518
+ biomeConfig.json = {
1519
+ formatter: {
1520
+ indentWidth: 2
1521
+ }
1522
+ };
1468
1523
  } else {
1469
1524
  biomeConfig.formatter = {
1470
1525
  enabled: false
1471
1526
  };
1472
1527
  }
1473
- generator.addFile("biome.json", {
1474
- type: "text",
1475
- content: JSON.stringify(biomeConfig, null, 2)
1476
- });
1477
- if (options.linter) {
1478
- generator.addScript("lint", "biome lint .");
1479
- }
1480
- if (options.formatter) {
1481
- generator.addScript("format", "biome format --write .");
1528
+ const isStealth = generator.isStealthConfig();
1529
+ if (isStealth) {
1530
+ generator.addFile(".config/biome.json", {
1531
+ type: "text",
1532
+ content: JSON.stringify(biomeConfig, null, 2)
1533
+ });
1534
+ if (options.linter) {
1535
+ generator.addScript("lint", "biome lint --config-path .config .");
1536
+ }
1537
+ if (options.formatter) {
1538
+ generator.addScript(
1539
+ "format",
1540
+ "biome format --config-path .config --write ."
1541
+ );
1542
+ }
1543
+ generator.addVscodeSetting("biome.linter.configPath", ".config/biome.json");
1544
+ } else {
1545
+ generator.addFile("biome.json", {
1546
+ type: "text",
1547
+ content: JSON.stringify(biomeConfig, null, 2)
1548
+ });
1549
+ if (options.linter) {
1550
+ generator.addScript("lint", "biome lint .");
1551
+ }
1552
+ if (options.formatter) {
1553
+ generator.addScript("format", "biome format --write .");
1554
+ }
1482
1555
  }
1483
1556
  const roles = [];
1484
1557
  if (options.linter) roles.push("linter");
1485
1558
  if (options.formatter) roles.push("formatter");
1486
1559
  generator.inject(
1487
1560
  "readme-tools",
1488
- `[Biome](https://biomejs.dev/) - Fast ${roles.join(" and ")} for JavaScript and TypeScript`
1561
+ `[Biome](https://biomejs.dev/) - Fast ${roles.join(
1562
+ " and "
1563
+ )} for JavaScript and TypeScript`
1489
1564
  );
1490
1565
  generator.inject("vscode-extension-suggestion", "biomejs.biome");
1491
1566
  generator.addVscodeSetting("biome.enabled", true);
@@ -1570,11 +1645,23 @@ function generateEslint(generator, options) {
1570
1645
  },`,
1571
1646
  "]"
1572
1647
  ].filter(Boolean).join("\n");
1573
- generator.addFile("eslint.config.js", {
1574
- type: "text",
1575
- content: configContent
1576
- });
1577
- generator.addScript("lint", "eslint .");
1648
+ const isStealth = generator.isStealthConfig();
1649
+ if (isStealth) {
1650
+ generator.addFile(".config/eslint.config.js", {
1651
+ type: "text",
1652
+ content: configContent
1653
+ });
1654
+ generator.addScript("lint", "eslint --config .config/eslint.config.js .");
1655
+ generator.addVscodeSetting("eslint.options", {
1656
+ overrideConfigFile: ".config/eslint.config.js"
1657
+ });
1658
+ } else {
1659
+ generator.addFile("eslint.config.js", {
1660
+ type: "text",
1661
+ content: configContent
1662
+ });
1663
+ generator.addScript("lint", "eslint .");
1664
+ }
1578
1665
  generator.inject(
1579
1666
  "readme-tools",
1580
1667
  "[ESLint](https://eslint.org/) - Linter for JavaScript and TypeScript"
@@ -1744,22 +1831,21 @@ function generateOxfmt(generator, options) {
1744
1831
  } else {
1745
1832
  const version = generator.versions.oxfmt ?? "0.1.0";
1746
1833
  generator.addDevDependency("oxfmt", `^${version}`);
1747
- const oxfmtConfig = {
1748
- printWidth: defaultFormatterConfig.printWidth,
1749
- tabWidth: defaultFormatterConfig.tabWidth,
1750
- useTabs: defaultFormatterConfig.useTabs,
1751
- semi: defaultFormatterConfig.semi,
1752
- singleQuote: defaultFormatterConfig.singleQuote,
1753
- trailingComma: defaultFormatterConfig.trailingComma,
1754
- bracketSpacing: defaultFormatterConfig.bracketSpacing,
1755
- arrowParens: defaultFormatterConfig.arrowParens
1756
- };
1757
- generator.addFile(".config/oxfmt.json", {
1758
- type: "text",
1759
- content: JSON.stringify(oxfmtConfig, null, 2)
1760
- });
1761
- generator.addScript("format", "oxfmt -c .config/oxfmt.json --write .");
1762
- generator.addVscodeSetting("oxc.fmt.configPath", ".config/oxfmt.json");
1834
+ const isStealth = generator.isStealthConfig();
1835
+ if (isStealth) {
1836
+ generator.addFile(".config/oxfmt.json", {
1837
+ type: "text",
1838
+ content: JSON.stringify(defaultOxfmtConfig, null, 2)
1839
+ });
1840
+ generator.addScript("format", "oxfmt -c .config/oxfmt.json --write .");
1841
+ generator.addVscodeSetting("oxc.fmt.configPath", ".config/oxfmt.json");
1842
+ } else {
1843
+ generator.addFile("oxfmt.json", {
1844
+ type: "text",
1845
+ content: JSON.stringify(defaultOxfmtConfig, null, 2)
1846
+ });
1847
+ generator.addScript("format", "oxfmt -c oxfmt.json --write .");
1848
+ }
1763
1849
  }
1764
1850
  generator.inject(
1765
1851
  "readme-tools",
@@ -1773,6 +1859,12 @@ function generateOxfmt(generator, options) {
1773
1859
  generator.addVscodeSetting("[jsonc]", {
1774
1860
  "editor.defaultFormatter": "vscode.json-language-features"
1775
1861
  });
1862
+ generator.addVscodeSetting("[markdown]", {
1863
+ "editor.defaultFormatter": "vscode.markdown-language-features"
1864
+ });
1865
+ generator.addVscodeSetting("[yaml]", {
1866
+ "editor.defaultFormatter": "redhat.vscode-yaml"
1867
+ });
1776
1868
  }
1777
1869
 
1778
1870
  function toOxlintLevel(level) {
@@ -1791,13 +1883,14 @@ function generateOxlint(generator, options) {
1791
1883
  } else {
1792
1884
  const version = generator.versions.oxlint ?? "0.16.0";
1793
1885
  generator.addDevDependency("oxlint", `^${version}`);
1886
+ const isStealth = generator.isStealthConfig();
1794
1887
  const { rules } = defaultLinterConfig;
1795
1888
  const plugins = ["unicorn", "typescript", "oxc"];
1796
1889
  if (isReact) {
1797
1890
  plugins.push("react");
1798
1891
  }
1799
1892
  const oxlintConfig = {
1800
- $schema: "../node_modules/oxlint/configuration_schema.json",
1893
+ $schema: isStealth ? "../node_modules/oxlint/configuration_schema.json" : "./node_modules/oxlint/configuration_schema.json",
1801
1894
  plugins,
1802
1895
  rules: {
1803
1896
  "no-unused-vars": [
@@ -1816,12 +1909,20 @@ function generateOxlint(generator, options) {
1816
1909
  },
1817
1910
  ignorePatterns: defaultLinterConfig.ignorePatterns
1818
1911
  };
1819
- generator.addFile(".config/oxlint.json", {
1820
- type: "text",
1821
- content: JSON.stringify(oxlintConfig, null, 2)
1822
- });
1823
- generator.addScript("lint", "oxlint -c .config/oxlint.json");
1824
- generator.addVscodeSetting("oxc.configPath", ".config/oxlint.json");
1912
+ if (isStealth) {
1913
+ generator.addFile(".config/oxlint.json", {
1914
+ type: "text",
1915
+ content: JSON.stringify(oxlintConfig, null, 2)
1916
+ });
1917
+ generator.addScript("lint", "oxlint -c .config/oxlint.json");
1918
+ generator.addVscodeSetting("oxc.configPath", ".config/oxlint.json");
1919
+ } else {
1920
+ generator.addFile("oxlint.json", {
1921
+ type: "text",
1922
+ content: JSON.stringify(oxlintConfig, null, 2)
1923
+ });
1924
+ generator.addScript("lint", "oxlint");
1925
+ }
1825
1926
  }
1826
1927
  generator.inject(
1827
1928
  "readme-tools",
@@ -1852,25 +1953,33 @@ function generatePostprocessing(generator, options) {
1852
1953
  function generatePrettier(generator, options) {
1853
1954
  const version = generator.versions.prettier ?? "3.4.2";
1854
1955
  generator.addDevDependency("prettier", `^${version}`);
1855
- const prettierConfig = {
1856
- $schema: "https://json.schemastore.org/prettierrc",
1857
- printWidth: defaultFormatterConfig.printWidth,
1858
- tabWidth: defaultFormatterConfig.tabWidth,
1859
- useTabs: defaultFormatterConfig.useTabs,
1860
- semi: defaultFormatterConfig.semi,
1861
- singleQuote: defaultFormatterConfig.singleQuote,
1862
- trailingComma: defaultFormatterConfig.trailingComma,
1863
- bracketSpacing: defaultFormatterConfig.bracketSpacing,
1864
- arrowParens: defaultFormatterConfig.arrowParens
1865
- };
1866
- generator.addFile(".prettierrc", {
1867
- type: "text",
1868
- content: JSON.stringify(prettierConfig, null, 2)
1869
- });
1870
- generator.addScript("format", "prettier --write .");
1871
- generator.inject("readme-tools", "[Prettier](https://prettier.io/) - Opinionated code formatter");
1956
+ const isStealth = generator.isStealthConfig();
1957
+ if (isStealth) {
1958
+ generator.addFile(".config/prettier.json", {
1959
+ type: "text",
1960
+ content: JSON.stringify(defaultPrettierConfig, null, 2)
1961
+ });
1962
+ generator.addScript(
1963
+ "format",
1964
+ "prettier --config .config/prettier.json --write ."
1965
+ );
1966
+ generator.addVscodeSetting("prettier.configPath", ".config/prettier.json");
1967
+ } else {
1968
+ generator.addFile(".prettierrc", {
1969
+ type: "text",
1970
+ content: JSON.stringify(defaultPrettierConfig, null, 2)
1971
+ });
1972
+ generator.addScript("format", "prettier --write .");
1973
+ }
1974
+ generator.inject(
1975
+ "readme-tools",
1976
+ "[Prettier](https://prettier.io/) - Opinionated code formatter"
1977
+ );
1872
1978
  generator.inject("vscode-extension-suggestion", "esbenp.prettier-vscode");
1873
- generator.addVscodeSetting("editor.defaultFormatter", "esbenp.prettier-vscode");
1979
+ generator.addVscodeSetting(
1980
+ "editor.defaultFormatter",
1981
+ "esbenp.prettier-vscode"
1982
+ );
1874
1983
  }
1875
1984
 
1876
1985
  function generateRapier(generator, options) {
@@ -2120,18 +2229,19 @@ function generateUnbuild(generator) {
2120
2229
  }
2121
2230
  buildConfigLines.push(` },`);
2122
2231
  buildConfigLines.push(`})`);
2123
- if (isMonorepo) {
2124
- generator.addFile(`build.config.${ext}`, {
2232
+ const isStealth = generator.isStealthConfig() && !isMonorepo;
2233
+ if (isStealth) {
2234
+ generator.addFile(`.config/build.config.${ext}`, {
2125
2235
  type: "text",
2126
2236
  content: buildConfigLines.join("\n")
2127
2237
  });
2128
- generator.addScript("build", "unbuild");
2238
+ generator.addScript("build", `unbuild --config .config/build.config.${ext}`);
2129
2239
  } else {
2130
- generator.addFile(`.config/build.config.${ext}`, {
2240
+ generator.addFile(`build.config.${ext}`, {
2131
2241
  type: "text",
2132
2242
  content: buildConfigLines.join("\n")
2133
2243
  });
2134
- generator.addScript("build", `unbuild --config .config/build.config.${ext}`);
2244
+ generator.addScript("build", "unbuild");
2135
2245
  }
2136
2246
  generator.inject(
2137
2247
  "readme-libraries",
@@ -2439,6 +2549,82 @@ function parseWorkspaceYamlContent(content) {
2439
2549
  }
2440
2550
  return directories;
2441
2551
  }
2552
+ async function pathExists(path) {
2553
+ try {
2554
+ await promises.access(path, fs.constants.F_OK);
2555
+ return true;
2556
+ } catch {
2557
+ return false;
2558
+ }
2559
+ }
2560
+ function detectLinterFromScript(script) {
2561
+ if (!script) return void 0;
2562
+ if (script.includes("oxlint")) return "oxlint";
2563
+ if (script.includes("eslint")) return "eslint";
2564
+ if (script.includes("biome check") || script.includes("biome lint")) return "biome";
2565
+ return void 0;
2566
+ }
2567
+ function detectFormatterFromScript(script) {
2568
+ if (!script) return void 0;
2569
+ if (script.includes("prettier")) return "prettier";
2570
+ if (script.includes("oxfmt")) return "oxfmt";
2571
+ if (script.includes("biome format")) return "biome";
2572
+ return void 0;
2573
+ }
2574
+ async function detectLinterFromConfig(root) {
2575
+ if (await pathExists(path.join(root, ".config/oxlint"))) return "oxlint";
2576
+ if (await pathExists(path.join(root, ".config/eslint"))) return "eslint";
2577
+ if (await pathExists(path.join(root, "biome.json"))) {
2578
+ try {
2579
+ const content = await promises.readFile(path.join(root, "biome.json"), "utf-8");
2580
+ const config = JSON.parse(content);
2581
+ if (config.linter?.enabled !== false) return "biome";
2582
+ } catch {
2583
+ return "biome";
2584
+ }
2585
+ }
2586
+ return void 0;
2587
+ }
2588
+ async function detectFormatterFromConfig(root) {
2589
+ if (await pathExists(path.join(root, ".config/prettier"))) return "prettier";
2590
+ if (await pathExists(path.join(root, ".config/oxfmt"))) return "oxfmt";
2591
+ if (await pathExists(path.join(root, "biome.json"))) {
2592
+ try {
2593
+ const content = await promises.readFile(path.join(root, "biome.json"), "utf-8");
2594
+ const config = JSON.parse(content);
2595
+ if (config.formatter?.enabled !== false) return "biome";
2596
+ } catch {
2597
+ return "biome";
2598
+ }
2599
+ }
2600
+ return void 0;
2601
+ }
2602
+ function detectLinterFromDeps(devDeps) {
2603
+ if (!devDeps) return void 0;
2604
+ if (devDeps["@biomejs/biome"]) return "biome";
2605
+ if (devDeps.eslint) return "eslint";
2606
+ if (devDeps.oxlint) return "oxlint";
2607
+ return void 0;
2608
+ }
2609
+ function detectFormatterFromDeps(devDeps) {
2610
+ if (!devDeps) return void 0;
2611
+ if (devDeps["@biomejs/biome"]) return "biome";
2612
+ if (devDeps.prettier) return "prettier";
2613
+ if (devDeps.oxfmt) return "oxfmt";
2614
+ return void 0;
2615
+ }
2616
+ async function detectTooling(root) {
2617
+ try {
2618
+ const pkgPath = path.join(root, "package.json");
2619
+ const content = await promises.readFile(pkgPath, "utf-8");
2620
+ const pkg = JSON.parse(content);
2621
+ const linter = detectLinterFromScript(pkg.scripts?.lint) ?? await detectLinterFromConfig(root) ?? detectLinterFromDeps(pkg.devDependencies);
2622
+ const formatter = detectFormatterFromScript(pkg.scripts?.format) ?? await detectFormatterFromConfig(root) ?? detectFormatterFromDeps(pkg.devDependencies);
2623
+ return { linter, formatter };
2624
+ } catch {
2625
+ return { linter: void 0, formatter: void 0 };
2626
+ }
2627
+ }
2442
2628
  function generateRandomName() {
2443
2629
  const adjectives = [
2444
2630
  "red",
@@ -2560,7 +2746,8 @@ function generate(options) {
2560
2746
  if (language === "typescript") {
2561
2747
  const tsResult = generateTypescriptConfig({
2562
2748
  baseTemplate,
2563
- useConfigPackage: clonedOptions.workspaceRoot != null
2749
+ useConfigPackage: clonedOptions.workspaceRoot != null,
2750
+ configStrategy: clonedOptions.configStrategy
2564
2751
  });
2565
2752
  Object.assign(files, tsResult.files);
2566
2753
  Object.assign(devDependencies, tsResult.devDependencies);
@@ -2572,7 +2759,9 @@ function generate(options) {
2572
2759
  build: "vite build"
2573
2760
  };
2574
2761
  if (!isLibrary && (isReact || isR3f)) {
2575
- codeSnippets["vite-config-import"] = ["import react from '@vitejs/plugin-react'"];
2762
+ codeSnippets["vite-config-import"] = [
2763
+ "import react from '@vitejs/plugin-react'"
2764
+ ];
2576
2765
  }
2577
2766
  if (!isLibrary && isR3f) {
2578
2767
  codeSnippets["import"] = [`import { Canvas } from "@react-three/fiber"`];
@@ -2592,6 +2781,9 @@ function generate(options) {
2592
2781
  const generator = {
2593
2782
  options: clonedOptions,
2594
2783
  versions,
2784
+ isStealthConfig() {
2785
+ return (clonedOptions.configStrategy ?? "stealth") === "stealth";
2786
+ },
2595
2787
  addDependency(name2, semver) {
2596
2788
  dependencies[name2];
2597
2789
  dependencies[name2] = semver;
@@ -2648,7 +2840,10 @@ function generate(options) {
2648
2840
  generateTsdown(generator);
2649
2841
  }
2650
2842
  const packageManager2 = clonedOptions.packageManager ?? "pnpm";
2651
- generator.addScript("release", `${packageManager2} run build && ${packageManager2} publish`);
2843
+ generator.addScript(
2844
+ "release",
2845
+ `${packageManager2} run build && ${packageManager2} publish`
2846
+ );
2652
2847
  }
2653
2848
  const testing = clonedOptions.testing ?? (isLibrary ? "vitest" : "none");
2654
2849
  if (testing === "vitest") {
@@ -2741,9 +2936,24 @@ function generate(options) {
2741
2936
  };
2742
2937
  files[".gitattributes"] = { type: "text", content: GitAttributes };
2743
2938
  }
2939
+ if (!isMonorepoPackage && clonedOptions.aiPlatforms?.length) {
2940
+ generateAiFiles(files, {
2941
+ name,
2942
+ packageManager: clonedOptions.packageManager ?? "pnpm",
2943
+ linter: clonedOptions.linter ?? "oxlint",
2944
+ formatter: clonedOptions.formatter ?? "prettier",
2945
+ isMonorepo: false,
2946
+ configStrategy: clonedOptions.configStrategy,
2947
+ platforms: clonedOptions.aiPlatforms
2948
+ });
2949
+ }
2744
2950
  return files;
2745
2951
  }
2746
2952
 
2953
+ exports.AI_PLATFORM_HINTS = AI_PLATFORM_HINTS;
2954
+ exports.AI_PLATFORM_LABELS = AI_PLATFORM_LABELS;
2955
+ exports.ALL_AI_PLATFORMS = ALL_AI_PLATFORMS;
2956
+ exports.detectTooling = detectTooling;
2747
2957
  exports.generate = generate;
2748
2958
  exports.generateAiFiles = generateAiFiles;
2749
2959
  exports.generateEslintConfigPackage = generateEslintConfigPackage;