create-krispya 0.5.2 → 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.
package/dist/cli.mjs CHANGED
@@ -2,14 +2,14 @@
2
2
  import { createRequire } from 'module';
3
3
  import { cwd } from 'process';
4
4
  import { join, dirname, resolve } from 'path';
5
- import { access, constants, mkdir, writeFile, unlink, readFile } from 'fs/promises';
5
+ import { access, constants, readFile, mkdir, writeFile, rm, readdir, unlink } from 'fs/promises';
6
6
  import { constants as constants$1 } from 'fs';
7
7
  import { Command } from 'commander';
8
8
  import * as p from '@clack/prompts';
9
9
  import color from 'chalk';
10
10
  import { fetch } from 'undici';
11
11
  import { spawn } from 'child_process';
12
- import { g as getBaseTemplate, a as getLanguageFromTemplate, b as generateRandomName, c as generateTypescriptConfigPackage, d as generateOxlintConfigPackage, e as generateEslintConfigPackage, f as generateOxfmtConfigPackage, h as generatePrettierConfigPackage, i as generateVscodeFiles, j as generateAiFiles, k as getLatestNpmVersion, l as generate, m as getLatestPnpmVersion, n as getLatestYarnVersion, o as getLatestNpmCliVersion, p as getLatestNodeVersion, v as validatePackageName, q as parseWorkspaceYamlContent } from './chunks/index.mjs';
12
+ import { g as getBaseTemplate, a as getLanguageFromTemplate, b as generateRandomName, d as detectTooling, c as generateAiFiles, A as ALL_AI_PLATFORMS, e as generateVscodeFiles, f as generateTypescriptConfigPackage, h as generateOxlintConfigPackage, i as generateEslintConfigPackage, j as generateOxfmtConfigPackage, k as generatePrettierConfigPackage, p as parseWorkspaceYamlContent, l as getLatestNpmVersion, m as generate, n as getLatestPnpmVersion, o as getLatestYarnVersion, q as getLatestNpmCliVersion, r as getLatestNodeVersion, s as AI_PLATFORM_LABELS, t as AI_PLATFORM_HINTS, v as validatePackageName } from './chunks/index.mjs';
13
13
  import Conf from 'conf';
14
14
 
15
15
  const editorNames = {
@@ -37,14 +37,15 @@ function openInEditor(editor, path, reuseWindow) {
37
37
  });
38
38
  }
39
39
 
40
- function formatConfigSummary(options) {
40
+ function formatConfigSummary(options, inherited) {
41
41
  const lines = [];
42
42
  const VALUE_COL = 27;
43
- const formatRow = (label, value, indent = "") => {
43
+ const formatRow = (label, value, isInherited = false, indent = "") => {
44
44
  const fullLabel = indent + label;
45
45
  const dotCount = Math.max(1, VALUE_COL - fullLabel.length - 1);
46
46
  const dots = color.gray(".".repeat(dotCount));
47
- return `${indent}${label} ${dots} ${value}`;
47
+ const displayValue = isInherited ? `${value} \u{1F512}` : value;
48
+ return `${indent}${label} ${dots} ${displayValue}`;
48
49
  };
49
50
  const formatLanguage = (lang) => {
50
51
  return lang === "typescript" ? "TypeScript" : lang === "javascript" ? "JavaScript" : lang;
@@ -63,20 +64,29 @@ function formatConfigSummary(options) {
63
64
  } else {
64
65
  lines.push(formatRow("Bundler", "vite"));
65
66
  }
66
- lines.push(formatRow("Node version", options.nodeVersion || "latest"));
67
- lines.push(formatRow("Package manager", options.packageManager || "pnpm"));
67
+ const nodeVersionInherited = inherited?.nodeVersion !== void 0;
68
+ lines.push(formatRow("Node version", options.nodeVersion || "latest", nodeVersionInherited));
69
+ const pmInherited = inherited?.packageManager !== void 0;
70
+ lines.push(formatRow("Package manager", options.packageManager || "pnpm", pmInherited));
68
71
  if (options.packageManager === "pnpm") {
69
72
  const versionManaged = options.pnpmManageVersions ? "yes" : "no";
70
- lines.push(formatRow("\u21B3 Version managed", versionManaged, ""));
73
+ const pnpmVersionInherited = inherited?.pnpmManageVersions !== void 0;
74
+ lines.push(formatRow("\u21B3 Version managed", versionManaged, pnpmVersionInherited, ""));
71
75
  }
72
76
  if (options.linter) {
73
- lines.push(formatRow("Linter", options.linter));
77
+ const linterInherited = inherited?.linter !== void 0;
78
+ lines.push(formatRow("Linter", options.linter, linterInherited));
74
79
  }
75
80
  if (options.formatter) {
76
- lines.push(formatRow("Formatter", options.formatter));
81
+ const formatterInherited = inherited?.formatter !== void 0;
82
+ lines.push(formatRow("Formatter", options.formatter, formatterInherited));
77
83
  }
78
84
  const testing = options.testing ?? (projectType === "library" ? "vitest" : "none");
79
85
  lines.push(formatRow("Testing", testing));
86
+ if (!inherited) {
87
+ const configStrategy = options.configStrategy ?? "stealth";
88
+ lines.push(formatRow("Config strategy", configStrategy));
89
+ }
80
90
  if (options.template && getBaseTemplate(options.template) === "r3f") {
81
91
  const integrationNames = [
82
92
  options.drei && "drei",
@@ -138,11 +148,14 @@ function getReuseWindow() {
138
148
  function setReuseWindow(reuse) {
139
149
  config.set("reuseWindow", reuse);
140
150
  }
141
- function getAiFiles() {
142
- return config.get("aiFiles");
151
+ function getAiPlatforms() {
152
+ return config.get("aiPlatforms");
153
+ }
154
+ function setAiPlatforms(platforms) {
155
+ config.set("aiPlatforms", platforms);
143
156
  }
144
- function setAiFiles(files) {
145
- config.set("aiFiles", files);
157
+ function getConfigStrategy() {
158
+ return config.get("configStrategy") ?? "stealth";
146
159
  }
147
160
  function clearConfig() {
148
161
  config.clear();
@@ -154,20 +167,21 @@ function getCustomTemplates() {
154
167
  return config.get("customTemplates") ?? {};
155
168
  }
156
169
 
157
- function getDefaultOptions(template, name, projectType = "app", libraryBundler, integrations, inheritedTooling) {
170
+ function getDefaultOptions(template, name, projectType = "app", libraryBundler, integrations, inheritedSettings) {
158
171
  const baseTemplate = getBaseTemplate(template);
159
172
  const base = {
160
173
  name,
161
174
  template,
162
175
  projectType,
163
176
  libraryBundler: projectType === "library" ? libraryBundler ?? "unbuild" : void 0,
164
- packageManager: "pnpm",
165
- pnpmManageVersions: true,
166
- nodeVersion: "latest",
167
- linter: inheritedTooling?.linter ?? "oxlint",
168
- formatter: inheritedTooling?.formatter ?? "oxfmt",
177
+ packageManager: inheritedSettings?.packageManager ?? "pnpm",
178
+ pnpmManageVersions: inheritedSettings?.pnpmManageVersions ?? true,
179
+ nodeVersion: inheritedSettings?.nodeVersion ?? "latest",
180
+ linter: inheritedSettings?.linter ?? "oxlint",
181
+ formatter: inheritedSettings?.formatter ?? "prettier",
169
182
  // Libraries get vitest by default, apps don't
170
- testing: projectType === "library" ? "vitest" : "none"
183
+ testing: projectType === "library" ? "vitest" : "none",
184
+ configStrategy: getConfigStrategy()
171
185
  };
172
186
  if (baseTemplate === "r3f" && integrations) {
173
187
  return {
@@ -199,7 +213,22 @@ function getDefaultProjectName(template) {
199
213
  return `react-three-${generateRandomName()}`;
200
214
  }
201
215
  }
202
- async function promptForR3fIntegrations() {
216
+ async function promptForR3fIntegrations(presets) {
217
+ const initialValues = [];
218
+ if (presets) {
219
+ if (presets.drei) initialValues.push("drei");
220
+ if (presets.handle) initialValues.push("handle");
221
+ if (presets.leva) initialValues.push("leva");
222
+ if (presets.postprocessing) initialValues.push("postprocessing");
223
+ if (presets.rapier) initialValues.push("rapier");
224
+ if (presets.xr) initialValues.push("xr");
225
+ if (presets.uikit) initialValues.push("uikit");
226
+ if (presets.offscreen) initialValues.push("offscreen");
227
+ if (presets.zustand) initialValues.push("zustand");
228
+ if (presets.koota) initialValues.push("koota");
229
+ if (presets.triplex) initialValues.push("triplex");
230
+ if (presets.viverse) initialValues.push("viverse");
231
+ }
203
232
  const selected = await p.multiselect({
204
233
  message: "R3F integrations",
205
234
  options: [
@@ -216,7 +245,7 @@ async function promptForR3fIntegrations() {
216
245
  { value: "triplex", label: "Triplex" },
217
246
  { value: "viverse", label: "Viverse" }
218
247
  ],
219
- initialValues: ["drei"],
248
+ initialValues: initialValues.length > 0 ? initialValues : ["drei"],
220
249
  required: false
221
250
  });
222
251
  if (p.isCancel(selected)) {
@@ -225,7 +254,7 @@ async function promptForR3fIntegrations() {
225
254
  }
226
255
  return selected;
227
256
  }
228
- async function promptForCustomization(template, name, projectType, integrations, inheritedTooling) {
257
+ async function promptForCustomization(template, name, projectType, integrations, inheritedSettings, presets) {
229
258
  let libraryBundler;
230
259
  if (projectType === "library") {
231
260
  const bundler = await p.select({
@@ -234,7 +263,7 @@ async function promptForCustomization(template, name, projectType, integrations,
234
263
  { value: "unbuild", label: "unbuild", hint: "unjs, simple config" },
235
264
  { value: "tsdown", label: "tsdown", hint: "fast, esbuild-based" }
236
265
  ],
237
- initialValue: "unbuild"
266
+ initialValue: presets?.bundler ?? "unbuild"
238
267
  });
239
268
  if (p.isCancel(bundler)) {
240
269
  p.cancel("Operation cancelled.");
@@ -242,64 +271,57 @@ async function promptForCustomization(template, name, projectType, integrations,
242
271
  }
243
272
  libraryBundler = bundler;
244
273
  }
245
- const nodeVersion = await p.text({
246
- message: "Node.js version",
247
- placeholder: "latest",
248
- defaultValue: "latest",
249
- validate: (value) => {
250
- if (!value.length) return "Required";
251
- if (value !== "latest" && !/^\d+(\.\d+(\.\d+)?)?$/.test(value)) {
252
- return 'Must be "latest" or a valid semver (e.g., "22" or "22.13.0")';
253
- }
254
- }
255
- });
256
- if (p.isCancel(nodeVersion)) {
257
- p.cancel("Operation cancelled.");
258
- process.exit(0);
259
- }
260
- const packageManager = await p.select({
261
- message: "Package manager",
262
- options: [
263
- { value: "pnpm", label: "pnpm" },
264
- { value: "npm", label: "npm" },
265
- { value: "yarn", label: "yarn" },
266
- { value: "custom", label: "Other (custom)" }
267
- ],
268
- initialValue: "pnpm"
269
- });
270
- if (p.isCancel(packageManager)) {
271
- p.cancel("Operation cancelled.");
272
- process.exit(0);
273
- }
274
- let finalPackageManager = packageManager;
275
- if (packageManager === "custom") {
276
- const customPm = await p.text({
277
- message: "Enter package manager command",
274
+ let nodeVersion = inheritedSettings?.nodeVersion ?? presets?.nodeVersion ?? "latest";
275
+ let finalPackageManager = inheritedSettings?.packageManager ?? presets?.packageManager ?? "pnpm";
276
+ let pnpmManageVersions = inheritedSettings?.pnpmManageVersions ?? presets?.pnpmManageVersions ?? true;
277
+ if (!inheritedSettings?.nodeVersion) {
278
+ const nodeVersionInput = await p.text({
279
+ message: "Node.js version",
280
+ placeholder: presets?.nodeVersion ?? "latest",
281
+ defaultValue: presets?.nodeVersion ?? "latest",
278
282
  validate: (value) => {
279
283
  if (!value.length) return "Required";
284
+ if (value !== "latest" && !/^\d+(\.\d+(\.\d+)?)?$/.test(value)) {
285
+ return 'Must be "latest" or a valid semver (e.g., "22" or "22.13.0")';
286
+ }
280
287
  }
281
288
  });
282
- if (p.isCancel(customPm)) {
289
+ if (p.isCancel(nodeVersionInput)) {
283
290
  p.cancel("Operation cancelled.");
284
291
  process.exit(0);
285
292
  }
286
- finalPackageManager = customPm;
293
+ nodeVersion = nodeVersionInput;
287
294
  }
288
- let pnpmManageVersions = true;
289
- if (packageManager === "pnpm") {
290
- const managePnpm = await p.confirm({
291
- message: "Enable manage-package-manager-versions?",
292
- initialValue: true
295
+ if (!inheritedSettings?.packageManager) {
296
+ const packageManager = await p.select({
297
+ message: "Package manager",
298
+ options: [
299
+ { value: "pnpm", label: "pnpm" },
300
+ { value: "npm", label: "npm" },
301
+ { value: "yarn", label: "yarn" }
302
+ ],
303
+ initialValue: presets?.packageManager ?? "pnpm"
293
304
  });
294
- if (p.isCancel(managePnpm)) {
305
+ if (p.isCancel(packageManager)) {
295
306
  p.cancel("Operation cancelled.");
296
307
  process.exit(0);
297
308
  }
298
- pnpmManageVersions = managePnpm;
309
+ finalPackageManager = packageManager;
310
+ if (packageManager === "pnpm") {
311
+ const managePnpm = await p.confirm({
312
+ message: "Enable manage-package-manager-versions?",
313
+ initialValue: presets?.pnpmManageVersions ?? true
314
+ });
315
+ if (p.isCancel(managePnpm)) {
316
+ p.cancel("Operation cancelled.");
317
+ process.exit(0);
318
+ }
319
+ pnpmManageVersions = managePnpm;
320
+ }
299
321
  }
300
- let linter = inheritedTooling?.linter ?? "oxlint";
301
- let formatter = inheritedTooling?.formatter ?? "oxfmt";
302
- if (!inheritedTooling?.linter) {
322
+ let linter = inheritedSettings?.linter ?? presets?.linter ?? "oxlint";
323
+ let formatter = inheritedSettings?.formatter ?? presets?.formatter ?? "prettier";
324
+ if (!inheritedSettings?.linter) {
303
325
  const linterChoice = await p.select({
304
326
  message: "Linter",
305
327
  options: [
@@ -307,7 +329,7 @@ async function promptForCustomization(template, name, projectType, integrations,
307
329
  { value: "eslint", label: "ESLint", hint: "classic" },
308
330
  { value: "biome", label: "Biome", hint: "all-in-one" }
309
331
  ],
310
- initialValue: "oxlint"
332
+ initialValue: presets?.linter ?? "oxlint"
311
333
  });
312
334
  if (p.isCancel(linterChoice)) {
313
335
  p.cancel("Operation cancelled.");
@@ -315,15 +337,15 @@ async function promptForCustomization(template, name, projectType, integrations,
315
337
  }
316
338
  linter = linterChoice;
317
339
  }
318
- if (!inheritedTooling?.formatter) {
340
+ if (!inheritedSettings?.formatter) {
319
341
  const formatterChoice = await p.select({
320
342
  message: "Formatter",
321
343
  options: [
344
+ { value: "prettier", label: "Prettier", hint: "widely adopted" },
322
345
  { value: "oxfmt", label: "Oxfmt", hint: "fast, Prettier-compatible" },
323
- { value: "prettier", label: "Prettier", hint: "classic" },
324
346
  { value: "biome", label: "Biome", hint: "all-in-one" }
325
347
  ],
326
- initialValue: "oxfmt"
348
+ initialValue: presets?.formatter ?? "prettier"
327
349
  });
328
350
  if (p.isCancel(formatterChoice)) {
329
351
  p.cancel("Operation cancelled.");
@@ -355,6 +377,18 @@ async function promptForCustomization(template, name, projectType, integrations,
355
377
  p.cancel("Operation cancelled.");
356
378
  process.exit(0);
357
379
  }
380
+ const configStrategyChoice = await p.select({
381
+ message: "Config strategy",
382
+ options: [
383
+ { value: "stealth", label: "stealth", hint: "configs in .config/" },
384
+ { value: "root", label: "root", hint: "configs at project root" }
385
+ ],
386
+ initialValue: getConfigStrategy()
387
+ });
388
+ if (p.isCancel(configStrategyChoice)) {
389
+ p.cancel("Operation cancelled.");
390
+ process.exit(0);
391
+ }
358
392
  const baseTemplate = getBaseTemplate(template);
359
393
  const finalTemplate = language === "javascript" ? `${baseTemplate}-js` : baseTemplate;
360
394
  const base = {
@@ -367,7 +401,8 @@ async function promptForCustomization(template, name, projectType, integrations,
367
401
  pnpmManageVersions,
368
402
  linter,
369
403
  formatter,
370
- testing
404
+ testing,
405
+ configStrategy: configStrategyChoice
371
406
  };
372
407
  if (baseTemplate === "r3f" && integrations) {
373
408
  return {
@@ -412,14 +447,14 @@ function getDefaultMonorepoOptions(name) {
412
447
  pnpmManageVersions: true,
413
448
  nodeVersion: "latest",
414
449
  linter: "oxlint",
415
- formatter: "oxfmt"
450
+ formatter: "prettier"
416
451
  };
417
452
  }
418
- async function promptForMonorepoCustomization(name) {
453
+ async function promptForMonorepoCustomization(name, presets) {
419
454
  const nodeVersion = await p.text({
420
455
  message: "Node.js version",
421
- placeholder: "latest",
422
- defaultValue: "latest",
456
+ placeholder: presets?.nodeVersion ?? "latest",
457
+ defaultValue: presets?.nodeVersion ?? "latest",
423
458
  validate: (value) => {
424
459
  if (!value.length) return "Required";
425
460
  if (value !== "latest" && !/^\d+(\.\d+(\.\d+)?)?$/.test(value)) {
@@ -433,7 +468,7 @@ async function promptForMonorepoCustomization(name) {
433
468
  }
434
469
  const managePnpm = await p.confirm({
435
470
  message: "Enable manage-package-manager-versions?",
436
- initialValue: true
471
+ initialValue: presets?.pnpmManageVersions ?? true
437
472
  });
438
473
  if (p.isCancel(managePnpm)) {
439
474
  p.cancel("Operation cancelled.");
@@ -446,7 +481,7 @@ async function promptForMonorepoCustomization(name) {
446
481
  { value: "eslint", label: "ESLint", hint: "classic" },
447
482
  { value: "biome", label: "Biome", hint: "all-in-one" }
448
483
  ],
449
- initialValue: "oxlint"
484
+ initialValue: presets?.linter ?? "oxlint"
450
485
  });
451
486
  if (p.isCancel(linter)) {
452
487
  p.cancel("Operation cancelled.");
@@ -455,11 +490,11 @@ async function promptForMonorepoCustomization(name) {
455
490
  const formatter = await p.select({
456
491
  message: "Formatter",
457
492
  options: [
493
+ { value: "prettier", label: "Prettier", hint: "widely adopted" },
458
494
  { value: "oxfmt", label: "Oxfmt", hint: "fast, Prettier-compatible" },
459
- { value: "prettier", label: "Prettier", hint: "classic" },
460
495
  { value: "biome", label: "Biome", hint: "all-in-one" }
461
496
  ],
462
- initialValue: "oxfmt"
497
+ initialValue: presets?.formatter ?? "prettier"
463
498
  });
464
499
  if (p.isCancel(formatter)) {
465
500
  p.cancel("Operation cancelled.");
@@ -475,8 +510,15 @@ async function promptForMonorepoCustomization(name) {
475
510
  formatter
476
511
  };
477
512
  }
478
- async function promptForMonorepo(workspaceName) {
513
+ async function promptForMonorepo(workspaceName, presets) {
479
514
  const defaultOptions = getDefaultMonorepoOptions(workspaceName);
515
+ if (presets) {
516
+ if (presets.linter) defaultOptions.linter = presets.linter;
517
+ if (presets.formatter) defaultOptions.formatter = presets.formatter;
518
+ if (presets.nodeVersion) defaultOptions.nodeVersion = presets.nodeVersion;
519
+ if (presets.pnpmManageVersions !== void 0)
520
+ defaultOptions.pnpmManageVersions = presets.pnpmManageVersions;
521
+ }
480
522
  p.note(
481
523
  formatMonorepoConfigSummary({
482
524
  name: defaultOptions.name,
@@ -484,24 +526,28 @@ async function promptForMonorepo(workspaceName) {
484
526
  packageManager: defaultOptions.packageManager ?? "pnpm",
485
527
  pnpmManageVersions: defaultOptions.pnpmManageVersions,
486
528
  linter: defaultOptions.linter ?? "oxlint",
487
- formatter: defaultOptions.formatter ?? "oxfmt"
529
+ formatter: defaultOptions.formatter ?? "prettier"
488
530
  }),
489
531
  "Workspace Configuration"
490
532
  );
491
- const proceed = await p.confirm({
533
+ const proceed = await p.select({
492
534
  message: "Proceed with these settings?",
493
- initialValue: true
535
+ options: [
536
+ { value: "continue", label: "Yes, continue" },
537
+ { value: "customize", label: "No, customize settings" }
538
+ ],
539
+ initialValue: "continue"
494
540
  });
495
541
  if (p.isCancel(proceed)) {
496
542
  p.cancel("Operation cancelled.");
497
543
  process.exit(0);
498
544
  }
499
- if (proceed) {
545
+ if (proceed === "continue") {
500
546
  return defaultOptions;
501
547
  }
502
- return promptForMonorepoCustomization(workspaceName);
548
+ return promptForMonorepoCustomization(workspaceName, presets);
503
549
  }
504
- async function promptForOptions(name) {
550
+ async function promptForOptions(name, presets) {
505
551
  let projectName = name;
506
552
  if (!projectName) {
507
553
  const nameResult = await p.text({
@@ -525,30 +571,36 @@ async function promptForOptions(name) {
525
571
  { value: "library", label: "Library" },
526
572
  { value: "monorepo", label: "Monorepo" }
527
573
  ],
528
- initialValue: "app"
574
+ initialValue: presets?.type ?? "app"
529
575
  });
530
576
  if (p.isCancel(projectType)) {
531
577
  p.cancel("Operation cancelled.");
532
578
  process.exit(0);
533
579
  }
534
580
  if (projectType === "monorepo") {
535
- return promptForMonorepo(projectName);
581
+ return promptForMonorepo(projectName, presets);
536
582
  }
537
- return promptForPackageOptions(projectName, projectType);
583
+ return promptForPackageOptions(
584
+ projectName,
585
+ projectType,
586
+ void 0,
587
+ presets
588
+ );
538
589
  }
539
- function customTemplateToOptions(customTemplate, name, projectType) {
590
+ function customTemplateToOptions(customTemplate, name, projectType, inheritedSettings) {
540
591
  const baseTemplate = customTemplate.baseTemplate;
541
592
  const template = baseTemplate;
542
593
  const base = {
543
594
  name,
544
595
  template,
545
596
  projectType,
546
- packageManager: "pnpm",
547
- pnpmManageVersions: true,
548
- nodeVersion: "latest",
549
- linter: customTemplate.linter,
550
- formatter: customTemplate.formatter,
551
- testing: customTemplate.testing
597
+ packageManager: inheritedSettings?.packageManager ?? "pnpm",
598
+ pnpmManageVersions: inheritedSettings?.pnpmManageVersions ?? true,
599
+ nodeVersion: inheritedSettings?.nodeVersion ?? "latest",
600
+ linter: inheritedSettings?.linter ?? customTemplate.linter,
601
+ formatter: inheritedSettings?.formatter ?? customTemplate.formatter,
602
+ testing: customTemplate.testing,
603
+ configStrategy: customTemplate.configStrategy ?? getConfigStrategy()
552
604
  };
553
605
  if (baseTemplate === "r3f" && customTemplate.integrations) {
554
606
  const integrations = customTemplate.integrations;
@@ -570,7 +622,17 @@ function customTemplateToOptions(customTemplate, name, projectType) {
570
622
  }
571
623
  return base;
572
624
  }
573
- async function promptForPackageOptions(projectName, projectType, inheritedTooling) {
625
+ function presetsToInheritedSettings(presets) {
626
+ if (!presets) return void 0;
627
+ return {
628
+ linter: presets.linter,
629
+ formatter: presets.formatter,
630
+ packageManager: presets.packageManager,
631
+ nodeVersion: presets.nodeVersion,
632
+ pnpmManageVersions: presets.pnpmManageVersions
633
+ };
634
+ }
635
+ async function promptForPackageOptions(projectName, projectType, inheritedSettings, presets) {
574
636
  const builtInOptions = [
575
637
  { value: "vanilla", label: "Vanilla" },
576
638
  { value: "react", label: "React" },
@@ -586,7 +648,7 @@ async function promptForPackageOptions(projectName, projectType, inheritedToolin
586
648
  const templateSelection = await p.select({
587
649
  message: "Select a template",
588
650
  options: allOptions,
589
- initialValue: "vanilla"
651
+ initialValue: presets?.template ?? "vanilla"
590
652
  });
591
653
  if (p.isCancel(templateSelection)) {
592
654
  p.cancel("Operation cancelled.");
@@ -596,24 +658,27 @@ async function promptForPackageOptions(projectName, projectType, inheritedToolin
596
658
  if (selection.startsWith("custom:")) {
597
659
  const customName = selection.slice(7);
598
660
  const customTemplate = customTemplates[customName];
599
- const defaultOptions2 = customTemplateToOptions(customTemplate, projectName, projectType);
600
- if (inheritedTooling?.linter) {
601
- defaultOptions2.linter = inheritedTooling.linter;
602
- }
603
- if (inheritedTooling?.formatter) {
604
- defaultOptions2.formatter = inheritedTooling.formatter;
605
- }
606
- const configTitle2 = inheritedTooling ? `Template: ${customName} (using workspace tooling)` : `Template: ${customName}`;
607
- p.note(formatConfigSummary(defaultOptions2), configTitle2);
608
- const proceed2 = await p.confirm({
661
+ const defaultOptions2 = customTemplateToOptions(
662
+ customTemplate,
663
+ projectName,
664
+ projectType,
665
+ inheritedSettings
666
+ );
667
+ const configTitle2 = inheritedSettings ? `Template: ${customName} (using workspace settings)` : `Template: ${customName}`;
668
+ p.note(formatConfigSummary(defaultOptions2, inheritedSettings), configTitle2);
669
+ const proceed2 = await p.select({
609
670
  message: "Proceed with these settings?",
610
- initialValue: true
671
+ options: [
672
+ { value: "continue", label: "Yes, continue" },
673
+ { value: "customize", label: "No, customize settings" }
674
+ ],
675
+ initialValue: "continue"
611
676
  });
612
677
  if (p.isCancel(proceed2)) {
613
678
  p.cancel("Operation cancelled.");
614
679
  process.exit(0);
615
680
  }
616
- if (proceed2) {
681
+ if (proceed2 === "continue") {
617
682
  return defaultOptions2;
618
683
  }
619
684
  return promptForCustomization(
@@ -621,37 +686,48 @@ async function promptForPackageOptions(projectName, projectType, inheritedToolin
621
686
  projectName,
622
687
  projectType,
623
688
  customTemplate.integrations,
624
- inheritedTooling
689
+ inheritedSettings
625
690
  );
626
691
  }
627
692
  const template = selection;
628
693
  const baseTemplate = getBaseTemplate(template);
629
694
  let integrations;
630
695
  if (baseTemplate === "r3f") {
631
- integrations = await promptForR3fIntegrations();
696
+ integrations = await promptForR3fIntegrations(presets);
632
697
  }
633
698
  const defaultOptions = getDefaultOptions(
634
699
  template,
635
700
  projectName,
636
701
  projectType,
637
- void 0,
702
+ presets?.bundler,
638
703
  integrations,
639
- inheritedTooling
704
+ inheritedSettings ?? presetsToInheritedSettings(presets)
640
705
  );
641
- const configTitle = inheritedTooling ? "Template Configuration (using workspace tooling)" : "Template Configuration";
642
- p.note(formatConfigSummary(defaultOptions), configTitle);
643
- const proceed = await p.confirm({
706
+ const configTitle = inheritedSettings ? "Template Configuration (using workspace settings)" : "Template Configuration";
707
+ p.note(formatConfigSummary(defaultOptions, inheritedSettings), configTitle);
708
+ const proceed = await p.select({
644
709
  message: "Proceed with these settings?",
645
- initialValue: true
710
+ options: [
711
+ { value: "continue", label: "Yes, continue" },
712
+ { value: "customize", label: "No, customize settings" }
713
+ ],
714
+ initialValue: "continue"
646
715
  });
647
716
  if (p.isCancel(proceed)) {
648
717
  p.cancel("Operation cancelled.");
649
718
  process.exit(0);
650
719
  }
651
- if (proceed) {
720
+ if (proceed === "continue") {
652
721
  return defaultOptions;
653
722
  }
654
- return promptForCustomization(template, projectName, projectType, integrations, inheritedTooling);
723
+ return promptForCustomization(
724
+ template,
725
+ projectName,
726
+ projectType,
727
+ integrations,
728
+ inheritedSettings,
729
+ presets
730
+ );
655
731
  }
656
732
 
657
733
  async function checkAnyExists(paths) {
@@ -699,8 +775,586 @@ async function validateWorkspace(monorepoRoot) {
699
775
  return { valid: errors.length === 0, errors };
700
776
  }
701
777
 
778
+ async function detectCurrentConfig(root) {
779
+ let name = root.split(/[/\\]/).pop() ?? "workspace";
780
+ try {
781
+ const pkgPath = join(root, "package.json");
782
+ const content = await readFile(pkgPath, "utf-8");
783
+ const pkgJson = JSON.parse(content);
784
+ if (pkgJson.name) {
785
+ name = pkgJson.name.replace(/^@/, "").replace(/\/.*$/, "");
786
+ }
787
+ } catch {
788
+ }
789
+ const tooling = await detectTooling(root);
790
+ return {
791
+ name,
792
+ linter: tooling.linter ?? "oxlint",
793
+ formatter: tooling.formatter ?? "prettier",
794
+ packageManager: "pnpm"
795
+ };
796
+ }
797
+ function generateExpectedFiles(config) {
798
+ const { name, linter, formatter, packageManager } = config;
799
+ const aiFilesMap = {};
800
+ generateAiFiles(aiFilesMap, {
801
+ name,
802
+ packageManager,
803
+ linter,
804
+ formatter,
805
+ isMonorepo: true,
806
+ platforms: ALL_AI_PLATFORMS
807
+ });
808
+ const vscodeFiles = {};
809
+ generateVscodeFiles(vscodeFiles, linter, formatter);
810
+ const configPackages = {};
811
+ generateTypescriptConfigPackage(configPackages);
812
+ if (linter === "oxlint") {
813
+ generateOxlintConfigPackage(configPackages);
814
+ } else if (linter === "eslint") {
815
+ generateEslintConfigPackage(configPackages);
816
+ }
817
+ if (formatter === "oxfmt") {
818
+ generateOxfmtConfigPackage(configPackages);
819
+ } else if (formatter === "prettier") {
820
+ generatePrettierConfigPackage(configPackages);
821
+ }
822
+ const workspaceConfig = {};
823
+ const rootConfig = {};
824
+ rootConfig[".gitignore"] = {
825
+ type: "text",
826
+ content: ["node_modules", "dist", "*.tsbuildinfo", ".DS_Store"].join("\n")
827
+ };
828
+ rootConfig[".gitattributes"] = {
829
+ type: "text",
830
+ content: `* text=auto eol=lf
831
+ *.{cmd,[cC][mM][dD]} text eol=crlf
832
+ *.{bat,[bB][aA][tT]} text eol=crlf
833
+ `
834
+ };
835
+ if (linter === "biome" || formatter === "biome") {
836
+ const biomeConfig = {
837
+ $schema: "https://biomejs.dev/schemas/1.9.4/schema.json",
838
+ vcs: {
839
+ enabled: true,
840
+ clientKind: "git",
841
+ useIgnoreFile: true
842
+ },
843
+ linter: {
844
+ enabled: linter === "biome",
845
+ rules: {
846
+ recommended: true
847
+ }
848
+ },
849
+ formatter: {
850
+ enabled: formatter === "biome"
851
+ }
852
+ };
853
+ rootConfig["biome.json"] = {
854
+ type: "text",
855
+ content: JSON.stringify(biomeConfig, null, 2)
856
+ };
857
+ }
858
+ return {
859
+ "ai-files": aiFilesMap,
860
+ vscode: vscodeFiles,
861
+ "config-packages": configPackages,
862
+ "workspace-config": workspaceConfig,
863
+ "root-config": rootConfig
864
+ };
865
+ }
866
+ async function fileExists$1(path) {
867
+ try {
868
+ await access(path, constants$1.F_OK);
869
+ return true;
870
+ } catch {
871
+ return false;
872
+ }
873
+ }
874
+ async function compareWithDisk(expected, root) {
875
+ const categoryLabels = {
876
+ "ai-files": "AI Files",
877
+ vscode: "VS Code",
878
+ "config-packages": "Config Packages",
879
+ "workspace-config": "Workspace Config",
880
+ "root-config": "Root Config"
881
+ };
882
+ const categories = [];
883
+ for (const [category, files] of Object.entries(expected)) {
884
+ const changes = [];
885
+ for (const [filePath, file] of Object.entries(files)) {
886
+ if (file.type !== "text") continue;
887
+ const fullPath = join(root, filePath);
888
+ const newContent = file.content;
889
+ if (await fileExists$1(fullPath)) {
890
+ const currentContent = await readFile(fullPath, "utf-8");
891
+ if (currentContent === newContent) {
892
+ changes.push({
893
+ path: filePath,
894
+ status: "unchanged",
895
+ currentContent,
896
+ newContent
897
+ });
898
+ } else {
899
+ changes.push({
900
+ path: filePath,
901
+ status: "modified",
902
+ currentContent,
903
+ newContent
904
+ });
905
+ }
906
+ } else {
907
+ changes.push({
908
+ path: filePath,
909
+ status: "added",
910
+ newContent
911
+ });
912
+ }
913
+ }
914
+ if (changes.length === 0) continue;
915
+ const hasUserModifications = changes.some((c) => c.status === "modified");
916
+ categories.push({
917
+ category,
918
+ label: categoryLabels[category],
919
+ changes,
920
+ hasUserModifications
921
+ });
922
+ }
923
+ return categories;
924
+ }
925
+ async function getWorkspaceConfigUpdates(root) {
926
+ const workspacePath = join(root, "pnpm-workspace.yaml");
927
+ const changes = [];
928
+ let currentContent = "";
929
+ let exists = false;
930
+ try {
931
+ currentContent = await readFile(workspacePath, "utf-8");
932
+ exists = true;
933
+ } catch {
934
+ }
935
+ if (!exists) {
936
+ const newContent = `manage-package-manager-versions: true
937
+
938
+ packages:
939
+ - ".config/*"
940
+ - "apps/*"
941
+ - "packages/*"
942
+
943
+ onlyBuiltDependencies:
944
+ - esbuild
945
+ `;
946
+ changes.push({
947
+ path: "pnpm-workspace.yaml",
948
+ status: "added",
949
+ newContent
950
+ });
951
+ return changes;
952
+ }
953
+ let updatedContent = currentContent;
954
+ let needsUpdate = false;
955
+ if (!currentContent.includes("manage-package-manager-versions")) {
956
+ updatedContent = `manage-package-manager-versions: true
957
+
958
+ ${updatedContent}`;
959
+ needsUpdate = true;
960
+ }
961
+ if (!currentContent.includes("onlyBuiltDependencies")) {
962
+ updatedContent = `${updatedContent.trimEnd()}
963
+
964
+ onlyBuiltDependencies:
965
+ - esbuild
966
+ `;
967
+ needsUpdate = true;
968
+ }
969
+ if (!currentContent.includes(".config/*") && !currentContent.includes('".config/*"')) {
970
+ const lines = updatedContent.split("\n");
971
+ const packagesIndex = lines.findIndex(
972
+ (line) => line.trim().startsWith("packages:")
973
+ );
974
+ if (packagesIndex !== -1) {
975
+ lines.splice(packagesIndex + 1, 0, ' - ".config/*"');
976
+ updatedContent = lines.join("\n");
977
+ needsUpdate = true;
978
+ }
979
+ }
980
+ if (needsUpdate) {
981
+ changes.push({
982
+ path: "pnpm-workspace.yaml",
983
+ status: "modified",
984
+ currentContent,
985
+ newContent: updatedContent
986
+ });
987
+ } else {
988
+ changes.push({
989
+ path: "pnpm-workspace.yaml",
990
+ status: "unchanged",
991
+ currentContent,
992
+ newContent: currentContent
993
+ });
994
+ }
995
+ return changes;
996
+ }
997
+ async function applyUpdates(changes, root) {
998
+ for (const change of changes) {
999
+ if (change.status === "unchanged") continue;
1000
+ const fullPath = join(root, change.path);
1001
+ await mkdir(dirname(fullPath), { recursive: true });
1002
+ await writeFile(fullPath, change.newContent);
1003
+ }
1004
+ }
1005
+ function formatFileChange(change) {
1006
+ const icon = change.status === "added" ? "+" : change.status === "modified" ? "~" : "=";
1007
+ return ` ${icon} ${change.path}`;
1008
+ }
1009
+ const LINTER_DEPS = {
1010
+ oxlint: "oxlint",
1011
+ eslint: "eslint",
1012
+ biome: "@biomejs/biome"
1013
+ };
1014
+ const FORMATTER_DEPS = {
1015
+ oxfmt: "oxfmt",
1016
+ prettier: "prettier",
1017
+ biome: "@biomejs/biome"
1018
+ };
1019
+ const LINTER_CONFIG_PACKAGES = {
1020
+ oxlint: "@config/oxlint",
1021
+ eslint: "@config/eslint",
1022
+ biome: null
1023
+ // biome uses root biome.json
1024
+ };
1025
+ const FORMATTER_CONFIG_PACKAGES = {
1026
+ oxfmt: "@config/oxfmt",
1027
+ prettier: "@config/prettier",
1028
+ biome: null
1029
+ // biome uses root biome.json
1030
+ };
1031
+ function needsMigration(current, target) {
1032
+ const linterChange = target.linter && target.linter !== current.linter;
1033
+ const formatterChange = target.formatter && target.formatter !== current.formatter;
1034
+ return linterChange || formatterChange || false;
1035
+ }
1036
+ async function getMigrationPlan(current, target, root) {
1037
+ const toLinter = target.linter ?? current.linter;
1038
+ const toFormatter = target.formatter ?? current.formatter;
1039
+ const changes = [];
1040
+ if (toLinter !== current.linter) {
1041
+ if (current.linter !== "biome") {
1042
+ changes.push({
1043
+ type: "remove-dir",
1044
+ path: `.config/${current.linter}`,
1045
+ description: `Remove @config/${current.linter} package`
1046
+ });
1047
+ }
1048
+ if (toLinter !== "biome") {
1049
+ const files = {};
1050
+ if (toLinter === "oxlint") {
1051
+ generateOxlintConfigPackage(files);
1052
+ } else if (toLinter === "eslint") {
1053
+ generateEslintConfigPackage(files);
1054
+ }
1055
+ for (const [path, file] of Object.entries(files)) {
1056
+ if (file.type === "text") {
1057
+ changes.push({
1058
+ type: "add-file",
1059
+ path,
1060
+ description: `Add ${path}`,
1061
+ content: file.content
1062
+ });
1063
+ }
1064
+ }
1065
+ }
1066
+ if (toLinter === "biome" && toFormatter === "biome") {
1067
+ changes.push({
1068
+ type: "add-file",
1069
+ path: "biome.json",
1070
+ description: "Add biome.json config",
1071
+ content: JSON.stringify(
1072
+ {
1073
+ $schema: "https://biomejs.dev/schemas/1.9.4/schema.json",
1074
+ vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
1075
+ linter: { enabled: true, rules: { recommended: true } },
1076
+ formatter: { enabled: true }
1077
+ },
1078
+ null,
1079
+ 2
1080
+ )
1081
+ });
1082
+ } else if (toLinter === "biome" && toFormatter !== "biome") {
1083
+ changes.push({
1084
+ type: "add-file",
1085
+ path: "biome.json",
1086
+ description: "Add biome.json config (linter only)",
1087
+ content: JSON.stringify(
1088
+ {
1089
+ $schema: "https://biomejs.dev/schemas/1.9.4/schema.json",
1090
+ vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
1091
+ linter: { enabled: true, rules: { recommended: true } },
1092
+ formatter: { enabled: false }
1093
+ },
1094
+ null,
1095
+ 2
1096
+ )
1097
+ });
1098
+ }
1099
+ if (current.linter === "biome" && toLinter !== "biome" && current.formatter !== "biome" && toFormatter !== "biome") {
1100
+ changes.push({
1101
+ type: "remove-file",
1102
+ path: "biome.json",
1103
+ description: "Remove biome.json"
1104
+ });
1105
+ }
1106
+ }
1107
+ if (toFormatter !== current.formatter) {
1108
+ const formatterSameAsLinter = current.formatter === current.linter;
1109
+ if (current.formatter !== "biome" && !formatterSameAsLinter) {
1110
+ changes.push({
1111
+ type: "remove-dir",
1112
+ path: `.config/${current.formatter}`,
1113
+ description: `Remove @config/${current.formatter} package`
1114
+ });
1115
+ }
1116
+ const newFormatterSameAsLinter = toFormatter === toLinter;
1117
+ if (toFormatter !== "biome" && !newFormatterSameAsLinter) {
1118
+ const files = {};
1119
+ if (toFormatter === "oxfmt") {
1120
+ generateOxfmtConfigPackage(files);
1121
+ } else if (toFormatter === "prettier") {
1122
+ generatePrettierConfigPackage(files);
1123
+ }
1124
+ for (const [path, file] of Object.entries(files)) {
1125
+ if (file.type === "text") {
1126
+ changes.push({
1127
+ type: "add-file",
1128
+ path,
1129
+ description: `Add ${path}`,
1130
+ content: file.content
1131
+ });
1132
+ }
1133
+ }
1134
+ }
1135
+ if (toFormatter === "biome" && toLinter !== "biome") {
1136
+ changes.push({
1137
+ type: "add-file",
1138
+ path: "biome.json",
1139
+ description: "Add biome.json config (formatter only)",
1140
+ content: JSON.stringify(
1141
+ {
1142
+ $schema: "https://biomejs.dev/schemas/1.9.4/schema.json",
1143
+ vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
1144
+ linter: { enabled: false },
1145
+ formatter: { enabled: true }
1146
+ },
1147
+ null,
1148
+ 2
1149
+ )
1150
+ });
1151
+ }
1152
+ if (current.formatter === "biome" && toFormatter !== "biome" && current.linter !== "biome" && toLinter !== "biome") {
1153
+ changes.push({
1154
+ type: "remove-file",
1155
+ path: "biome.json",
1156
+ description: "Remove biome.json"
1157
+ });
1158
+ }
1159
+ }
1160
+ changes.push({
1161
+ type: "update-package-json",
1162
+ path: "package.json",
1163
+ description: "Update root package.json (devDependencies, scripts)"
1164
+ });
1165
+ const subPackageUpdates = await getSubPackageUpdates(
1166
+ root,
1167
+ current,
1168
+ toLinter,
1169
+ toFormatter
1170
+ );
1171
+ return {
1172
+ fromLinter: current.linter,
1173
+ toLinter,
1174
+ fromFormatter: current.formatter,
1175
+ toFormatter,
1176
+ changes,
1177
+ subPackageUpdates
1178
+ };
1179
+ }
1180
+ async function getSubPackageUpdates(root, current, toLinter, toFormatter) {
1181
+ const updates = [];
1182
+ const workspacePath = join(root, "pnpm-workspace.yaml");
1183
+ let workspaceContent;
1184
+ try {
1185
+ workspaceContent = await readFile(workspacePath, "utf-8");
1186
+ } catch {
1187
+ return updates;
1188
+ }
1189
+ const packageGlobs = parseWorkspaceYamlContent(workspaceContent);
1190
+ for (const glob of packageGlobs) {
1191
+ if (glob.includes(".config")) continue;
1192
+ const baseDir = glob.replace(/\/\*$/, "").replace(/^["']|["']$/g, "");
1193
+ const basePath = join(root, baseDir);
1194
+ try {
1195
+ const entries = await readdir(basePath, { withFileTypes: true });
1196
+ for (const entry of entries) {
1197
+ if (!entry.isDirectory()) continue;
1198
+ const pkgJsonPath = join(basePath, entry.name, "package.json");
1199
+ try {
1200
+ const content = await readFile(pkgJsonPath, "utf-8");
1201
+ const pkg = JSON.parse(content);
1202
+ const devDeps = pkg.devDependencies ?? {};
1203
+ const remove = [];
1204
+ const add = [];
1205
+ const oldLinterPkg = LINTER_CONFIG_PACKAGES[current.linter];
1206
+ const newLinterPkg = LINTER_CONFIG_PACKAGES[toLinter];
1207
+ if (oldLinterPkg && oldLinterPkg !== newLinterPkg && devDeps[oldLinterPkg]) {
1208
+ remove.push(oldLinterPkg);
1209
+ }
1210
+ if (newLinterPkg && newLinterPkg !== oldLinterPkg && oldLinterPkg && devDeps[oldLinterPkg]) {
1211
+ add.push(newLinterPkg);
1212
+ }
1213
+ if (current.formatter !== current.linter) {
1214
+ const oldFormatterPkg = FORMATTER_CONFIG_PACKAGES[current.formatter];
1215
+ const newFormatterPkg = FORMATTER_CONFIG_PACKAGES[toFormatter];
1216
+ if (oldFormatterPkg && oldFormatterPkg !== newFormatterPkg && devDeps[oldFormatterPkg]) {
1217
+ remove.push(oldFormatterPkg);
1218
+ }
1219
+ if (newFormatterPkg && newFormatterPkg !== oldFormatterPkg && oldFormatterPkg && devDeps[oldFormatterPkg]) {
1220
+ add.push(newFormatterPkg);
1221
+ }
1222
+ }
1223
+ if (remove.length > 0 || add.length > 0) {
1224
+ updates.push({
1225
+ path: join(baseDir, entry.name, "package.json"),
1226
+ remove,
1227
+ add
1228
+ });
1229
+ }
1230
+ } catch {
1231
+ }
1232
+ }
1233
+ } catch {
1234
+ }
1235
+ }
1236
+ return updates;
1237
+ }
1238
+ async function applyMigration(plan, root) {
1239
+ for (const change of plan.changes) {
1240
+ if (change.type === "remove-dir") {
1241
+ const fullPath = join(root, change.path);
1242
+ try {
1243
+ await rm(fullPath, { recursive: true });
1244
+ } catch {
1245
+ }
1246
+ }
1247
+ }
1248
+ for (const change of plan.changes) {
1249
+ if (change.type === "remove-file") {
1250
+ const fullPath = join(root, change.path);
1251
+ try {
1252
+ await rm(fullPath);
1253
+ } catch {
1254
+ }
1255
+ }
1256
+ }
1257
+ for (const change of plan.changes) {
1258
+ if (change.type === "add-file" && change.content) {
1259
+ const fullPath = join(root, change.path);
1260
+ await mkdir(dirname(fullPath), { recursive: true });
1261
+ await writeFile(fullPath, change.content);
1262
+ }
1263
+ }
1264
+ await updateRootPackageJson(root, plan);
1265
+ for (const update of plan.subPackageUpdates) {
1266
+ await updateSubPackageJson(root, update);
1267
+ }
1268
+ }
1269
+ async function updateRootPackageJson(root, plan) {
1270
+ const pkgPath = join(root, "package.json");
1271
+ const content = await readFile(pkgPath, "utf-8");
1272
+ const pkg = JSON.parse(content);
1273
+ const devDeps = pkg.devDependencies ?? {};
1274
+ const oldLinterDep = LINTER_DEPS[plan.fromLinter];
1275
+ delete devDeps[oldLinterDep];
1276
+ if (plan.fromFormatter !== plan.fromLinter) {
1277
+ const oldFormatterDep = FORMATTER_DEPS[plan.fromFormatter];
1278
+ delete devDeps[oldFormatterDep];
1279
+ }
1280
+ const newLinterDep = LINTER_DEPS[plan.toLinter];
1281
+ if (plan.toLinter === "oxlint") {
1282
+ devDeps[newLinterDep] = "^1.36.0";
1283
+ } else if (plan.toLinter === "eslint") {
1284
+ devDeps[newLinterDep] = "^9.17.0";
1285
+ } else if (plan.toLinter === "biome") {
1286
+ devDeps[newLinterDep] = "^1.9.4";
1287
+ }
1288
+ if (plan.toFormatter !== plan.toLinter) {
1289
+ const newFormatterDep = FORMATTER_DEPS[plan.toFormatter];
1290
+ if (plan.toFormatter === "oxfmt") {
1291
+ devDeps[newFormatterDep] = "^0.21.0";
1292
+ } else if (plan.toFormatter === "prettier") {
1293
+ devDeps[newFormatterDep] = "^3.4.2";
1294
+ } else if (plan.toFormatter === "biome") {
1295
+ devDeps[newFormatterDep] = "^1.9.4";
1296
+ }
1297
+ }
1298
+ pkg.devDependencies = Object.fromEntries(
1299
+ Object.entries(devDeps).sort(([a], [b]) => a.localeCompare(b))
1300
+ );
1301
+ const scripts = pkg.scripts ?? {};
1302
+ if (plan.toLinter === "oxlint") {
1303
+ scripts.lint = "oxlint .";
1304
+ } else if (plan.toLinter === "eslint") {
1305
+ scripts.lint = "eslint .";
1306
+ } else if (plan.toLinter === "biome") {
1307
+ scripts.lint = "biome check .";
1308
+ }
1309
+ if (plan.toFormatter === "oxfmt") {
1310
+ scripts.format = "oxfmt .";
1311
+ } else if (plan.toFormatter === "prettier") {
1312
+ scripts.format = "prettier --write .";
1313
+ } else if (plan.toFormatter === "biome") {
1314
+ scripts.format = "biome format . --write";
1315
+ }
1316
+ pkg.scripts = scripts;
1317
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1318
+ }
1319
+ async function updateSubPackageJson(root, update) {
1320
+ const pkgPath = join(root, update.path);
1321
+ const content = await readFile(pkgPath, "utf-8");
1322
+ const pkg = JSON.parse(content);
1323
+ const devDeps = pkg.devDependencies ?? {};
1324
+ for (const dep of update.remove) {
1325
+ delete devDeps[dep];
1326
+ }
1327
+ for (const dep of update.add) {
1328
+ devDeps[dep] = "workspace:*";
1329
+ }
1330
+ pkg.devDependencies = Object.fromEntries(
1331
+ Object.entries(devDeps).sort(([a], [b]) => a.localeCompare(b))
1332
+ );
1333
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1334
+ }
1335
+ function formatMigrationChange(change) {
1336
+ const icon = change.type === "remove-dir" || change.type === "remove-file" ? "-" : change.type === "add-file" ? "+" : "~";
1337
+ return ` ${icon} ${change.description}`;
1338
+ }
1339
+
702
1340
  const require$1 = createRequire(import.meta.url);
703
1341
  const pkg = require$1("../package.json");
1342
+ const META_OPTIONS = [
1343
+ "clearConfig",
1344
+ "configPath",
1345
+ "check",
1346
+ "fix",
1347
+ "update",
1348
+ "yes",
1349
+ "workspace",
1350
+ "path",
1351
+ "dir"
1352
+ ];
1353
+ function hasConfigOptions(options) {
1354
+ return Object.keys(options).some(
1355
+ (key) => !META_OPTIONS.includes(key)
1356
+ );
1357
+ }
704
1358
  async function fileExists(path) {
705
1359
  try {
706
1360
  await access(path, constants$1.F_OK);
@@ -709,6 +1363,50 @@ async function fileExists(path) {
709
1363
  return false;
710
1364
  }
711
1365
  }
1366
+ async function promptForAiPlatforms(isNonInteractive) {
1367
+ const savedPlatforms = getAiPlatforms();
1368
+ if (isNonInteractive) {
1369
+ return savedPlatforms ?? ALL_AI_PLATFORMS;
1370
+ }
1371
+ if (savedPlatforms && savedPlatforms.length > 0) {
1372
+ const savedLabels = savedPlatforms.map((plat) => AI_PLATFORM_LABELS[plat]).join(", ");
1373
+ const useDefault = await p.confirm({
1374
+ message: `Add AI rules? ${color.dim(`(${savedLabels})`)}`,
1375
+ initialValue: true
1376
+ });
1377
+ if (p.isCancel(useDefault)) {
1378
+ return [];
1379
+ }
1380
+ if (useDefault) {
1381
+ return savedPlatforms;
1382
+ }
1383
+ }
1384
+ const selected = await p.multiselect({
1385
+ message: "Add AI rules?",
1386
+ options: ALL_AI_PLATFORMS.map((platform) => ({
1387
+ value: platform,
1388
+ label: AI_PLATFORM_LABELS[platform],
1389
+ hint: AI_PLATFORM_HINTS[platform]
1390
+ })),
1391
+ initialValues: [],
1392
+ required: false
1393
+ });
1394
+ if (p.isCancel(selected)) {
1395
+ return [];
1396
+ }
1397
+ const platforms = selected;
1398
+ if (platforms.length === 0) {
1399
+ return [];
1400
+ }
1401
+ const saveChoice = await p.confirm({
1402
+ message: "Save selection for future projects?",
1403
+ initialValue: true
1404
+ });
1405
+ if (!p.isCancel(saveChoice) && saveChoice) {
1406
+ setAiPlatforms(platforms);
1407
+ }
1408
+ return platforms;
1409
+ }
712
1410
  async function writeGeneratedFiles(basePath, files) {
713
1411
  const filePaths = Object.keys(files).sort();
714
1412
  for (const filePath of filePaths) {
@@ -753,15 +1451,39 @@ async function parseWorkspaceDirectories(monorepoRoot) {
753
1451
  return [];
754
1452
  }
755
1453
  }
756
- async function detectWorkspaceTooling(monorepoRoot) {
1454
+ async function detectWorkspaceSettings(monorepoRoot) {
757
1455
  try {
1456
+ const tooling = await detectTooling(monorepoRoot);
758
1457
  const pkgPath = join(monorepoRoot, "package.json");
759
1458
  const content = await readFile(pkgPath, "utf-8");
760
1459
  const pkgJson = JSON.parse(content);
761
- const devDeps = pkgJson.devDependencies ?? {};
762
- const linter = devDeps.oxlint ? "oxlint" : devDeps.eslint ? "eslint" : devDeps["@biomejs/biome"] ? "biome" : void 0;
763
- const formatter = devDeps.oxfmt ? "oxfmt" : devDeps.prettier ? "prettier" : devDeps["@biomejs/biome"] ? "biome" : void 0;
764
- return { linter, formatter };
1460
+ let packageManager;
1461
+ if (pkgJson.packageManager) {
1462
+ packageManager = pkgJson.packageManager.split("@")[0];
1463
+ }
1464
+ let nodeVersion;
1465
+ if (pkgJson.engines?.node) {
1466
+ const match = pkgJson.engines.node.match(/(\d+)/);
1467
+ if (match) {
1468
+ nodeVersion = match[1];
1469
+ }
1470
+ }
1471
+ let pnpmManageVersions;
1472
+ try {
1473
+ const workspaceFile = join(monorepoRoot, "pnpm-workspace.yaml");
1474
+ const workspaceContent = await readFile(workspaceFile, "utf-8");
1475
+ pnpmManageVersions = workspaceContent.includes(
1476
+ "manage-package-manager-versions: true"
1477
+ );
1478
+ } catch {
1479
+ }
1480
+ return {
1481
+ linter: tooling.linter,
1482
+ formatter: tooling.formatter,
1483
+ packageManager,
1484
+ nodeVersion,
1485
+ pnpmManageVersions
1486
+ };
765
1487
  } catch {
766
1488
  return {};
767
1489
  }
@@ -800,29 +1522,26 @@ async function getMonorepoScope(monorepoRoot) {
800
1522
  }
801
1523
  async function getWorkspacePackages(monorepoRoot) {
802
1524
  const packagesDir = join(monorepoRoot, "packages");
803
- const packages = [];
804
1525
  try {
805
1526
  const { readdir } = await import('fs/promises');
806
1527
  const entries = await readdir(packagesDir, { withFileTypes: true });
1528
+ const names = [];
807
1529
  for (const entry of entries) {
808
- if (entry.isDirectory()) {
809
- try {
810
- const pkgJsonPath = join(packagesDir, entry.name, "package.json");
811
- const content = await readFile(pkgJsonPath, "utf-8");
812
- const pkgJson = JSON.parse(content);
813
- if (pkgJson.name) {
814
- packages.push({
815
- name: pkgJson.name,
816
- path: `packages/${entry.name}`
817
- });
818
- }
819
- } catch {
820
- }
1530
+ if (!entry.isDirectory()) continue;
1531
+ try {
1532
+ const content = await readFile(
1533
+ join(packagesDir, entry.name, "package.json"),
1534
+ "utf-8"
1535
+ );
1536
+ const pkg2 = JSON.parse(content);
1537
+ if (pkg2.name) names.push(pkg2.name);
1538
+ } catch {
821
1539
  }
822
1540
  }
1541
+ return names;
823
1542
  } catch {
1543
+ return [];
824
1544
  }
825
- return packages;
826
1545
  }
827
1546
  async function ensureConfigInWorkspace(monorepoRoot) {
828
1547
  const workspacePath = join(monorepoRoot, "pnpm-workspace.yaml");
@@ -990,7 +1709,7 @@ Or in \`package.json\`:
990
1709
  content: existingContent
991
1710
  };
992
1711
  }
993
- async function createPackageInWorkspace(monorepoRoot, packageManager, inheritedTooling, scope) {
1712
+ async function createPackageInWorkspace(monorepoRoot, packageManager, inheritedSettings, scope) {
994
1713
  const workspaceDirectories = await parseWorkspaceDirectories(monorepoRoot);
995
1714
  const defaultDirectories = ["apps", "packages"];
996
1715
  const hasCustomDirectories = workspaceDirectories.length > 0 && !workspaceDirectories.every((dir) => defaultDirectories.includes(dir));
@@ -1026,7 +1745,7 @@ async function createPackageInWorkspace(monorepoRoot, packageManager, inheritedT
1026
1745
  const packageOptions = await promptForPackageOptions(
1027
1746
  scopedName,
1028
1747
  packageType,
1029
- inheritedTooling
1748
+ inheritedSettings
1030
1749
  );
1031
1750
  let targetDir = defaultDir;
1032
1751
  if (hasCustomDirectories && workspaceDirectories.length > 0) {
@@ -1104,7 +1823,7 @@ async function createPackageInWorkspace(monorepoRoot, packageManager, inheritedT
1104
1823
  })
1105
1824
  );
1106
1825
  }
1107
- const formatter = packageOptions.formatter ?? "oxfmt";
1826
+ const formatter = packageOptions.formatter ?? "prettier";
1108
1827
  if (formatter === "prettier") {
1109
1828
  versionPromises.push(
1110
1829
  getLatestNpmVersion("prettier", "3.4.2").then((v) => {
@@ -1126,20 +1845,15 @@ async function createPackageInWorkspace(monorepoRoot, packageManager, inheritedT
1126
1845
  }
1127
1846
  await Promise.all(versionPromises);
1128
1847
  packageOptions.versions = versions;
1129
- if (packageType === "app") {
1130
- const workspacePackages = await getWorkspacePackages(monorepoRoot);
1131
- if (workspacePackages.length > 0) {
1132
- const selectedDeps = await p.multiselect({
1133
- message: "Add workspace dependencies?",
1134
- options: workspacePackages.map((pkgInfo) => ({
1135
- value: pkgInfo.name,
1136
- label: pkgInfo.name.replace(/^@[^/]+\//, "")
1137
- })),
1138
- required: false
1139
- });
1140
- if (!p.isCancel(selectedDeps) && selectedDeps.length > 0) {
1141
- packageOptions.workspaceDependencies = selectedDeps;
1142
- }
1848
+ const workspacePackages = packageType === "app" ? await getWorkspacePackages(monorepoRoot) : [];
1849
+ if (workspacePackages.length > 0) {
1850
+ const selectedDeps = await p.multiselect({
1851
+ message: "Add workspace dependencies?",
1852
+ options: workspacePackages.map((name) => ({ value: name, label: name })),
1853
+ required: false
1854
+ });
1855
+ if (!p.isCancel(selectedDeps) && selectedDeps.length > 0) {
1856
+ packageOptions.workspaceDependencies = selectedDeps;
1143
1857
  }
1144
1858
  }
1145
1859
  const outputPath = join(monorepoRoot, relativePkgPath);
@@ -1264,11 +1978,11 @@ async function handleFixCommand(options) {
1264
1978
  console.log(color.dim(` \u2022 ${error}`));
1265
1979
  }
1266
1980
  console.log();
1267
- const tooling = await detectWorkspaceTooling(monorepoRoot);
1981
+ const tooling = await detectWorkspaceSettings(monorepoRoot);
1268
1982
  const existingConfigs = await detectExistingConfigs(monorepoRoot);
1269
1983
  const detectedLinter = tooling.linter ?? existingConfigs.linter ?? "oxlint";
1270
- const detectedFormatter = tooling.formatter ?? existingConfigs.formatter ?? "oxfmt";
1271
- const isNonInteractive = options.linter && options.formatter;
1984
+ const detectedFormatter = tooling.formatter ?? existingConfigs.formatter ?? "prettier";
1985
+ const isNonInteractive = Boolean(options.linter && options.formatter);
1272
1986
  let linter;
1273
1987
  let formatter;
1274
1988
  if (isNonInteractive) {
@@ -1446,85 +2160,346 @@ async function handleFixCommand(options) {
1446
2160
  console.log(color.dim(" Generated .vscode/extensions.json"));
1447
2161
  }
1448
2162
  }
1449
- const aiFilePaths = {
1450
- "cursor-rules": ".cursor/rules",
1451
- "agents-md": "AGENTS.md",
1452
- "claude-md": "CLAUDE.md",
1453
- "copilot-md": ".github/copilot-instructions.md"
1454
- };
1455
- const existingAiFiles = [];
1456
- for (const [choice, path] of Object.entries(aiFilePaths)) {
1457
- if (await fileExists(join(monorepoRoot, path))) {
1458
- existingAiFiles.push(choice);
2163
+ const aiRulesExist = await fileExists(
2164
+ join(monorepoRoot, ".ai/workspace.md")
2165
+ );
2166
+ if (!aiRulesExist) {
2167
+ const platforms = await promptForAiPlatforms(isNonInteractive);
2168
+ if (platforms.length > 0) {
2169
+ const scope = await getMonorepoScope(monorepoRoot);
2170
+ const aiFilesOutput = {};
2171
+ generateAiFiles(aiFilesOutput, {
2172
+ name: scope,
2173
+ packageManager: "pnpm",
2174
+ linter,
2175
+ formatter,
2176
+ isMonorepo: true,
2177
+ platforms
2178
+ });
2179
+ for (const [filePath, file] of Object.entries(aiFilesOutput)) {
2180
+ const fullPath = join(monorepoRoot, filePath);
2181
+ await mkdir(dirname(fullPath), { recursive: true });
2182
+ await writeFile(fullPath, file.content);
2183
+ console.log(color.dim(` Generated ${filePath}`));
2184
+ }
1459
2185
  }
1460
2186
  }
1461
- let selectedAiFiles = [];
1462
- const savedAiFiles = getAiFiles();
1463
- const availableChoices = ["cursor-rules", "agents-md", "claude-md", "copilot-md"].filter((c) => !existingAiFiles.includes(c));
1464
- if (availableChoices.length === 0) {
1465
- } else if (isNonInteractive) {
1466
- const preferred = savedAiFiles ?? ["cursor-rules"];
1467
- selectedAiFiles = preferred.filter((f) => availableChoices.includes(f));
1468
- } else if (savedAiFiles && savedAiFiles.length > 0) {
1469
- const availableSaved = savedAiFiles.filter(
1470
- (f) => availableChoices.includes(f)
2187
+ process.exit(0);
2188
+ } catch (error) {
2189
+ spinner.stop(color.red("\u2717") + " Failed to fix workspace");
2190
+ console.error(error);
2191
+ process.exit(1);
2192
+ }
2193
+ }
2194
+ async function handleMigration(config, target, root, options) {
2195
+ const plan = await getMigrationPlan(config, target, root);
2196
+ console.log(color.cyan("Migration:"));
2197
+ if (plan.fromLinter !== plan.toLinter) {
2198
+ console.log(
2199
+ ` Linter: ${color.dim(plan.fromLinter)} \u2192 ${color.green(plan.toLinter)}`
2200
+ );
2201
+ }
2202
+ if (plan.fromFormatter !== plan.toFormatter) {
2203
+ console.log(
2204
+ ` Formatter: ${color.dim(plan.fromFormatter)} \u2192 ${color.green(
2205
+ plan.toFormatter
2206
+ )}`
2207
+ );
2208
+ }
2209
+ console.log();
2210
+ console.log(color.cyan("Changes:"));
2211
+ for (const change of plan.changes) {
2212
+ console.log(formatMigrationChange(change));
2213
+ }
2214
+ if (plan.subPackageUpdates.length > 0) {
2215
+ console.log();
2216
+ console.log(color.cyan(`Sub-packages (${plan.subPackageUpdates.length}):`));
2217
+ for (const update of plan.subPackageUpdates) {
2218
+ const changes = [
2219
+ ...update.remove.map((d) => `-${d}`),
2220
+ ...update.add.map((d) => `+${d}`)
2221
+ ].join(", ");
2222
+ console.log(` ~ ${update.path} (${changes})`);
2223
+ }
2224
+ }
2225
+ console.log();
2226
+ if (!options.yes) {
2227
+ const confirm = await p.confirm({
2228
+ message: "Apply migration?",
2229
+ initialValue: true
2230
+ });
2231
+ if (p.isCancel(confirm) || !confirm) {
2232
+ console.log(color.dim(" Migration cancelled"));
2233
+ process.exit(0);
2234
+ }
2235
+ }
2236
+ await applyMigration(plan, root);
2237
+ const aiWorkspacePath = join(root, ".ai/workspace.md");
2238
+ const aiRulesExist = await fileExists(aiWorkspacePath);
2239
+ if (aiRulesExist) {
2240
+ console.log();
2241
+ console.log(color.cyan("Updating AI rules..."));
2242
+ const scope = await getMonorepoScope(root);
2243
+ const existingPlatforms = [];
2244
+ if (await fileExists(join(root, "AGENTS.md"))) {
2245
+ existingPlatforms.push("agents");
2246
+ }
2247
+ if (await fileExists(join(root, "CLAUDE.md"))) {
2248
+ existingPlatforms.push("claude");
2249
+ }
2250
+ const aiFilesOutput = {};
2251
+ generateAiFiles(aiFilesOutput, {
2252
+ name: scope,
2253
+ packageManager: "pnpm",
2254
+ linter: plan.toLinter,
2255
+ formatter: plan.toFormatter,
2256
+ isMonorepo: true,
2257
+ platforms: existingPlatforms.length > 0 ? existingPlatforms : ["agents"]
2258
+ });
2259
+ for (const [filePath, file] of Object.entries(aiFilesOutput)) {
2260
+ const fullPath = join(root, filePath);
2261
+ await mkdir(dirname(fullPath), { recursive: true });
2262
+ await writeFile(fullPath, file.content);
2263
+ console.log(color.dim(` ${filePath}`));
2264
+ }
2265
+ }
2266
+ console.log();
2267
+ console.log(
2268
+ color.green("\u2713") + ` Migrated to ${plan.toLinter}/${plan.toFormatter}`
2269
+ );
2270
+ console.log(color.dim(" Run `pnpm install` to update dependencies"));
2271
+ process.exit(0);
2272
+ }
2273
+ async function handleUpdateCommand(options) {
2274
+ const monorepoRoot = await detectMonorepoRoot();
2275
+ if (!monorepoRoot) {
2276
+ console.log(color.red("\u2717") + " Not a monorepo workspace");
2277
+ console.log(color.dim(" --update only supports pnpm monorepos"));
2278
+ process.exit(1);
2279
+ }
2280
+ const { valid, errors } = await validateWorkspace(monorepoRoot);
2281
+ if (!valid) {
2282
+ console.log(color.yellow("!") + " Workspace has issues:");
2283
+ for (const error of errors) {
2284
+ console.log(color.dim(` \u2022 ${error}`));
2285
+ }
2286
+ console.log();
2287
+ const shouldFix = options.yes || await p.confirm({
2288
+ message: "Run fix first to resolve these issues?",
2289
+ initialValue: true
2290
+ });
2291
+ if (p.isCancel(shouldFix) || !shouldFix) {
2292
+ console.log(
2293
+ color.dim(" Run `pnpm create krispya --fix` to fix manually")
1471
2294
  );
1472
- if (availableSaved.length > 0) {
1473
- const savedLabels = availableSaved.map((f) => aiFilePaths[f]).join(", ");
1474
- const useDefault = await p.confirm({
1475
- message: `Generate AI instruction files? ${color.dim(
1476
- `(${savedLabels})`
1477
- )}`,
2295
+ process.exit(1);
2296
+ }
2297
+ const preFixConfig = await detectCurrentConfig(monorepoRoot);
2298
+ const fixOptions = {
2299
+ ...options,
2300
+ linter: options.linter ?? preFixConfig.linter,
2301
+ formatter: options.formatter ?? preFixConfig.formatter
2302
+ };
2303
+ await handleFixCommand(fixOptions);
2304
+ }
2305
+ const config = await detectCurrentConfig(monorepoRoot);
2306
+ const targetLinter = options.linter;
2307
+ const targetFormatter = options.formatter;
2308
+ const migrationTarget = { linter: targetLinter, formatter: targetFormatter };
2309
+ if (needsMigration(config, migrationTarget)) {
2310
+ await handleMigration(config, migrationTarget, monorepoRoot, options);
2311
+ return;
2312
+ }
2313
+ console.log(
2314
+ color.cyan("Checking for updates...") + color.dim(` (${config.linter}/${config.formatter})`)
2315
+ );
2316
+ console.log();
2317
+ const expected = generateExpectedFiles(config);
2318
+ const categories = await compareWithDisk(expected, monorepoRoot);
2319
+ const workspaceConfigChanges = await getWorkspaceConfigUpdates(monorepoRoot);
2320
+ const workspaceCategory = {
2321
+ category: "workspace-config",
2322
+ label: "Workspace Config",
2323
+ changes: workspaceConfigChanges,
2324
+ hasUserModifications: workspaceConfigChanges.some(
2325
+ (c) => c.status === "modified"
2326
+ )
2327
+ };
2328
+ const allCategories = categories.filter(
2329
+ (c) => c.category !== "workspace-config"
2330
+ );
2331
+ if (workspaceConfigChanges.length > 0) {
2332
+ const configPkgIndex = allCategories.findIndex(
2333
+ (c) => c.category === "config-packages"
2334
+ );
2335
+ if (configPkgIndex !== -1) {
2336
+ allCategories.splice(configPkgIndex + 1, 0, workspaceCategory);
2337
+ } else {
2338
+ allCategories.push(workspaceCategory);
2339
+ }
2340
+ }
2341
+ let updatedCount = 0;
2342
+ let skippedCount = 0;
2343
+ for (const category of allCategories) {
2344
+ const newChanges = category.changes.filter((c) => c.status === "added");
2345
+ const modifiedChanges = category.changes.filter(
2346
+ (c) => c.status === "modified"
2347
+ );
2348
+ const hasNew = newChanges.length > 0;
2349
+ const hasModified = modifiedChanges.length > 0;
2350
+ const hasChanges = hasNew || hasModified;
2351
+ if (!hasChanges) {
2352
+ console.log(color.green("\u2713") + ` ${category.label}: Up to date`);
2353
+ continue;
2354
+ }
2355
+ if (category.category === "ai-files") {
2356
+ if (hasNew) {
2357
+ console.log(color.cyan(category.label + ":"));
2358
+ console.log(
2359
+ color.dim(` ${newChanges.length} AI file(s) can be added`)
2360
+ );
2361
+ console.log();
2362
+ const applyAi = options.yes ? true : await p.confirm({
2363
+ message: "Add AI rules?",
1478
2364
  initialValue: true
1479
2365
  });
1480
- if (!p.isCancel(useDefault) && useDefault) {
1481
- selectedAiFiles = availableSaved;
2366
+ if (!p.isCancel(applyAi) && applyAi) {
2367
+ await applyUpdates(newChanges, monorepoRoot);
2368
+ console.log(
2369
+ color.green("\u2713") + ` Added ${newChanges.length} AI file(s)`
2370
+ );
2371
+ updatedCount++;
2372
+ } else {
2373
+ console.log(color.dim(` Skipped ${category.label}`));
2374
+ skippedCount++;
1482
2375
  }
1483
2376
  }
1484
- } else {
1485
- const aiFilesChoice = await p.multiselect({
1486
- message: "Generate AI instruction files?",
1487
- options: availableChoices.map((c) => ({
1488
- value: c,
1489
- label: aiFilePaths[c],
1490
- hint: c === "cursor-rules" ? "Cursor AI" : c === "agents-md" ? "GitHub Copilot, general" : c === "claude-md" ? "Claude" : "GitHub Copilot"
2377
+ if (hasModified) {
2378
+ console.log(color.cyan("AI Files (existing):"));
2379
+ for (const change of modifiedChanges) {
2380
+ console.log(formatFileChange(change));
2381
+ }
2382
+ console.log();
2383
+ if (options.yes) {
2384
+ console.log(color.dim(" (--yes mode: keeping existing AI files)"));
2385
+ } else {
2386
+ const updateExisting = await p.confirm({
2387
+ message: "Update existing AI files to latest template?",
2388
+ initialValue: false
2389
+ });
2390
+ if (!p.isCancel(updateExisting) && updateExisting) {
2391
+ await applyUpdates(modifiedChanges, monorepoRoot);
2392
+ console.log(color.green("\u2713") + " Updated existing AI files");
2393
+ }
2394
+ }
2395
+ }
2396
+ console.log();
2397
+ continue;
2398
+ }
2399
+ let changesToApply = [];
2400
+ if (options.yes) {
2401
+ console.log(color.cyan(category.label + ":"));
2402
+ for (const change of [...newChanges, ...modifiedChanges]) {
2403
+ console.log(formatFileChange(change));
2404
+ }
2405
+ console.log();
2406
+ if (category.category === "workspace-config") {
2407
+ changesToApply = [...newChanges, ...modifiedChanges];
2408
+ if (changesToApply.length > 0) {
2409
+ console.log(color.dim(" (--yes mode: applying merge updates)"));
2410
+ }
2411
+ } else {
2412
+ changesToApply = newChanges;
2413
+ if (newChanges.length > 0) {
2414
+ console.log(color.dim(" (--yes mode: adding new files only)"));
2415
+ }
2416
+ }
2417
+ } else if (hasNew && hasModified) {
2418
+ const allChanges = [...newChanges, ...modifiedChanges];
2419
+ const selectedFiles = await p.multiselect({
2420
+ message: `${category.label} (+ new, ~ changed)`,
2421
+ options: allChanges.map((change) => ({
2422
+ value: change.path,
2423
+ label: change.status === "added" ? `+ ${change.path}` : `~ ${change.path}`
1491
2424
  })),
2425
+ initialValues: newChanges.map((c) => c.path),
2426
+ // Pre-select new files
1492
2427
  required: false
1493
2428
  });
1494
- if (!p.isCancel(aiFilesChoice) && aiFilesChoice.length > 0) {
1495
- selectedAiFiles = aiFilesChoice;
1496
- const saveChoice = await p.confirm({
1497
- message: "Save as default for future?",
1498
- initialValue: true
1499
- });
1500
- if (!p.isCancel(saveChoice) && saveChoice) {
1501
- setAiFiles(selectedAiFiles);
1502
- }
2429
+ if (p.isCancel(selectedFiles)) {
2430
+ p.cancel("Operation cancelled.");
2431
+ process.exit(0);
1503
2432
  }
1504
- }
1505
- if (selectedAiFiles.length > 0) {
1506
- const scope = await getMonorepoScope(monorepoRoot);
1507
- const aiFilesOutput = {};
1508
- generateAiFiles(aiFilesOutput, {
1509
- name: scope,
1510
- packageManager: "pnpm",
1511
- linter,
1512
- formatter,
1513
- aiFiles: selectedAiFiles
2433
+ if (selectedFiles.length > 0) {
2434
+ changesToApply = allChanges.filter(
2435
+ (c) => selectedFiles.includes(c.path)
2436
+ );
2437
+ }
2438
+ } else if (hasNew) {
2439
+ console.log(color.cyan(category.label + ":"));
2440
+ for (const change of newChanges) {
2441
+ console.log(formatFileChange(change));
2442
+ }
2443
+ console.log();
2444
+ const shouldAdd = await p.confirm({
2445
+ message: `Add ${newChanges.length} new file(s)?`,
2446
+ initialValue: true
1514
2447
  });
1515
- for (const [filePath, file] of Object.entries(aiFilesOutput)) {
1516
- const fullPath = join(monorepoRoot, filePath);
1517
- await mkdir(dirname(fullPath), { recursive: true });
1518
- await writeFile(fullPath, file.content);
1519
- console.log(color.dim(` Generated ${filePath}`));
2448
+ if (p.isCancel(shouldAdd)) {
2449
+ p.cancel("Operation cancelled.");
2450
+ process.exit(0);
2451
+ }
2452
+ if (shouldAdd) {
2453
+ changesToApply = newChanges;
2454
+ }
2455
+ } else if (hasModified) {
2456
+ console.log(color.cyan(category.label + ":"));
2457
+ for (const change of modifiedChanges) {
2458
+ console.log(formatFileChange(change));
2459
+ }
2460
+ console.log();
2461
+ const shouldUpdate = await p.confirm({
2462
+ message: `Update ${modifiedChanges.length} file(s)? (will overwrite)`,
2463
+ initialValue: false
2464
+ });
2465
+ if (p.isCancel(shouldUpdate)) {
2466
+ p.cancel("Operation cancelled.");
2467
+ process.exit(0);
2468
+ }
2469
+ if (shouldUpdate) {
2470
+ changesToApply = modifiedChanges;
1520
2471
  }
1521
2472
  }
1522
- process.exit(0);
1523
- } catch (error) {
1524
- spinner.stop(color.red("\u2717") + " Failed to fix workspace");
1525
- console.error(error);
1526
- process.exit(1);
2473
+ if (changesToApply.length > 0) {
2474
+ await applyUpdates(changesToApply, monorepoRoot);
2475
+ const addedCount = changesToApply.filter(
2476
+ (c) => c.status === "added"
2477
+ ).length;
2478
+ const updatedFilesCount = changesToApply.filter(
2479
+ (c) => c.status === "modified"
2480
+ ).length;
2481
+ const parts = [];
2482
+ if (addedCount > 0) parts.push(`added ${addedCount}`);
2483
+ if (updatedFilesCount > 0) parts.push(`updated ${updatedFilesCount}`);
2484
+ console.log(color.green("\u2713") + ` ${category.label}: ${parts.join(", ")}`);
2485
+ updatedCount++;
2486
+ } else {
2487
+ console.log(color.dim(` Skipped ${category.label}`));
2488
+ skippedCount++;
2489
+ }
2490
+ console.log();
1527
2491
  }
2492
+ if (updatedCount === 0 && skippedCount === 0) {
2493
+ console.log(color.green("\u2713") + " Everything is up to date!");
2494
+ } else if (updatedCount > 0) {
2495
+ console.log(
2496
+ color.green("\u2713") + ` Updated ${updatedCount} ${updatedCount === 1 ? "category" : "categories"}`
2497
+ );
2498
+ if (skippedCount > 0) {
2499
+ console.log(color.dim(` Skipped ${skippedCount}`));
2500
+ }
2501
+ }
2502
+ process.exit(0);
1528
2503
  }
1529
2504
  async function handleWorkspaceCommand(name, options) {
1530
2505
  const monorepoRoot = await detectMonorepoRoot();
@@ -1546,7 +2521,7 @@ async function handleWorkspaceCommand(name, options) {
1546
2521
  process.exit(1);
1547
2522
  }
1548
2523
  const scope = await getMonorepoScope(monorepoRoot);
1549
- const inheritedTooling = await detectWorkspaceTooling(monorepoRoot);
2524
+ const inheritedSettings = await detectWorkspaceSettings(monorepoRoot);
1550
2525
  const projectType = options.type ?? "app";
1551
2526
  const defaultDir = projectType === "library" ? "packages" : "apps";
1552
2527
  const targetDir = options.dir ?? defaultDir;
@@ -1572,8 +2547,11 @@ async function handleWorkspaceCommand(name, options) {
1572
2547
  })
1573
2548
  );
1574
2549
  }
1575
- const linter = inheritedTooling.linter ?? options.linter ?? "oxlint";
1576
- const formatter = inheritedTooling.formatter ?? options.formatter ?? "oxfmt";
2550
+ const linter = inheritedSettings.linter ?? options.linter ?? "oxlint";
2551
+ const formatter = inheritedSettings.formatter ?? options.formatter ?? "prettier";
2552
+ const packageManager = inheritedSettings.packageManager ?? "pnpm";
2553
+ const nodeVersion = inheritedSettings.nodeVersion ?? "latest";
2554
+ const pnpmManageVersions = inheritedSettings.pnpmManageVersions ?? true;
1577
2555
  await Promise.all(versionPromises);
1578
2556
  const relativePkgPath = join(targetDir, name);
1579
2557
  const workspaceRoot = calculateWorkspaceRoot(relativePkgPath);
@@ -1584,6 +2562,9 @@ async function handleWorkspaceCommand(name, options) {
1584
2562
  template,
1585
2563
  linter,
1586
2564
  formatter,
2565
+ packageManager,
2566
+ nodeVersion,
2567
+ pnpmManageVersions,
1587
2568
  workspaceRoot,
1588
2569
  versions,
1589
2570
  ...baseTemplate === "r3f" && {
@@ -1617,8 +2598,8 @@ async function handleWorkspaceCommand(name, options) {
1617
2598
  process.exit(1);
1618
2599
  }
1619
2600
  }
1620
- async function handleMonorepoCreation(generateOptions) {
1621
- const { generateMonorepo } = await import('./chunks/index.mjs').then(function (n) { return n.s; });
2601
+ async function handleMonorepoCreation(generateOptions, isNonInteractive) {
2602
+ const { generateMonorepo } = await import('./chunks/index.mjs').then(function (n) { return n.w; });
1622
2603
  const packageManager = generateOptions.packageManager || "pnpm";
1623
2604
  if (packageManager === "pnpm") {
1624
2605
  generateOptions.pnpmVersion = await getLatestPnpmVersion();
@@ -1631,55 +2612,7 @@ async function handleMonorepoCreation(generateOptions) {
1631
2612
  if (nodeVersion === "latest") {
1632
2613
  generateOptions.nodeVersion = await getLatestNodeVersion();
1633
2614
  }
1634
- const savedAiFiles = getAiFiles();
1635
- let selectedAiFiles = [];
1636
- if (savedAiFiles && savedAiFiles.length > 0) {
1637
- const aiFileLabels = {
1638
- "cursor-rules": ".cursor/rules",
1639
- "agents-md": "AGENTS.md",
1640
- "claude-md": "CLAUDE.md",
1641
- "copilot-md": ".github/copilot-instructions.md"
1642
- };
1643
- const savedLabels = savedAiFiles.map((f) => aiFileLabels[f]).join(", ");
1644
- const useDefault = await p.confirm({
1645
- message: `Generate AI instruction files? ${color.dim(
1646
- `(${savedLabels})`
1647
- )}`,
1648
- initialValue: true
1649
- });
1650
- if (!p.isCancel(useDefault) && useDefault) {
1651
- selectedAiFiles = savedAiFiles;
1652
- }
1653
- } else {
1654
- const aiFilesChoice = await p.multiselect({
1655
- message: "Generate AI instruction files?",
1656
- options: [
1657
- { value: "cursor-rules", label: ".cursor/rules", hint: "Cursor AI" },
1658
- {
1659
- value: "agents-md",
1660
- label: "AGENTS.md",
1661
- hint: "GitHub Copilot, general"
1662
- },
1663
- { value: "claude-md", label: "CLAUDE.md", hint: "Claude" },
1664
- {
1665
- value: "copilot-md",
1666
- label: ".github/copilot-instructions.md",
1667
- hint: "GitHub Copilot"
1668
- }
1669
- ],
1670
- required: false
1671
- });
1672
- if (!p.isCancel(aiFilesChoice) && aiFilesChoice.length > 0) {
1673
- selectedAiFiles = aiFilesChoice;
1674
- const saveChoice = await p.confirm({
1675
- message: "Save as default for future monorepos?",
1676
- initialValue: true
1677
- });
1678
- if (!p.isCancel(saveChoice) && saveChoice) {
1679
- setAiFiles(selectedAiFiles);
1680
- }
1681
- }
1682
- }
2615
+ const aiPlatforms = await promptForAiPlatforms(isNonInteractive);
1683
2616
  const projectPath = join(cwd(), generateOptions.name);
1684
2617
  const spinner = p.spinner();
1685
2618
  spinner.start("Creating monorepo workspace...");
@@ -1687,12 +2620,12 @@ async function handleMonorepoCreation(generateOptions) {
1687
2620
  const { files } = generateMonorepo({
1688
2621
  name: generateOptions.name,
1689
2622
  linter: generateOptions.linter ?? "oxlint",
1690
- formatter: generateOptions.formatter ?? "oxfmt",
2623
+ formatter: generateOptions.formatter ?? "prettier",
1691
2624
  packageManager,
1692
2625
  pnpmVersion: generateOptions.pnpmVersion,
1693
2626
  pnpmManageVersions: generateOptions.pnpmManageVersions,
1694
2627
  nodeVersion: generateOptions.nodeVersion,
1695
- aiFiles: selectedAiFiles.length > 0 ? selectedAiFiles : void 0
2628
+ aiPlatforms: aiPlatforms.length > 0 ? aiPlatforms : void 0
1696
2629
  });
1697
2630
  const filePaths = Object.keys(files).sort();
1698
2631
  for (const filePath of filePaths) {
@@ -1704,9 +2637,15 @@ async function handleMonorepoCreation(generateOptions) {
1704
2637
  }
1705
2638
  }
1706
2639
  spinner.stop(color.green.inverse(" \u2713 Monorepo workspace created! "));
1707
- const newMonorepoTooling = {
2640
+ if (isNonInteractive) {
2641
+ process.exit(0);
2642
+ }
2643
+ const newWorkspaceSettings = {
1708
2644
  linter: generateOptions.linter,
1709
- formatter: generateOptions.formatter
2645
+ formatter: generateOptions.formatter,
2646
+ packageManager,
2647
+ nodeVersion: generateOptions.nodeVersion,
2648
+ pnpmManageVersions: generateOptions.pnpmManageVersions
1710
2649
  };
1711
2650
  const scope = generateOptions.name;
1712
2651
  let addMore = true;
@@ -1714,7 +2653,7 @@ async function handleMonorepoCreation(generateOptions) {
1714
2653
  addMore = await createPackageInWorkspace(
1715
2654
  projectPath,
1716
2655
  packageManager,
1717
- newMonorepoTooling,
2656
+ newWorkspaceSettings,
1718
2657
  scope
1719
2658
  );
1720
2659
  }
@@ -1733,10 +2672,14 @@ async function handleMonorepoCreation(generateOptions) {
1733
2672
  process.exit(1);
1734
2673
  }
1735
2674
  }
1736
- async function handleStandaloneProjectCreation(generateOptions) {
2675
+ async function handleStandaloneProjectCreation(generateOptions, isNonInteractive) {
1737
2676
  const base = generateOptions.template ? getBaseTemplate(generateOptions.template) : "vanilla";
1738
2677
  const defaultFallbackName = base === "vanilla" ? "vanilla-app" : base === "react" ? "react-app" : "react-three-app";
1739
2678
  generateOptions.name ??= defaultFallbackName;
2679
+ const aiPlatforms = await promptForAiPlatforms(isNonInteractive);
2680
+ if (aiPlatforms.length > 0) {
2681
+ generateOptions.aiPlatforms = aiPlatforms;
2682
+ }
1740
2683
  const packageManager = generateOptions.packageManager || "pnpm";
1741
2684
  if (packageManager === "pnpm") {
1742
2685
  generateOptions.pnpmVersion = await getLatestPnpmVersion();
@@ -1787,7 +2730,7 @@ async function handleStandaloneProjectCreation(generateOptions) {
1787
2730
  })
1788
2731
  );
1789
2732
  }
1790
- const formatter = generateOptions.formatter ?? "oxfmt";
2733
+ const formatter = generateOptions.formatter ?? "prettier";
1791
2734
  if (formatter === "prettier") {
1792
2735
  versionPromises.push(
1793
2736
  getLatestNpmVersion("prettier", "3.4.2").then((v) => {
@@ -1816,6 +2759,9 @@ async function handleStandaloneProjectCreation(generateOptions) {
1816
2759
  const files = generate(generateOptions);
1817
2760
  await writeGeneratedFiles(projectPath, files);
1818
2761
  spinner.stop(color.green.inverse(" \u2713 Project created! "));
2762
+ if (isNonInteractive) {
2763
+ process.exit(0);
2764
+ }
1819
2765
  const nextSteps = isLibrary ? [
1820
2766
  `cd ${generateOptions.name}`,
1821
2767
  `${packageManager} install`,
@@ -1848,21 +2794,23 @@ async function handleInteractiveMonorepoMode(monorepoRoot) {
1848
2794
  process.exit(0);
1849
2795
  }
1850
2796
  if (choice === "add") {
1851
- const inheritedTooling = await detectWorkspaceTooling(monorepoRoot);
1852
- if (inheritedTooling.linter || inheritedTooling.formatter) {
1853
- const toolingInfo = [
1854
- inheritedTooling.linter && `linter: ${inheritedTooling.linter}`,
1855
- inheritedTooling.formatter && `formatter: ${inheritedTooling.formatter}`
2797
+ const inheritedSettings = await detectWorkspaceSettings(monorepoRoot);
2798
+ const hasSettings = Object.values(inheritedSettings).some(Boolean);
2799
+ if (hasSettings) {
2800
+ const settingsInfo = [
2801
+ inheritedSettings.linter && `linter: ${inheritedSettings.linter}`,
2802
+ inheritedSettings.formatter && `formatter: ${inheritedSettings.formatter}`,
2803
+ inheritedSettings.packageManager && `pm: ${inheritedSettings.packageManager}`
1856
2804
  ].filter(Boolean).join(", ");
1857
- p.log.info(`Using workspace tooling (${toolingInfo})`);
2805
+ p.log.info(`Using workspace settings (${settingsInfo})`);
1858
2806
  }
1859
2807
  const scope = await getMonorepoScope(monorepoRoot);
1860
2808
  let addMore = true;
1861
2809
  while (addMore) {
1862
2810
  addMore = await createPackageInWorkspace(
1863
2811
  monorepoRoot,
1864
- "pnpm",
1865
- inheritedTooling,
2812
+ inheritedSettings.packageManager ?? "pnpm",
2813
+ inheritedSettings,
1866
2814
  scope
1867
2815
  );
1868
2816
  }
@@ -1889,7 +2837,7 @@ async function main() {
1889
2837
  "linter: eslint, oxlint, or biome (default: oxlint)"
1890
2838
  ).option(
1891
2839
  "--formatter <type>",
1892
- "formatter: prettier, oxfmt, or biome (default: oxfmt)"
2840
+ "formatter: prettier, oxfmt, or biome (default: prettier)"
1893
2841
  ).option("--drei", "add @react-three/drei (r3f only)").option("--handle", "add @react-three/handle (r3f only)").option("--leva", "add leva (r3f only)").option("--postprocessing", "add @react-three/postprocessing (r3f only)").option("--rapier", "add @react-three/rapier (r3f only)").option("--xr", "add @react-three/xr (r3f only)").option("--uikit", "add @react-three/uikit (r3f only)").option("--offscreen", "add @react-three/offscreen (r3f only)").option("--zustand", "add zustand (r3f only)").option("--koota", "add koota (r3f only)").option("--triplex", "set up triplex development environment (r3f only)").option("--viverse", "set up viverse deployment (r3f only)").option(
1894
2842
  "--package-manager <manager>",
1895
2843
  "specify package manager (e.g. npm, yarn, pnpm)"
@@ -1911,7 +2859,7 @@ async function main() {
1911
2859
  ).option("--clear-config", "Clear saved preferences (e.g. editor choice)").option("--config-path", "Print the path to the config file").option(
1912
2860
  "--check",
1913
2861
  "Check if current directory is in a valid monorepo workspace"
1914
- ).option("--fix", "Fix monorepo by generating missing .config packages").option(
2862
+ ).option("--fix", "Fix monorepo by generating missing .config packages").option("--update", "Update monorepo workspace to latest configuration").option("-y, --yes", "Non-interactive mode - accept all prompts").option(
1915
2863
  "--path <directory>",
1916
2864
  "Run in specified directory instead of current working directory"
1917
2865
  ).action(async (name, options) => {
@@ -1950,6 +2898,12 @@ async function main() {
1950
2898
  case "--fix":
1951
2899
  options.fix = true;
1952
2900
  break;
2901
+ case "--update":
2902
+ options.update = true;
2903
+ break;
2904
+ case "--yes":
2905
+ options.yes = true;
2906
+ break;
1953
2907
  default:
1954
2908
  console.error(color.red(`Unknown option: ${name}`));
1955
2909
  process.exit(1);
@@ -1961,6 +2915,9 @@ async function main() {
1961
2915
  if (options.fix) {
1962
2916
  await handleFixCommand(options);
1963
2917
  }
2918
+ if (options.update) {
2919
+ await handleUpdateCommand(options);
2920
+ }
1964
2921
  if (options.dir && !options.workspace) {
1965
2922
  console.error(color.red("Error:") + " --dir requires --workspace flag");
1966
2923
  console.log(
@@ -1976,11 +2933,11 @@ async function main() {
1976
2933
  console.clear();
1977
2934
  p.intro(color.bgCyan(color.black(` create-krispya v${pkg.version} `)));
1978
2935
  const monorepoRoot = await detectMonorepoRoot();
1979
- if (monorepoRoot && Object.keys(options).length === 0) {
2936
+ if (monorepoRoot && !hasConfigOptions(options)) {
1980
2937
  await handleInteractiveMonorepoMode(monorepoRoot);
1981
2938
  }
1982
2939
  let generateOptions;
1983
- if (Object.keys(options).length > 0) {
2940
+ if (options.yes) {
1984
2941
  const template = options.template ?? "vanilla";
1985
2942
  const baseTemplate = getBaseTemplate(template);
1986
2943
  const defaultName = getDefaultProjectName(template);
@@ -1991,7 +2948,7 @@ async function main() {
1991
2948
  libraryBundler: projectType === "library" ? options.bundler ?? "unbuild" : void 0,
1992
2949
  template,
1993
2950
  linter: options.linter ?? "oxlint",
1994
- formatter: options.formatter ?? "oxfmt",
2951
+ formatter: options.formatter ?? "prettier",
1995
2952
  ...baseTemplate === "r3f" && {
1996
2953
  drei: options.drei ? {} : void 0,
1997
2954
  handle: options.handle ? {} : void 0,
@@ -2011,12 +2968,35 @@ async function main() {
2011
2968
  nodeVersion: options.nodeVersion ?? "latest"
2012
2969
  };
2013
2970
  } else {
2014
- generateOptions = await promptForOptions(name);
2971
+ const presets = hasConfigOptions(options) ? {
2972
+ type: options.type,
2973
+ template: options.template,
2974
+ bundler: options.bundler,
2975
+ linter: options.linter,
2976
+ formatter: options.formatter,
2977
+ packageManager: options.packageManager,
2978
+ nodeVersion: options.nodeVersion,
2979
+ pnpmManageVersions: options.pnpmManageVersions,
2980
+ drei: options.drei,
2981
+ handle: options.handle,
2982
+ leva: options.leva,
2983
+ postprocessing: options.postprocessing,
2984
+ rapier: options.rapier,
2985
+ xr: options.xr,
2986
+ uikit: options.uikit,
2987
+ offscreen: options.offscreen,
2988
+ zustand: options.zustand,
2989
+ koota: options.koota,
2990
+ triplex: options.triplex,
2991
+ viverse: options.viverse
2992
+ } : void 0;
2993
+ generateOptions = await promptForOptions(name, presets);
2015
2994
  }
2995
+ const isNonInteractive = options.yes ?? false;
2016
2996
  if (generateOptions.projectType === "monorepo") {
2017
- await handleMonorepoCreation(generateOptions);
2997
+ await handleMonorepoCreation(generateOptions, isNonInteractive);
2018
2998
  } else {
2019
- await handleStandaloneProjectCreation(generateOptions);
2999
+ await handleStandaloneProjectCreation(generateOptions, isNonInteractive);
2020
3000
  }
2021
3001
  });
2022
3002
  await program.parseAsync();