create-krispya 0.9.0 → 0.11.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,17 +2,17 @@
2
2
  import * as p from '@clack/prompts';
3
3
  import color from 'chalk';
4
4
  import { Command } from 'commander';
5
- import { constants as constants$2 } from 'node:fs';
6
- import { mkdir as mkdir$1, writeFile as writeFile$1, unlink, access as access$1, readFile as readFile$1 } from 'node:fs/promises';
5
+ import { access as access$1, readFile, writeFile, readdir, mkdir, unlink } from 'node:fs/promises';
7
6
  import { createRequire } from 'node:module';
8
- import { join as join$1, dirname as dirname$1, resolve } from 'node:path';
7
+ import { resolve, join as join$1, dirname } from 'node:path';
9
8
  import { cwd } from 'node:process';
10
9
  import { fetch } from 'undici';
11
- import { g as getEngineName, a as getBaseTemplate, b as getLanguageFromTemplate, c as getPackageManagerName, d as generateRandomName, e as detectTooling, r as resolveMonorepoRootPackageVersions, f as generateAiFiles, A as ALL_AI_PLATFORMS, h as generateVscodeFiles, i as generateTypescriptConfigPackage, j as generateOxlintConfigPackage, k as generateEslintConfigPackage, l as generateOxfmtConfigPackage, m as generatePrettierConfigPackage, n as generateGitignore, o as getResolvedPackageVersion, p as parseWorkspaceYamlContent, q as formatResolvedPackageVersion, s as resolvePackageManager, t as resolveEngine, u as resolveProjectPackageVersions, v as generate, w as parsePackageManager, x as parseEngine, y as AI_PLATFORM_LABELS, z as AI_PLATFORM_HINTS, B as validatePackageName } from './chunks/index.mjs';
10
+ import { q as getEngineName, a as getBaseTemplate, b as getLanguageFromTemplate, s as getPackageManagerName, g as generateRandomName, A as ALL_AI_PLATFORMS, t as AI_PLATFORM_LABELS, x as AI_PLATFORM_HINTS, d as detectTooling, y as parsePackageManager, z as parseEngine, p as parseWorkspaceYamlContent, B as renderTypescriptConfigPackage, C as renderOxlintConfigPackage, D as renderEslintConfigPackage, E as renderOxfmtConfigPackage, F as renderPrettierConfigPackage, G as resolveMonorepoRootPackageVersions, H as getResolvedPackageVersion, I as renderVscodeFiles, J as renderAiFiles, K as renderVscodeFiles$1, L as renderEditorConfig, M as renderGitignore, N as toPrettierIgnoreContent, O as mergePackageJsonScripts, P as renderViteConfig, Q as packageJsonScripts, R as resolveDefaultPackageJsonScripts, S as formatResolvedPackageVersion, T as renderOxlintConfig, k as planProject, r as resolveProjectPlanInput, v as validatePackageName, o as resolveWorkspacePlanInput, l as planWorkspace } from './shared/create-krispya.DblF9gKc.mjs';
12
11
  import Conf from 'conf';
13
- import { readFile, mkdir, writeFile, rm, access, readdir, constants as constants$1 } from 'fs/promises';
14
- import { constants } from 'fs';
15
- import { join, dirname } from 'path';
12
+ import { access, constants, readFile as readFile$1, mkdir as mkdir$1, writeFile as writeFile$1 } from 'fs/promises';
13
+ import { constants as constants$2 } from 'fs';
14
+ import { join, dirname as dirname$1 } from 'path';
15
+ import { constants as constants$1 } from 'node:fs';
16
16
 
17
17
  function formatConfigSummary(options, inherited) {
18
18
  const lines = [];
@@ -69,6 +69,7 @@ function formatConfigSummary(options, inherited) {
69
69
  const testing = options.testing ?? (projectType === "library" ? "vitest" : "none");
70
70
  lines.push(formatRow("Testing", testing));
71
71
  if (!inherited) {
72
+ lines.push(formatRow("Editor config", options.ide === "none" ? "generic" : "generic + vscode"));
72
73
  const configStrategy = options.configStrategy ?? "stealth";
73
74
  lines.push(formatRow("Config strategy", configStrategy));
74
75
  }
@@ -88,7 +89,7 @@ function formatConfigSummary(options, inherited) {
88
89
  options.viverse && "viverse"
89
90
  ].filter(Boolean);
90
91
  lines.push("");
91
- lines.push(color.dim("Integrations"));
92
+ lines.push(color.dim("Features"));
92
93
  for (let i = 0; i < integrationNames.length; i += 2) {
93
94
  const left = `${color.green("\u25CF")} ${integrationNames[i]}`;
94
95
  const right = integrationNames[i + 1] ? `${color.green("\u25CF")} ${integrationNames[i + 1]}` : "";
@@ -117,6 +118,7 @@ function formatMonorepoConfigSummary(options) {
117
118
  }
118
119
  lines.push(formatRow("Linter", options.linter));
119
120
  lines.push(formatRow("Formatter", options.formatter));
121
+ lines.push(formatRow("Editor config", options.ide === "none" ? "generic" : "generic + vscode"));
120
122
  return lines.join("\n");
121
123
  }
122
124
 
@@ -135,11 +137,57 @@ function clearConfig() {
135
137
  function getConfigPath() {
136
138
  return config.path;
137
139
  }
138
- function getCustomTemplates() {
139
- return config.get("customTemplates") ?? {};
140
- }
141
140
 
142
- function getDefaultOptions(template, name, projectType = "app", libraryBundler, integrations, inheritedSettings) {
141
+ const R3F_INTEGRATION_OPTIONS = [
142
+ { value: "drei", label: "Drei" },
143
+ { value: "handle", label: "Handle" },
144
+ { value: "leva", label: "Leva" },
145
+ { value: "postprocessing", label: "Postprocessing" },
146
+ { value: "rapier", label: "Rapier" },
147
+ { value: "xr", label: "XR" },
148
+ { value: "uikit", label: "UIKit" },
149
+ { value: "offscreen", label: "Offscreen" },
150
+ { value: "zustand", label: "Zustand" },
151
+ { value: "koota", label: "Koota" },
152
+ { value: "triplex", label: "Triplex" },
153
+ { value: "viverse", label: "Viverse" }
154
+ ];
155
+ function getR3fIntegrationFlags(features) {
156
+ if (!features) return {};
157
+ return {
158
+ drei: features.includes("drei") ? {} : void 0,
159
+ handle: features.includes("handle") ? {} : void 0,
160
+ leva: features.includes("leva") ? {} : void 0,
161
+ postprocessing: features.includes("postprocessing") ? {} : void 0,
162
+ rapier: features.includes("rapier") ? {} : void 0,
163
+ xr: features.includes("xr") ? {} : void 0,
164
+ uikit: features.includes("uikit") ? {} : void 0,
165
+ offscreen: features.includes("offscreen") ? {} : void 0,
166
+ zustand: features.includes("zustand") ? {} : void 0,
167
+ koota: features.includes("koota") ? {} : void 0,
168
+ triplex: features.includes("triplex") ? {} : void 0,
169
+ viverse: features.includes("viverse") ? {} : void 0
170
+ };
171
+ }
172
+ function getInitialR3fIntegrations(presets) {
173
+ if (!presets) return ["drei"];
174
+ const initialValues = R3F_INTEGRATION_OPTIONS.filter(({ value }) => presets[value]).map(
175
+ ({ value }) => value
176
+ );
177
+ return initialValues.length > 0 ? initialValues : ["drei"];
178
+ }
179
+ async function promptForProceed() {
180
+ const proceed = await p.confirm({
181
+ message: "Proceed with these settings?",
182
+ initialValue: true
183
+ });
184
+ if (p.isCancel(proceed)) {
185
+ p.cancel("Operation cancelled.");
186
+ process.exit(0);
187
+ }
188
+ return proceed;
189
+ }
190
+ function getDefaultOptions(template, name, projectType = "app", libraryBundler, features, inheritedSettings) {
143
191
  const baseTemplate = getBaseTemplate(template);
144
192
  const base = {
145
193
  name,
@@ -153,26 +201,13 @@ function getDefaultOptions(template, name, projectType = "app", libraryBundler,
153
201
  formatter: inheritedSettings?.formatter ?? "prettier",
154
202
  // Libraries get vitest by default, apps don't
155
203
  testing: projectType === "library" ? "vitest" : "none",
156
- configStrategy: getConfigStrategy()
204
+ configStrategy: getConfigStrategy(),
205
+ ide: "vscode"
206
+ };
207
+ return {
208
+ ...base,
209
+ ...baseTemplate === "r3f" ? getR3fIntegrationFlags(features) : {}
157
210
  };
158
- if (baseTemplate === "r3f" && integrations) {
159
- return {
160
- ...base,
161
- drei: integrations.includes("drei") ? {} : void 0,
162
- handle: integrations.includes("handle") ? {} : void 0,
163
- leva: integrations.includes("leva") ? {} : void 0,
164
- postprocessing: integrations.includes("postprocessing") ? {} : void 0,
165
- rapier: integrations.includes("rapier") ? {} : void 0,
166
- xr: integrations.includes("xr") ? {} : void 0,
167
- uikit: integrations.includes("uikit") ? {} : void 0,
168
- offscreen: integrations.includes("offscreen") ? {} : void 0,
169
- zustand: integrations.includes("zustand") ? {} : void 0,
170
- koota: integrations.includes("koota") ? {} : void 0,
171
- triplex: integrations.includes("triplex") ? {} : void 0,
172
- viverse: integrations.includes("viverse") ? {} : void 0
173
- };
174
- }
175
- return base;
176
211
  }
177
212
  function getDefaultProjectName(template) {
178
213
  const base = getBaseTemplate(template);
@@ -186,38 +221,10 @@ function getDefaultProjectName(template) {
186
221
  }
187
222
  }
188
223
  async function promptForR3fIntegrations(presets) {
189
- const initialValues = [];
190
- if (presets) {
191
- if (presets.drei) initialValues.push("drei");
192
- if (presets.handle) initialValues.push("handle");
193
- if (presets.leva) initialValues.push("leva");
194
- if (presets.postprocessing) initialValues.push("postprocessing");
195
- if (presets.rapier) initialValues.push("rapier");
196
- if (presets.xr) initialValues.push("xr");
197
- if (presets.uikit) initialValues.push("uikit");
198
- if (presets.offscreen) initialValues.push("offscreen");
199
- if (presets.zustand) initialValues.push("zustand");
200
- if (presets.koota) initialValues.push("koota");
201
- if (presets.triplex) initialValues.push("triplex");
202
- if (presets.viverse) initialValues.push("viverse");
203
- }
204
224
  const selected = await p.multiselect({
205
- message: "R3F integrations",
206
- options: [
207
- { value: "drei", label: "Drei" },
208
- { value: "handle", label: "Handle" },
209
- { value: "leva", label: "Leva" },
210
- { value: "postprocessing", label: "Postprocessing" },
211
- { value: "rapier", label: "Rapier" },
212
- { value: "xr", label: "XR" },
213
- { value: "uikit", label: "UIKit" },
214
- { value: "offscreen", label: "Offscreen" },
215
- { value: "zustand", label: "Zustand" },
216
- { value: "koota", label: "Koota" },
217
- { value: "triplex", label: "Triplex" },
218
- { value: "viverse", label: "Viverse" }
219
- ],
220
- initialValues: initialValues.length > 0 ? initialValues : ["drei"],
225
+ message: "R3F features",
226
+ options: [...R3F_INTEGRATION_OPTIONS],
227
+ initialValues: getInitialR3fIntegrations(presets),
221
228
  required: false
222
229
  });
223
230
  if (p.isCancel(selected)) {
@@ -226,7 +233,7 @@ async function promptForR3fIntegrations(presets) {
226
233
  }
227
234
  return selected;
228
235
  }
229
- async function promptForCustomization(template, name, projectType, integrations, inheritedSettings, presets) {
236
+ async function promptForCustomization(template, name, projectType, features, inheritedSettings, presets) {
230
237
  let libraryBundler;
231
238
  if (projectType === "library") {
232
239
  const bundler = await p.select({
@@ -361,6 +368,18 @@ async function promptForCustomization(template, name, projectType, integrations,
361
368
  p.cancel("Operation cancelled.");
362
369
  process.exit(0);
363
370
  }
371
+ const ideChoice = await p.select({
372
+ message: "IDE config",
373
+ options: [
374
+ { value: "vscode", label: "vscode" },
375
+ { value: "none", label: "None" }
376
+ ],
377
+ initialValue: presets?.ide ?? "vscode"
378
+ });
379
+ if (p.isCancel(ideChoice)) {
380
+ p.cancel("Operation cancelled.");
381
+ process.exit(0);
382
+ }
364
383
  const baseTemplate = getBaseTemplate(template);
365
384
  const finalTemplate = language === "javascript" ? `${baseTemplate}-js` : baseTemplate;
366
385
  const base = {
@@ -374,26 +393,13 @@ async function promptForCustomization(template, name, projectType, integrations,
374
393
  linter,
375
394
  formatter,
376
395
  testing,
377
- configStrategy: configStrategyChoice
396
+ configStrategy: configStrategyChoice,
397
+ ide: ideChoice
398
+ };
399
+ return {
400
+ ...base,
401
+ ...baseTemplate === "r3f" ? getR3fIntegrationFlags(features) : {}
378
402
  };
379
- if (baseTemplate === "r3f" && integrations) {
380
- return {
381
- ...base,
382
- drei: integrations.includes("drei") ? {} : void 0,
383
- handle: integrations.includes("handle") ? {} : void 0,
384
- leva: integrations.includes("leva") ? {} : void 0,
385
- postprocessing: integrations.includes("postprocessing") ? {} : void 0,
386
- rapier: integrations.includes("rapier") ? {} : void 0,
387
- xr: integrations.includes("xr") ? {} : void 0,
388
- uikit: integrations.includes("uikit") ? {} : void 0,
389
- offscreen: integrations.includes("offscreen") ? {} : void 0,
390
- zustand: integrations.includes("zustand") ? {} : void 0,
391
- koota: integrations.includes("koota") ? {} : void 0,
392
- triplex: integrations.includes("triplex") ? {} : void 0,
393
- viverse: integrations.includes("viverse") ? {} : void 0
394
- };
395
- }
396
- return base;
397
403
  }
398
404
  async function promptForInitialPackage() {
399
405
  const choice = await p.select({
@@ -419,7 +425,8 @@ function getDefaultMonorepoOptions(name) {
419
425
  pnpmManageVersions: true,
420
426
  engine: { name: "node", version: "latest" },
421
427
  linter: "oxlint",
422
- formatter: "prettier"
428
+ formatter: "prettier",
429
+ ide: "vscode"
423
430
  };
424
431
  }
425
432
  async function promptForMonorepoCustomization(name, presets) {
@@ -472,6 +479,18 @@ async function promptForMonorepoCustomization(name, presets) {
472
479
  p.cancel("Operation cancelled.");
473
480
  process.exit(0);
474
481
  }
482
+ const ide = await p.select({
483
+ message: "IDE config",
484
+ options: [
485
+ { value: "vscode", label: "vscode" },
486
+ { value: "none", label: "None" }
487
+ ],
488
+ initialValue: presets?.ide ?? "vscode"
489
+ });
490
+ if (p.isCancel(ide)) {
491
+ p.cancel("Operation cancelled.");
492
+ process.exit(0);
493
+ }
475
494
  return {
476
495
  name,
477
496
  projectType: "monorepo",
@@ -479,7 +498,8 @@ async function promptForMonorepoCustomization(name, presets) {
479
498
  packageManager: { name: "pnpm" },
480
499
  pnpmManageVersions: managePnpm,
481
500
  linter,
482
- formatter
501
+ formatter,
502
+ ide
483
503
  };
484
504
  }
485
505
  async function promptForMonorepo(workspaceName, presets) {
@@ -487,6 +507,7 @@ async function promptForMonorepo(workspaceName, presets) {
487
507
  if (presets) {
488
508
  if (presets.linter) defaultOptions.linter = presets.linter;
489
509
  if (presets.formatter) defaultOptions.formatter = presets.formatter;
510
+ if (presets.ide) defaultOptions.ide = presets.ide;
490
511
  if (presets.engine) defaultOptions.engine = presets.engine;
491
512
  if (presets.pnpmManageVersions !== void 0)
492
513
  defaultOptions.pnpmManageVersions = presets.pnpmManageVersions;
@@ -498,23 +519,12 @@ async function promptForMonorepo(workspaceName, presets) {
498
519
  packageManager: getPackageManagerName(defaultOptions.packageManager),
499
520
  pnpmManageVersions: defaultOptions.pnpmManageVersions,
500
521
  linter: defaultOptions.linter ?? "oxlint",
501
- formatter: defaultOptions.formatter ?? "prettier"
522
+ formatter: defaultOptions.formatter ?? "prettier",
523
+ ide: defaultOptions.ide ?? "vscode"
502
524
  }),
503
525
  "Workspace Configuration"
504
526
  );
505
- const proceed = await p.select({
506
- message: "Proceed with these settings?",
507
- options: [
508
- { value: "continue", label: "Yes, continue" },
509
- { value: "customize", label: "No, customize settings" }
510
- ],
511
- initialValue: "continue"
512
- });
513
- if (p.isCancel(proceed)) {
514
- p.cancel("Operation cancelled.");
515
- process.exit(0);
516
- }
517
- if (proceed === "continue") {
527
+ if (await promptForProceed()) {
518
528
  return defaultOptions;
519
529
  }
520
530
  return promptForMonorepoCustomization(workspaceName, presets);
@@ -523,7 +533,7 @@ async function promptForOptions(name, presets) {
523
533
  let projectName = name;
524
534
  if (!projectName) {
525
535
  const nameResult = await p.text({
526
- message: "What is your project named?",
536
+ message: "Project name:",
527
537
  placeholder: generateRandomName(),
528
538
  defaultValue: generateRandomName(),
529
539
  validate: (value) => {
@@ -537,7 +547,7 @@ async function promptForOptions(name, presets) {
537
547
  projectName = nameResult;
538
548
  }
539
549
  const projectType = await p.select({
540
- message: "Project type",
550
+ message: "Project type:",
541
551
  options: [
542
552
  { value: "app", label: "Application" },
543
553
  { value: "library", label: "Library" },
@@ -554,41 +564,6 @@ async function promptForOptions(name, presets) {
554
564
  }
555
565
  return promptForPackageOptions(projectName, projectType, void 0, presets);
556
566
  }
557
- function customTemplateToOptions(customTemplate, name, projectType, inheritedSettings) {
558
- const baseTemplate = customTemplate.baseTemplate;
559
- const template = baseTemplate;
560
- const base = {
561
- name,
562
- template,
563
- projectType,
564
- packageManager: inheritedSettings?.packageManager ?? { name: "pnpm" },
565
- pnpmManageVersions: inheritedSettings?.pnpmManageVersions ?? true,
566
- engine: inheritedSettings?.engine ?? { name: "node", version: "latest" },
567
- linter: inheritedSettings?.linter ?? customTemplate.linter,
568
- formatter: inheritedSettings?.formatter ?? customTemplate.formatter,
569
- testing: customTemplate.testing,
570
- configStrategy: customTemplate.configStrategy ?? getConfigStrategy()
571
- };
572
- if (baseTemplate === "r3f" && customTemplate.integrations) {
573
- const integrations = customTemplate.integrations;
574
- return {
575
- ...base,
576
- drei: integrations.includes("drei") ? {} : void 0,
577
- handle: integrations.includes("handle") ? {} : void 0,
578
- leva: integrations.includes("leva") ? {} : void 0,
579
- postprocessing: integrations.includes("postprocessing") ? {} : void 0,
580
- rapier: integrations.includes("rapier") ? {} : void 0,
581
- xr: integrations.includes("xr") ? {} : void 0,
582
- uikit: integrations.includes("uikit") ? {} : void 0,
583
- offscreen: integrations.includes("offscreen") ? {} : void 0,
584
- zustand: integrations.includes("zustand") ? {} : void 0,
585
- koota: integrations.includes("koota") ? {} : void 0,
586
- triplex: integrations.includes("triplex") ? {} : void 0,
587
- viverse: integrations.includes("viverse") ? {} : void 0
588
- };
589
- }
590
- return base;
591
- }
592
567
  function presetsToInheritedSettings(presets) {
593
568
  if (!presets) return void 0;
594
569
  return {
@@ -600,956 +575,313 @@ function presetsToInheritedSettings(presets) {
600
575
  };
601
576
  }
602
577
  async function promptForPackageOptions(projectName, projectType, inheritedSettings, presets) {
603
- const builtInOptions = [
604
- { value: "vanilla", label: "Vanilla" },
605
- { value: "react", label: "React", hint: "experimental" },
606
- { value: "r3f", label: "React Three Fiber", hint: "experimental" }
607
- ];
608
- const customTemplates = getCustomTemplates();
609
- const customOptions = Object.keys(customTemplates).map((name) => ({
610
- value: `custom:${name}`,
611
- label: name,
612
- hint: "saved template"
613
- }));
614
- const allOptions = [...builtInOptions, ...customOptions];
615
578
  const templateSelection = await p.select({
616
- message: "Select a template",
617
- options: allOptions,
618
- initialValue: presets?.template ?? "vanilla"
579
+ message: "Select a template:",
580
+ options: [
581
+ { value: "vanilla", label: color.yellow("Vanilla") },
582
+ { value: "react", label: color.cyan("React"), hint: "experimental" },
583
+ { value: "r3f", label: color.magenta("React Three Fiber"), hint: "experimental" }
584
+ ],
585
+ initialValue: presets?.template ? getBaseTemplate(presets.template) : "vanilla"
619
586
  });
620
587
  if (p.isCancel(templateSelection)) {
621
588
  p.cancel("Operation cancelled.");
622
589
  process.exit(0);
623
590
  }
624
- const selection = templateSelection;
625
- if (selection.startsWith("custom:")) {
626
- const customName = selection.slice(7);
627
- const customTemplate = customTemplates[customName];
628
- const defaultOptions2 = customTemplateToOptions(
629
- customTemplate,
630
- projectName,
631
- projectType,
632
- inheritedSettings
633
- );
634
- const configTitle2 = inheritedSettings ? `Template: ${customName} (using workspace settings)` : `Template: ${customName}`;
635
- p.note(formatConfigSummary(defaultOptions2, inheritedSettings), configTitle2);
636
- const proceed2 = await p.select({
637
- message: "Proceed with these settings?",
638
- options: [
639
- { value: "continue", label: "Yes, continue" },
640
- { value: "customize", label: "No, customize settings" }
641
- ],
642
- initialValue: "continue"
643
- });
644
- if (p.isCancel(proceed2)) {
645
- p.cancel("Operation cancelled.");
646
- process.exit(0);
647
- }
648
- if (proceed2 === "continue") {
649
- return defaultOptions2;
650
- }
651
- return promptForCustomization(
652
- customTemplate.baseTemplate,
653
- projectName,
654
- projectType,
655
- customTemplate.integrations,
656
- inheritedSettings
657
- );
658
- }
659
- const template = selection;
591
+ const template = templateSelection;
660
592
  const baseTemplate = getBaseTemplate(template);
661
- let integrations;
593
+ let features;
662
594
  if (baseTemplate === "r3f") {
663
- integrations = await promptForR3fIntegrations(presets);
595
+ features = await promptForR3fIntegrations(presets);
664
596
  }
665
597
  const defaultOptions = getDefaultOptions(
666
598
  template,
667
599
  projectName,
668
600
  projectType,
669
601
  presets?.bundler,
670
- integrations,
602
+ features,
671
603
  inheritedSettings ?? presetsToInheritedSettings(presets)
672
604
  );
605
+ if (presets?.ide && !inheritedSettings) {
606
+ defaultOptions.ide = presets.ide;
607
+ }
673
608
  const configTitle = inheritedSettings ? "Template Configuration (using workspace settings)" : "Template Configuration";
674
609
  p.note(formatConfigSummary(defaultOptions, inheritedSettings), configTitle);
675
- const proceed = await p.select({
676
- message: "Proceed with these settings?",
677
- options: [
678
- { value: "continue", label: "Yes, continue" },
679
- { value: "customize", label: "No, customize settings" }
680
- ],
681
- initialValue: "continue"
682
- });
683
- if (p.isCancel(proceed)) {
684
- p.cancel("Operation cancelled.");
685
- process.exit(0);
686
- }
687
- if (proceed === "continue") {
610
+ if (await promptForProceed()) {
688
611
  return defaultOptions;
689
612
  }
690
613
  return promptForCustomization(
691
614
  template,
692
615
  projectName,
693
616
  projectType,
694
- integrations,
617
+ features,
695
618
  inheritedSettings,
696
619
  presets
697
620
  );
698
621
  }
699
622
 
700
- async function detectCurrentConfig(root, isMonorepo = true) {
701
- let name = root.split(/[/\\]/).pop() ?? "workspace";
702
- let packageManager = "pnpm";
703
- try {
704
- const pkgPath = join(root, "package.json");
705
- const content = await readFile(pkgPath, "utf-8");
706
- const pkgJson = JSON.parse(content);
707
- if (pkgJson.name) {
708
- name = pkgJson.name.replace(/^@/, "").replace(/\/.*$/, "");
623
+ async function promptForAiAgentPlatforms(isNonInteractive) {
624
+ const savedPlatforms = getAiPlatforms();
625
+ if (isNonInteractive) {
626
+ return savedPlatforms ?? ALL_AI_PLATFORMS;
627
+ }
628
+ if (savedPlatforms && savedPlatforms.length > 0) {
629
+ const savedLabels = savedPlatforms.map((platform) => AI_PLATFORM_LABELS[platform]).join(", ");
630
+ const useDefault = await p.confirm({
631
+ message: `Add AI rules? ${color.dim(`(${savedLabels})`)}`,
632
+ initialValue: true
633
+ });
634
+ if (p.isCancel(useDefault)) {
635
+ return [];
709
636
  }
710
- if (pkgJson.packageManager) {
711
- packageManager = pkgJson.packageManager.split("@")[0] ?? packageManager;
637
+ if (useDefault) {
638
+ return savedPlatforms;
712
639
  }
713
- } catch {
714
640
  }
715
- const tooling = await detectTooling(root);
716
- const configStrategy = isMonorepo ? void 0 : await detectStandaloneConfigStrategy(root);
717
- return {
718
- name,
719
- linter: tooling.linter ?? "oxlint",
720
- formatter: tooling.formatter ?? "prettier",
721
- packageManager,
722
- isMonorepo,
723
- configStrategy
724
- };
725
- }
726
- async function detectStandaloneConfigStrategy(root) {
727
- const hasStealthConfig = await Promise.all([
728
- fileExists$1(join(root, ".config/tsconfig.app.json")),
729
- fileExists$1(join(root, ".config/tsconfig.node.json")),
730
- fileExists$1(join(root, ".config/prettier.json")),
731
- fileExists$1(join(root, ".config/oxlint.json"))
732
- ]).then((matches) => matches.some(Boolean));
733
- return hasStealthConfig ? "stealth" : "root";
734
- }
735
- async function generateExpectedFiles(config) {
736
- const { name, linter, formatter, packageManager, isMonorepo, configStrategy } = config;
737
- const versions = linter === "biome" || formatter === "biome" ? await resolveMonorepoRootPackageVersions({ linter, formatter }) : {};
738
- const aiFilesMap = {};
739
- generateAiFiles(aiFilesMap, {
740
- name,
741
- packageManager,
742
- linter,
743
- formatter,
744
- isMonorepo,
745
- configStrategy,
746
- platforms: ALL_AI_PLATFORMS
641
+ const selected = await p.multiselect({
642
+ message: "Add AI rules?",
643
+ options: ALL_AI_PLATFORMS.map((platform) => ({
644
+ value: platform,
645
+ label: AI_PLATFORM_LABELS[platform],
646
+ hint: AI_PLATFORM_HINTS[platform]
647
+ })),
648
+ initialValues: ["agents"],
649
+ required: false
747
650
  });
748
- const vscodeFiles = {};
749
- generateVscodeFiles(vscodeFiles, linter, formatter);
750
- const configPackages = {};
751
- if (isMonorepo) {
752
- generateTypescriptConfigPackage(configPackages);
753
- if (linter === "oxlint") {
754
- generateOxlintConfigPackage(configPackages);
755
- } else if (linter === "eslint") {
756
- generateEslintConfigPackage(configPackages);
757
- }
758
- if (formatter === "oxfmt") {
759
- generateOxfmtConfigPackage(configPackages);
760
- } else if (formatter === "prettier") {
761
- generatePrettierConfigPackage(configPackages);
651
+ if (p.isCancel(selected)) {
652
+ return [];
653
+ }
654
+ return selected;
655
+ }
656
+
657
+ async function checkAnyExists(paths) {
658
+ for (const path of paths) {
659
+ try {
660
+ await access(path, constants.F_OK);
661
+ return true;
662
+ } catch {
762
663
  }
763
664
  }
764
- const workspaceConfig = {};
765
- const rootConfig = {};
766
- rootConfig[".gitignore"] = generateGitignore(isMonorepo ? "workspace-root" : "standalone");
767
- rootConfig[".gitattributes"] = {
768
- type: "text",
769
- content: `* text=auto eol=lf
770
- *.{cmd,[cC][mM][dD]} text eol=crlf
771
- *.{bat,[bB][aA][tT]} text eol=crlf
772
- `
773
- };
774
- if (linter === "biome" || formatter === "biome") {
775
- const biomeVersion = getResolvedPackageVersion(versions, "@biomejs/biome");
776
- const biomeConfig = {
777
- $schema: `https://biomejs.dev/schemas/${biomeVersion}/schema.json`,
778
- vcs: {
779
- enabled: true,
780
- clientKind: "git",
781
- useIgnoreFile: true
782
- },
783
- linter: {
784
- enabled: linter === "biome",
785
- rules: {
786
- recommended: true
787
- }
788
- },
789
- formatter: {
790
- enabled: formatter === "biome"
791
- }
792
- };
793
- rootConfig["biome.json"] = {
794
- type: "text",
795
- content: JSON.stringify(biomeConfig, null, 2)
796
- };
665
+ return false;
666
+ }
667
+ async function validateWorkspace(monorepoRoot) {
668
+ const errors = [];
669
+ const tsConfigPath = join(monorepoRoot, ".config/typescript/package.json");
670
+ try {
671
+ await access(tsConfigPath, constants.F_OK);
672
+ } catch {
673
+ errors.push("Missing .config/typescript package");
797
674
  }
798
- return {
799
- "ai-files": aiFilesMap,
800
- vscode: vscodeFiles,
801
- "config-packages": configPackages,
802
- "workspace-config": workspaceConfig,
803
- "root-config": rootConfig
804
- };
675
+ const linterPaths = [
676
+ join(monorepoRoot, ".config/oxlint/package.json"),
677
+ join(monorepoRoot, ".config/eslint/package.json"),
678
+ join(monorepoRoot, "eslint.config.js"),
679
+ join(monorepoRoot, "biome.json")
680
+ ];
681
+ const hasLinter = await checkAnyExists(linterPaths);
682
+ if (!hasLinter) {
683
+ errors.push(
684
+ "Missing linter config (.config/oxlint, .config/eslint, eslint.config.js, or biome.json)"
685
+ );
686
+ }
687
+ const formatterPaths = [
688
+ join(monorepoRoot, ".config/oxfmt/package.json"),
689
+ join(monorepoRoot, ".config/prettier/package.json"),
690
+ join(monorepoRoot, ".prettierrc.json"),
691
+ join(monorepoRoot, "biome.json")
692
+ ];
693
+ const hasFormatter = await checkAnyExists(formatterPaths);
694
+ if (!hasFormatter) {
695
+ errors.push(
696
+ "Missing formatter config (.config/oxfmt, .config/prettier, .prettierrc.json, or biome.json)"
697
+ );
698
+ }
699
+ return { valid: errors.length === 0, errors };
805
700
  }
701
+
806
702
  async function fileExists$1(path) {
807
703
  try {
808
- await access(path, constants.F_OK);
704
+ await access$1(path, constants$1.F_OK);
809
705
  return true;
810
706
  } catch {
811
707
  return false;
812
708
  }
813
709
  }
814
- async function compareWithDisk(expected, root) {
815
- const categoryLabels = {
816
- "ai-files": "AI Files",
817
- vscode: "VS Code",
818
- "config-packages": "Config Packages",
819
- "workspace-config": "Workspace Config",
820
- "root-config": "Root Config"
821
- };
822
- const categories = [];
823
- for (const [category, files] of Object.entries(expected)) {
824
- const changes = [];
825
- for (const [filePath, file] of Object.entries(files)) {
826
- if (file.type !== "text") continue;
827
- const fullPath = join(root, filePath);
828
- const newContent = file.content;
829
- if (await fileExists$1(fullPath)) {
830
- const currentContent = await readFile(fullPath, "utf-8");
831
- if (currentContent === newContent) {
832
- changes.push({
833
- path: filePath,
834
- status: "unchanged",
835
- currentContent,
836
- newContent
837
- });
838
- } else {
839
- changes.push({
840
- path: filePath,
841
- status: "modified",
842
- currentContent,
843
- newContent
844
- });
845
- }
846
- } else {
847
- changes.push({
848
- path: filePath,
849
- status: "added",
850
- newContent
851
- });
710
+ function calculateWorkspaceRoot(packagePath) {
711
+ const segments = packagePath.split(/[/\\]/).filter(Boolean);
712
+ return segments.map(() => "..").join("/");
713
+ }
714
+ async function detectMonorepoRoot() {
715
+ let currentDir = cwd();
716
+ const root = resolve("/");
717
+ while (currentDir !== root) {
718
+ const workspaceFile = join$1(currentDir, "pnpm-workspace.yaml");
719
+ try {
720
+ await access$1(workspaceFile, constants$1.F_OK);
721
+ const content = await readFile(workspaceFile, "utf-8");
722
+ if (content.includes("packages:")) {
723
+ return currentDir;
852
724
  }
725
+ } catch {
853
726
  }
854
- if (changes.length === 0) continue;
855
- const hasUserModifications = changes.some((c) => c.status === "modified");
856
- categories.push({
857
- category,
858
- label: categoryLabels[category],
859
- changes,
860
- hasUserModifications
861
- });
727
+ currentDir = dirname(currentDir);
862
728
  }
863
- return categories;
729
+ return null;
864
730
  }
865
- async function getWorkspaceConfigUpdates(root) {
866
- const workspacePath = join(root, "pnpm-workspace.yaml");
867
- const changes = [];
868
- let currentContent = "";
869
- let exists = false;
731
+ async function detectPackageRoot() {
732
+ let currentDir = cwd();
733
+ const root = resolve("/");
734
+ while (currentDir !== root) {
735
+ if (await fileExists$1(join$1(currentDir, "package.json"))) {
736
+ return currentDir;
737
+ }
738
+ currentDir = dirname(currentDir);
739
+ }
740
+ return await fileExists$1(join$1(root, "package.json")) ? root : null;
741
+ }
742
+ async function parseWorkspaceDirectories(monorepoRoot) {
870
743
  try {
871
- currentContent = await readFile(workspacePath, "utf-8");
872
- exists = true;
744
+ const workspaceFile = join$1(monorepoRoot, "pnpm-workspace.yaml");
745
+ const content = await readFile(workspaceFile, "utf-8");
746
+ return parseWorkspaceYamlContent(content);
873
747
  } catch {
748
+ return [];
874
749
  }
875
- if (!exists) {
876
- const newContent = `manage-package-manager-versions: true
877
-
878
- packages:
879
- - ".config/*"
880
- - "apps/*"
881
- - "packages/*"
882
-
883
- onlyBuiltDependencies:
884
- - esbuild
885
- `;
886
- changes.push({
887
- path: "pnpm-workspace.yaml",
888
- status: "added",
889
- newContent
890
- });
891
- return changes;
892
- }
893
- let updatedContent = currentContent;
894
- let needsUpdate = false;
895
- if (!currentContent.includes("manage-package-manager-versions")) {
896
- updatedContent = `manage-package-manager-versions: true
897
-
898
- ${updatedContent}`;
899
- needsUpdate = true;
900
- }
901
- if (!currentContent.includes("onlyBuiltDependencies")) {
902
- updatedContent = `${updatedContent.trimEnd()}
903
-
904
- onlyBuiltDependencies:
905
- - esbuild
906
- `;
907
- needsUpdate = true;
908
- }
909
- if (!currentContent.includes(".config/*") && !currentContent.includes('".config/*"')) {
910
- const lines = updatedContent.split("\n");
911
- const packagesIndex = lines.findIndex((line) => line.trim().startsWith("packages:"));
912
- if (packagesIndex !== -1) {
913
- lines.splice(packagesIndex + 1, 0, ' - ".config/*"');
914
- updatedContent = lines.join("\n");
915
- needsUpdate = true;
750
+ }
751
+ async function detectWorkspaceSettings(monorepoRoot) {
752
+ try {
753
+ const tooling = await detectTooling(monorepoRoot);
754
+ const pkgPath = join$1(monorepoRoot, "package.json");
755
+ const content = await readFile(pkgPath, "utf-8");
756
+ const pkgJson = JSON.parse(content);
757
+ const packageManager = parsePackageManager(pkgJson.packageManager);
758
+ const engine = parseEngine(pkgJson.engines);
759
+ let pnpmManageVersions;
760
+ try {
761
+ const workspaceFile = join$1(monorepoRoot, "pnpm-workspace.yaml");
762
+ const workspaceContent = await readFile(workspaceFile, "utf-8");
763
+ pnpmManageVersions = workspaceContent.includes("manage-package-manager-versions: true");
764
+ } catch {
916
765
  }
766
+ return {
767
+ linter: tooling.linter,
768
+ formatter: tooling.formatter,
769
+ packageManager,
770
+ engine,
771
+ pnpmManageVersions
772
+ };
773
+ } catch {
774
+ return {};
917
775
  }
918
- if (needsUpdate) {
919
- changes.push({
920
- path: "pnpm-workspace.yaml",
921
- status: "modified",
922
- currentContent,
923
- newContent: updatedContent
924
- });
925
- } else {
926
- changes.push({
927
- path: "pnpm-workspace.yaml",
928
- status: "unchanged",
929
- currentContent,
930
- newContent: currentContent
931
- });
932
- }
933
- return changes;
934
776
  }
935
- async function applyUpdates(changes, root) {
936
- for (const change of changes) {
937
- if (change.status === "unchanged") continue;
938
- const fullPath = join(root, change.path);
939
- await mkdir(dirname(fullPath), { recursive: true });
940
- await writeFile(fullPath, change.newContent);
777
+ async function detectExistingConfigs(monorepoRoot) {
778
+ const configs = {};
779
+ const eslintPath = join$1(monorepoRoot, "eslint.config.js");
780
+ if (await fileExists$1(eslintPath)) {
781
+ configs.linter = "eslint";
782
+ configs.eslintConfigPath = eslintPath;
941
783
  }
942
- }
943
- function formatFileChange(change) {
944
- const icon = change.status === "added" ? "+" : change.status === "modified" ? "~" : "=";
945
- return ` ${icon} ${change.path}`;
946
- }
947
- const LINTER_DEPS = {
948
- oxlint: "oxlint",
949
- eslint: "eslint",
950
- biome: "@biomejs/biome"
951
- };
952
- const FORMATTER_DEPS = {
953
- oxfmt: "oxfmt",
954
- prettier: "prettier",
955
- biome: "@biomejs/biome"
956
- };
957
- const LINTER_CONFIG_PACKAGES = {
958
- oxlint: "@config/oxlint",
959
- eslint: "@config/eslint",
960
- biome: null
961
- // biome uses root biome.json
962
- };
963
- const FORMATTER_CONFIG_PACKAGES = {
964
- oxfmt: "@config/oxfmt",
965
- prettier: "@config/prettier",
966
- biome: null
967
- // biome uses root biome.json
968
- };
969
- function needsMigration(current, target) {
970
- const linterChange = target.linter && target.linter !== current.linter;
971
- const formatterChange = target.formatter && target.formatter !== current.formatter;
972
- return linterChange || formatterChange || false;
973
- }
974
- async function getMigrationPlan(current, target, root) {
975
- const toLinter = target.linter ?? current.linter;
976
- const toFormatter = target.formatter ?? current.formatter;
977
- const targetVersions = toLinter === "biome" || toFormatter === "biome" ? await resolveMonorepoRootPackageVersions({
978
- linter: toLinter,
979
- formatter: toFormatter
980
- }) : {};
981
- const biomeSchemaUrl = toLinter === "biome" || toFormatter === "biome" ? `https://biomejs.dev/schemas/${getResolvedPackageVersion(
982
- targetVersions,
983
- "@biomejs/biome"
984
- )}/schema.json` : "";
985
- const changes = [];
986
- if (toLinter !== current.linter) {
987
- if (current.linter !== "biome") {
988
- changes.push({
989
- type: "remove-dir",
990
- path: `.config/${current.linter}`,
991
- description: `Remove @config/${current.linter} package`
992
- });
993
- }
994
- if (toLinter !== "biome") {
995
- const files = {};
996
- if (toLinter === "oxlint") {
997
- generateOxlintConfigPackage(files);
998
- } else if (toLinter === "eslint") {
999
- generateEslintConfigPackage(files);
1000
- }
1001
- for (const [path, file] of Object.entries(files)) {
1002
- if (file.type === "text") {
1003
- changes.push({
1004
- type: "add-file",
1005
- path,
1006
- description: `Add ${path}`,
1007
- content: file.content
1008
- });
1009
- }
1010
- }
1011
- }
1012
- if (toLinter === "biome" && toFormatter === "biome") {
1013
- changes.push({
1014
- type: "add-file",
1015
- path: "biome.json",
1016
- description: "Add biome.json config",
1017
- content: JSON.stringify(
1018
- {
1019
- $schema: biomeSchemaUrl,
1020
- vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
1021
- linter: { enabled: true, rules: { recommended: true } },
1022
- formatter: { enabled: true }
1023
- },
1024
- null,
1025
- 2
1026
- )
1027
- });
1028
- } else if (toLinter === "biome" && toFormatter !== "biome") {
1029
- changes.push({
1030
- type: "add-file",
1031
- path: "biome.json",
1032
- description: "Add biome.json config (linter only)",
1033
- content: JSON.stringify(
1034
- {
1035
- $schema: biomeSchemaUrl,
1036
- vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
1037
- linter: { enabled: true, rules: { recommended: true } },
1038
- formatter: { enabled: false }
1039
- },
1040
- null,
1041
- 2
1042
- )
1043
- });
1044
- }
1045
- if (current.linter === "biome" && toLinter !== "biome" && current.formatter !== "biome" && toFormatter !== "biome") {
1046
- changes.push({
1047
- type: "remove-file",
1048
- path: "biome.json",
1049
- description: "Remove biome.json"
1050
- });
1051
- }
784
+ const prettierPath = join$1(monorepoRoot, ".prettierrc.json");
785
+ if (await fileExists$1(prettierPath)) {
786
+ configs.formatter = "prettier";
787
+ configs.prettierConfigPath = prettierPath;
1052
788
  }
1053
- if (toFormatter !== current.formatter) {
1054
- const formatterSameAsLinter = current.formatter === current.linter;
1055
- if (current.formatter !== "biome" && !formatterSameAsLinter) {
1056
- changes.push({
1057
- type: "remove-dir",
1058
- path: `.config/${current.formatter}`,
1059
- description: `Remove @config/${current.formatter} package`
1060
- });
1061
- }
1062
- const newFormatterSameAsLinter = toFormatter === toLinter;
1063
- if (toFormatter !== "biome" && !newFormatterSameAsLinter) {
1064
- const files = {};
1065
- if (toFormatter === "oxfmt") {
1066
- generateOxfmtConfigPackage(files);
1067
- } else if (toFormatter === "prettier") {
1068
- generatePrettierConfigPackage(files);
1069
- }
1070
- for (const [path, file] of Object.entries(files)) {
1071
- if (file.type === "text") {
1072
- changes.push({
1073
- type: "add-file",
1074
- path,
1075
- description: `Add ${path}`,
1076
- content: file.content
1077
- });
1078
- }
1079
- }
1080
- }
1081
- if (toFormatter === "biome" && toLinter !== "biome") {
1082
- changes.push({
1083
- type: "add-file",
1084
- path: "biome.json",
1085
- description: "Add biome.json config (formatter only)",
1086
- content: JSON.stringify(
1087
- {
1088
- $schema: biomeSchemaUrl,
1089
- vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
1090
- linter: { enabled: false },
1091
- formatter: { enabled: true }
1092
- },
1093
- null,
1094
- 2
1095
- )
1096
- });
1097
- }
1098
- if (current.formatter === "biome" && toFormatter !== "biome" && current.linter !== "biome" && toLinter !== "biome") {
1099
- changes.push({
1100
- type: "remove-file",
1101
- path: "biome.json",
1102
- description: "Remove biome.json"
1103
- });
1104
- }
789
+ const biomePath = join$1(monorepoRoot, "biome.json");
790
+ if (await fileExists$1(biomePath)) {
791
+ configs.biomeConfigPath = biomePath;
792
+ if (!configs.linter) configs.linter = "biome";
793
+ if (!configs.formatter) configs.formatter = "biome";
1105
794
  }
1106
- changes.push({
1107
- type: "update-package-json",
1108
- path: "package.json",
1109
- description: "Update root package.json (devDependencies, scripts)"
1110
- });
1111
- const subPackageUpdates = await getSubPackageUpdates(root, current, toLinter, toFormatter);
1112
- return {
1113
- fromLinter: current.linter,
1114
- toLinter,
1115
- fromFormatter: current.formatter,
1116
- toFormatter,
1117
- changes,
1118
- subPackageUpdates
1119
- };
795
+ return configs;
1120
796
  }
1121
- async function getSubPackageUpdates(root, current, toLinter, toFormatter) {
1122
- const updates = [];
1123
- const workspacePath = join(root, "pnpm-workspace.yaml");
1124
- let workspaceContent;
797
+ async function getMonorepoScope(monorepoRoot) {
1125
798
  try {
1126
- workspaceContent = await readFile(workspacePath, "utf-8");
1127
- } catch {
1128
- return updates;
1129
- }
1130
- const packageGlobs = parseWorkspaceYamlContent(workspaceContent);
1131
- for (const glob of packageGlobs) {
1132
- if (glob.includes(".config")) continue;
1133
- const baseDir = glob.replace(/\/\*$/, "").replace(/^["']|["']$/g, "");
1134
- const basePath = join(root, baseDir);
1135
- try {
1136
- const entries = await readdir(basePath, { withFileTypes: true });
1137
- for (const entry of entries) {
1138
- if (!entry.isDirectory()) continue;
1139
- const pkgJsonPath = join(basePath, entry.name, "package.json");
1140
- try {
1141
- const content = await readFile(pkgJsonPath, "utf-8");
1142
- const pkg = JSON.parse(content);
1143
- const devDeps = pkg.devDependencies ?? {};
1144
- const remove = [];
1145
- const add = [];
1146
- const oldLinterPkg = LINTER_CONFIG_PACKAGES[current.linter];
1147
- const newLinterPkg = LINTER_CONFIG_PACKAGES[toLinter];
1148
- if (oldLinterPkg && oldLinterPkg !== newLinterPkg && devDeps[oldLinterPkg]) {
1149
- remove.push(oldLinterPkg);
1150
- }
1151
- if (newLinterPkg && newLinterPkg !== oldLinterPkg && oldLinterPkg && devDeps[oldLinterPkg]) {
1152
- add.push(newLinterPkg);
1153
- }
1154
- if (current.formatter !== current.linter) {
1155
- const oldFormatterPkg = FORMATTER_CONFIG_PACKAGES[current.formatter];
1156
- const newFormatterPkg = FORMATTER_CONFIG_PACKAGES[toFormatter];
1157
- if (oldFormatterPkg && oldFormatterPkg !== newFormatterPkg && devDeps[oldFormatterPkg]) {
1158
- remove.push(oldFormatterPkg);
1159
- }
1160
- if (newFormatterPkg && newFormatterPkg !== oldFormatterPkg && oldFormatterPkg && devDeps[oldFormatterPkg]) {
1161
- add.push(newFormatterPkg);
1162
- }
1163
- }
1164
- if (remove.length > 0 || add.length > 0) {
1165
- updates.push({
1166
- path: join(baseDir, entry.name, "package.json"),
1167
- remove,
1168
- add
1169
- });
1170
- }
1171
- } catch {
1172
- }
1173
- }
1174
- } catch {
799
+ const pkgPath = join$1(monorepoRoot, "package.json");
800
+ const content = await readFile(pkgPath, "utf-8");
801
+ const pkgJson = JSON.parse(content);
802
+ if (pkgJson.name) {
803
+ return pkgJson.name.replace(/^@/, "").replace(/\/.*$/, "");
1175
804
  }
805
+ } catch {
1176
806
  }
1177
- return updates;
807
+ return monorepoRoot.split(/[/\\]/).pop() ?? "workspace";
1178
808
  }
1179
- async function applyMigration(plan, root) {
1180
- for (const change of plan.changes) {
1181
- if (change.type === "remove-dir") {
1182
- const fullPath = join(root, change.path);
809
+ async function getWorkspacePackages(monorepoRoot) {
810
+ const packagesDir = join$1(monorepoRoot, "packages");
811
+ try {
812
+ const entries = await readdir(packagesDir, { withFileTypes: true });
813
+ const names = [];
814
+ for (const entry of entries) {
815
+ if (!entry.isDirectory()) continue;
1183
816
  try {
1184
- await rm(fullPath, { recursive: true });
817
+ const content = await readFile(join$1(packagesDir, entry.name, "package.json"), "utf-8");
818
+ const pkg = JSON.parse(content);
819
+ if (pkg.name) names.push(pkg.name);
1185
820
  } catch {
1186
821
  }
1187
822
  }
823
+ return names;
824
+ } catch {
825
+ return [];
1188
826
  }
1189
- for (const change of plan.changes) {
1190
- if (change.type === "remove-file") {
1191
- const fullPath = join(root, change.path);
1192
- try {
1193
- await rm(fullPath);
1194
- } catch {
1195
- }
1196
- }
827
+ }
828
+ async function ensureConfigInWorkspace(monorepoRoot) {
829
+ const workspacePath = join$1(monorepoRoot, "pnpm-workspace.yaml");
830
+ let content;
831
+ try {
832
+ content = await readFile(workspacePath, "utf-8");
833
+ } catch {
834
+ content = `packages:
835
+ - '.config/*'
836
+ - 'packages/*'
837
+ `;
838
+ await writeFile(workspacePath, content);
839
+ return;
1197
840
  }
1198
- for (const change of plan.changes) {
1199
- if (change.type === "add-file" && change.content) {
1200
- const fullPath = join(root, change.path);
1201
- await mkdir(dirname(fullPath), { recursive: true });
1202
- await writeFile(fullPath, change.content);
1203
- }
841
+ if (content.includes(".config/*")) {
842
+ return;
1204
843
  }
1205
- await updateRootPackageJson(root, plan);
1206
- for (const update of plan.subPackageUpdates) {
1207
- await updateSubPackageJson(root, update);
844
+ const lines = content.split("\n");
845
+ const packagesIndex = lines.findIndex((line) => line.trim().startsWith("packages:"));
846
+ if (packagesIndex === -1) {
847
+ content = `packages:
848
+ - '.config/*'
849
+ ${content}`;
850
+ } else {
851
+ lines.splice(packagesIndex + 1, 0, " - '.config/*'");
852
+ content = lines.join("\n");
1208
853
  }
854
+ await writeFile(workspacePath, content);
1209
855
  }
1210
- async function updateRootPackageJson(root, plan) {
1211
- const pkgPath = join(root, "package.json");
1212
- const content = await readFile(pkgPath, "utf-8");
1213
- const pkg = JSON.parse(content);
1214
- const devDeps = pkg.devDependencies ?? {};
1215
- const oldLinterDep = LINTER_DEPS[plan.fromLinter];
1216
- delete devDeps[oldLinterDep];
1217
- if (plan.fromFormatter !== plan.fromLinter) {
1218
- const oldFormatterDep = FORMATTER_DEPS[plan.fromFormatter];
1219
- delete devDeps[oldFormatterDep];
856
+
857
+ async function handleCheckCommand() {
858
+ const monorepoRoot = await detectMonorepoRoot();
859
+ if (!monorepoRoot) {
860
+ console.log(color.red("\u2717") + " Not a monorepo workspace");
861
+ process.exit(1);
1220
862
  }
1221
- const resolvedVersions = await resolveMonorepoRootPackageVersions({
1222
- linter: plan.toLinter,
1223
- formatter: plan.toFormatter
1224
- });
1225
- const newLinterDep = LINTER_DEPS[plan.toLinter];
1226
- devDeps[newLinterDep] = formatResolvedPackageVersion(resolvedVersions, newLinterDep);
1227
- if (plan.toFormatter !== plan.toLinter) {
1228
- const newFormatterDep = FORMATTER_DEPS[plan.toFormatter];
1229
- devDeps[newFormatterDep] = formatResolvedPackageVersion(resolvedVersions, newFormatterDep);
1230
- }
1231
- pkg.devDependencies = Object.fromEntries(
1232
- Object.entries(devDeps).sort(([a], [b]) => a.localeCompare(b))
1233
- );
1234
- const scripts = pkg.scripts ?? {};
1235
- if (plan.toLinter === "oxlint") {
1236
- scripts.lint = "oxlint .";
1237
- } else if (plan.toLinter === "eslint") {
1238
- scripts.lint = "eslint .";
1239
- } else if (plan.toLinter === "biome") {
1240
- scripts.lint = "biome check .";
1241
- }
1242
- if (plan.toFormatter === "oxfmt") {
1243
- scripts.format = "oxfmt .";
1244
- } else if (plan.toFormatter === "prettier") {
1245
- scripts.format = "prettier --write .";
1246
- } else if (plan.toFormatter === "biome") {
1247
- scripts.format = "biome format . --write";
1248
- }
1249
- pkg.scripts = scripts;
1250
- await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1251
- }
1252
- async function updateSubPackageJson(root, update) {
1253
- const pkgPath = join(root, update.path);
1254
- const content = await readFile(pkgPath, "utf-8");
1255
- const pkg = JSON.parse(content);
1256
- const devDeps = pkg.devDependencies ?? {};
1257
- for (const dep of update.remove) {
1258
- delete devDeps[dep];
1259
- }
1260
- for (const dep of update.add) {
1261
- devDeps[dep] = "workspace:*";
1262
- }
1263
- pkg.devDependencies = Object.fromEntries(
1264
- Object.entries(devDeps).sort(([a], [b]) => a.localeCompare(b))
1265
- );
1266
- await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1267
- }
1268
- function formatMigrationChange(change) {
1269
- const icon = change.type === "remove-dir" || change.type === "remove-file" ? "-" : change.type === "add-file" ? "+" : "~";
1270
- return ` ${icon} ${change.description}`;
863
+ const { valid, errors } = await validateWorkspace(monorepoRoot);
864
+ if (valid) {
865
+ console.log(color.green("\u2713") + " Valid monorepo workspace");
866
+ console.log(color.dim(` ${monorepoRoot}`));
867
+ } else {
868
+ console.log(color.red("\u2717") + " Invalid monorepo workspace");
869
+ console.log(color.dim(` ${monorepoRoot}`));
870
+ for (const error of errors) {
871
+ console.log(color.red(` \u2022 ${error}`));
872
+ }
873
+ }
874
+ process.exit(valid ? 0 : 1);
1271
875
  }
1272
876
 
1273
- async function checkAnyExists(paths) {
1274
- for (const path of paths) {
1275
- try {
1276
- await access(path, constants$1.F_OK);
1277
- return true;
1278
- } catch {
1279
- }
1280
- }
1281
- return false;
1282
- }
1283
- async function validateWorkspace(monorepoRoot) {
1284
- const errors = [];
1285
- const tsConfigPath = join(monorepoRoot, ".config/typescript/package.json");
1286
- try {
1287
- await access(tsConfigPath, constants$1.F_OK);
1288
- } catch {
1289
- errors.push("Missing .config/typescript package");
1290
- }
1291
- const linterPaths = [
1292
- join(monorepoRoot, ".config/oxlint/package.json"),
1293
- join(monorepoRoot, ".config/eslint/package.json"),
1294
- join(monorepoRoot, "eslint.config.js"),
1295
- join(monorepoRoot, "biome.json")
1296
- ];
1297
- const hasLinter = await checkAnyExists(linterPaths);
1298
- if (!hasLinter) {
1299
- errors.push(
1300
- "Missing linter config (.config/oxlint, .config/eslint, eslint.config.js, or biome.json)"
1301
- );
1302
- }
1303
- const formatterPaths = [
1304
- join(monorepoRoot, ".config/oxfmt/package.json"),
1305
- join(monorepoRoot, ".config/prettier/package.json"),
1306
- join(monorepoRoot, ".prettierrc.json"),
1307
- join(monorepoRoot, "biome.json")
1308
- ];
1309
- const hasFormatter = await checkAnyExists(formatterPaths);
1310
- if (!hasFormatter) {
1311
- errors.push(
1312
- "Missing formatter config (.config/oxfmt, .config/prettier, .prettierrc.json, or biome.json)"
1313
- );
1314
- }
1315
- return { valid: errors.length === 0, errors };
1316
- }
1317
-
1318
- const require$1 = createRequire(import.meta.url);
1319
- const pkg = require$1("../package.json");
1320
- const META_OPTIONS = [
1321
- "clearConfig",
1322
- "configPath",
1323
- "check",
1324
- "fix",
1325
- "update",
1326
- "yes",
1327
- "workspace",
1328
- "path",
1329
- "dir"
1330
- ];
1331
- function hasConfigOptions(options) {
1332
- return Object.keys(options).some(
1333
- (key) => !META_OPTIONS.includes(key)
1334
- );
1335
- }
1336
- async function fileExists(path) {
1337
- try {
1338
- await access$1(path, constants$2.F_OK);
1339
- return true;
1340
- } catch {
1341
- return false;
1342
- }
1343
- }
1344
- async function promptForAiPlatforms(isNonInteractive) {
1345
- const savedPlatforms = getAiPlatforms();
1346
- if (isNonInteractive) {
1347
- return savedPlatforms ?? ALL_AI_PLATFORMS;
1348
- }
1349
- if (savedPlatforms && savedPlatforms.length > 0) {
1350
- const savedLabels = savedPlatforms.map((plat) => AI_PLATFORM_LABELS[plat]).join(", ");
1351
- const useDefault = await p.confirm({
1352
- message: `Add AI rules? ${color.dim(`(${savedLabels})`)}`,
1353
- initialValue: true
1354
- });
1355
- if (p.isCancel(useDefault)) {
1356
- return [];
1357
- }
1358
- if (useDefault) {
1359
- return savedPlatforms;
1360
- }
1361
- }
1362
- const selected = await p.multiselect({
1363
- message: "Add AI rules?",
1364
- options: ALL_AI_PLATFORMS.map((platform) => ({
1365
- value: platform,
1366
- label: AI_PLATFORM_LABELS[platform],
1367
- hint: AI_PLATFORM_HINTS[platform]
1368
- })),
1369
- initialValues: ["agents"],
1370
- required: false
1371
- });
1372
- if (p.isCancel(selected)) {
1373
- return [];
1374
- }
1375
- const platforms = selected;
1376
- if (platforms.length === 0) {
1377
- return [];
1378
- }
1379
- return platforms;
1380
- }
1381
- async function writeGeneratedFiles(basePath, files) {
1382
- const filePaths = Object.keys(files).sort();
1383
- for (const filePath of filePaths) {
1384
- const fullFilePath = join$1(basePath, filePath);
1385
- await mkdir$1(dirname$1(fullFilePath), { recursive: true });
1386
- const file = files[filePath];
1387
- if (file.type === "text") {
1388
- await writeFile$1(fullFilePath, file.content);
1389
- } else {
1390
- const response = await fetch(file.url);
1391
- await writeFile$1(fullFilePath, response.body);
1392
- }
1393
- }
1394
- }
1395
- function calculateWorkspaceRoot(packagePath) {
1396
- const segments = packagePath.split(/[/\\]/).filter(Boolean);
1397
- return segments.map(() => "..").join("/");
1398
- }
1399
- async function detectMonorepoRoot() {
1400
- let currentDir = cwd();
1401
- const root = resolve("/");
1402
- while (currentDir !== root) {
1403
- const workspaceFile = join$1(currentDir, "pnpm-workspace.yaml");
1404
- try {
1405
- await access$1(workspaceFile, constants$2.F_OK);
1406
- const content = await readFile$1(workspaceFile, "utf-8");
1407
- if (content.includes("packages:")) {
1408
- return currentDir;
1409
- }
1410
- } catch {
1411
- }
1412
- currentDir = dirname$1(currentDir);
1413
- }
1414
- return null;
1415
- }
1416
- async function detectPackageRoot() {
1417
- let currentDir = cwd();
1418
- const root = resolve("/");
1419
- while (currentDir !== root) {
1420
- if (await fileExists(join$1(currentDir, "package.json"))) {
1421
- return currentDir;
1422
- }
1423
- currentDir = dirname$1(currentDir);
1424
- }
1425
- return await fileExists(join$1(root, "package.json")) ? root : null;
1426
- }
1427
- async function parseWorkspaceDirectories(monorepoRoot) {
1428
- try {
1429
- const workspaceFile = join$1(monorepoRoot, "pnpm-workspace.yaml");
1430
- const content = await readFile$1(workspaceFile, "utf-8");
1431
- return parseWorkspaceYamlContent(content);
1432
- } catch {
1433
- return [];
1434
- }
1435
- }
1436
- async function detectWorkspaceSettings(monorepoRoot) {
1437
- try {
1438
- const tooling = await detectTooling(monorepoRoot);
1439
- const pkgPath = join$1(monorepoRoot, "package.json");
1440
- const content = await readFile$1(pkgPath, "utf-8");
1441
- const pkgJson = JSON.parse(content);
1442
- const packageManager = parsePackageManager(pkgJson.packageManager);
1443
- const engine = parseEngine(pkgJson.engines);
1444
- let pnpmManageVersions;
1445
- try {
1446
- const workspaceFile = join$1(monorepoRoot, "pnpm-workspace.yaml");
1447
- const workspaceContent = await readFile$1(workspaceFile, "utf-8");
1448
- pnpmManageVersions = workspaceContent.includes("manage-package-manager-versions: true");
1449
- } catch {
1450
- }
1451
- return {
1452
- linter: tooling.linter,
1453
- formatter: tooling.formatter,
1454
- packageManager,
1455
- engine,
1456
- pnpmManageVersions
1457
- };
1458
- } catch {
1459
- return {};
1460
- }
1461
- }
1462
- async function detectExistingConfigs(monorepoRoot) {
1463
- const configs = {};
1464
- const eslintPath = join$1(monorepoRoot, "eslint.config.js");
1465
- if (await fileExists(eslintPath)) {
1466
- configs.linter = "eslint";
1467
- configs.eslintConfigPath = eslintPath;
1468
- }
1469
- const prettierPath = join$1(monorepoRoot, ".prettierrc.json");
1470
- if (await fileExists(prettierPath)) {
1471
- configs.formatter = "prettier";
1472
- configs.prettierConfigPath = prettierPath;
1473
- }
1474
- const biomePath = join$1(monorepoRoot, "biome.json");
1475
- if (await fileExists(biomePath)) {
1476
- configs.biomeConfigPath = biomePath;
1477
- if (!configs.linter) configs.linter = "biome";
1478
- if (!configs.formatter) configs.formatter = "biome";
1479
- }
1480
- return configs;
1481
- }
1482
- async function getMonorepoScope(monorepoRoot) {
1483
- try {
1484
- const pkgPath = join$1(monorepoRoot, "package.json");
1485
- const content = await readFile$1(pkgPath, "utf-8");
1486
- const pkgJson = JSON.parse(content);
1487
- if (pkgJson.name) {
1488
- return pkgJson.name.replace(/^@/, "").replace(/\/.*$/, "");
1489
- }
1490
- } catch {
1491
- }
1492
- return monorepoRoot.split(/[/\\]/).pop() ?? "workspace";
1493
- }
1494
- async function getWorkspacePackages(monorepoRoot) {
1495
- const packagesDir = join$1(monorepoRoot, "packages");
1496
- try {
1497
- const { readdir } = await import('fs/promises');
1498
- const entries = await readdir(packagesDir, { withFileTypes: true });
1499
- const names = [];
1500
- for (const entry of entries) {
1501
- if (!entry.isDirectory()) continue;
1502
- try {
1503
- const content = await readFile$1(
1504
- join$1(packagesDir, entry.name, "package.json"),
1505
- "utf-8"
1506
- );
1507
- const pkg2 = JSON.parse(content);
1508
- if (pkg2.name) names.push(pkg2.name);
1509
- } catch {
1510
- }
1511
- }
1512
- return names;
1513
- } catch {
1514
- return [];
1515
- }
1516
- }
1517
- async function ensureConfigInWorkspace(monorepoRoot) {
1518
- const workspacePath = join$1(monorepoRoot, "pnpm-workspace.yaml");
1519
- let content;
1520
- try {
1521
- content = await readFile$1(workspacePath, "utf-8");
1522
- } catch {
1523
- content = `packages:
1524
- - ".config/*"
1525
- - "packages/*"
1526
- `;
1527
- await writeFile$1(workspacePath, content);
1528
- return;
1529
- }
1530
- if (content.includes(".config/*") || content.includes('".config/*"')) {
1531
- return;
1532
- }
1533
- const lines = content.split("\n");
1534
- const packagesIndex = lines.findIndex((line) => line.trim().startsWith("packages:"));
1535
- if (packagesIndex === -1) {
1536
- content = `packages:
1537
- - ".config/*"
1538
- ${content}`;
1539
- } else {
1540
- lines.splice(packagesIndex + 1, 0, ' - ".config/*"');
1541
- content = lines.join("\n");
1542
- }
1543
- await writeFile$1(workspacePath, content);
1544
- }
1545
877
  async function migrateEslintConfig(monorepoRoot, files) {
1546
878
  const configBasePath = ".config/eslint";
1547
879
  const existingConfigPath = join$1(monorepoRoot, "eslint.config.js");
1548
880
  let existingContent;
1549
881
  try {
1550
- existingContent = await readFile$1(existingConfigPath, "utf-8");
882
+ existingContent = await readFile(existingConfigPath, "utf-8");
1551
883
  } catch {
1552
- generateEslintConfigPackage(files);
884
+ renderEslintConfigPackage(files);
1553
885
  return;
1554
886
  }
1555
887
  files[`${configBasePath}/package.json`] = {
@@ -1626,9 +958,9 @@ async function migratePrettierConfig(monorepoRoot, files) {
1626
958
  const existingConfigPath = join$1(monorepoRoot, ".prettierrc.json");
1627
959
  let existingContent;
1628
960
  try {
1629
- existingContent = await readFile$1(existingConfigPath, "utf-8");
961
+ existingContent = await readFile(existingConfigPath, "utf-8");
1630
962
  } catch {
1631
- generatePrettierConfigPackage(files);
963
+ renderPrettierConfigPackage(files);
1632
964
  return;
1633
965
  }
1634
966
  files[`${configBasePath}/package.json`] = {
@@ -1678,128 +1010,12 @@ Or in \`package.json\`:
1678
1010
  content: existingContent
1679
1011
  };
1680
1012
  }
1681
- async function createPackageInWorkspace(monorepoRoot, packageManager, inheritedSettings, scope) {
1682
- const workspaceDirectories = await parseWorkspaceDirectories(monorepoRoot);
1683
- const defaultDirectories = ["apps", "packages"];
1684
- const hasCustomDirectories = workspaceDirectories.length > 0 && !workspaceDirectories.every((dir) => defaultDirectories.includes(dir));
1685
- const packageType = await promptForInitialPackage();
1686
- if (packageType === "skip") {
1687
- return false;
1688
- }
1689
- const defaultDir = packageType === "app" ? "apps" : "packages";
1690
- const packageNameInput = await p.text({
1691
- message: "Package name?",
1692
- initialValue: `@${scope}/`,
1693
- validate: (value) => {
1694
- const validationError = validatePackageName(value);
1695
- if (validationError) return validationError;
1696
- const dirName = value.includes("/") ? value.split("/").pop() : value;
1697
- if (!dirName) return "Package name is required";
1698
- if (!hasCustomDirectories) {
1699
- const targetPath = join$1(monorepoRoot, defaultDir, dirName);
1700
- try {
1701
- const { statSync } = require$1("fs");
1702
- statSync(targetPath);
1703
- return `Directory ${defaultDir}/${dirName} already exists`;
1704
- } catch {
1705
- }
1706
- }
1707
- }
1708
- });
1709
- if (p.isCancel(packageNameInput)) {
1710
- return false;
1711
- }
1712
- const scopedName = packageNameInput;
1713
- const shortName = scopedName.includes("/") ? scopedName.split("/").pop() : scopedName;
1714
- const packageOptions = await promptForPackageOptions(scopedName, packageType, inheritedSettings);
1715
- let targetDir = defaultDir;
1716
- if (hasCustomDirectories && workspaceDirectories.length > 0) {
1717
- const dirChoice = await p.select({
1718
- message: "Target directory",
1719
- options: workspaceDirectories.map((dir) => ({
1720
- value: dir,
1721
- label: dir
1722
- })),
1723
- initialValue: workspaceDirectories.includes(defaultDir) ? defaultDir : workspaceDirectories[0]
1724
- });
1725
- if (p.isCancel(dirChoice)) {
1726
- return false;
1727
- }
1728
- targetDir = dirChoice;
1729
- const targetPath = join$1(monorepoRoot, targetDir, shortName);
1730
- try {
1731
- const { statSync } = require$1("fs");
1732
- statSync(targetPath);
1733
- p.log.error(`Directory ${targetDir}/${shortName} already exists`);
1734
- return false;
1735
- } catch {
1736
- }
1737
- }
1738
- const relativePkgPath = join$1(targetDir, shortName);
1739
- const workspaceRoot = calculateWorkspaceRoot(relativePkgPath);
1740
- packageOptions.workspaceRoot = workspaceRoot;
1741
- packageOptions.name = scopedName;
1742
- packageOptions.packageManager = await resolvePackageManager(packageOptions);
1743
- packageOptions.engine = await resolveEngine(packageOptions);
1744
- packageOptions.versions = await resolveProjectPackageVersions(packageOptions);
1745
- const workspacePackages = packageType === "app" ? await getWorkspacePackages(monorepoRoot) : [];
1746
- if (workspacePackages.length > 0) {
1747
- const selectedDeps = await p.multiselect({
1748
- message: "Add workspace dependencies?",
1749
- options: workspacePackages.map((name) => ({ value: name, label: name })),
1750
- required: false
1751
- });
1752
- if (!p.isCancel(selectedDeps) && selectedDeps.length > 0) {
1753
- packageOptions.workspaceDependencies = selectedDeps;
1754
- }
1755
- }
1756
- const outputPath = join$1(monorepoRoot, relativePkgPath);
1757
- const spinner = p.spinner();
1758
- spinner.start("Creating package...");
1759
- try {
1760
- const files = generate(packageOptions);
1761
- await writeGeneratedFiles(outputPath, files);
1762
- spinner.stop(color.green.inverse(` \u2713 Package created at ${relativePkgPath}! `));
1763
- const addAnother = await p.select({
1764
- message: "Add another package?",
1765
- options: [
1766
- { value: "no", label: "No, I'm done" },
1767
- { value: "yes", label: "Yes, add another" }
1768
- ],
1769
- initialValue: "no"
1770
- });
1771
- return !p.isCancel(addAnother) && addAnother === "yes";
1772
- } catch (error) {
1773
- spinner.stop("Failed to create package");
1774
- p.log.error(String(error));
1775
- return false;
1776
- }
1777
- }
1778
- async function handleCheckCommand() {
1779
- const monorepoRoot = await detectMonorepoRoot();
1780
- if (!monorepoRoot) {
1781
- console.log(color.red("\u2717") + " Not a monorepo workspace");
1782
- process.exit(1);
1783
- }
1784
- const { valid, errors } = await validateWorkspace(monorepoRoot);
1785
- if (valid) {
1786
- console.log(color.green("\u2713") + " Valid monorepo workspace");
1787
- console.log(color.dim(` ${monorepoRoot}`));
1788
- } else {
1789
- console.log(color.red("\u2717") + " Invalid monorepo workspace");
1790
- console.log(color.dim(` ${monorepoRoot}`));
1791
- for (const error of errors) {
1792
- console.log(color.red(` \u2022 ${error}`));
1793
- }
1794
- }
1795
- process.exit(valid ? 0 : 1);
1796
- }
1797
- async function handleFixCommand(options) {
1798
- const monorepoRoot = await detectMonorepoRoot();
1799
- if (!monorepoRoot) {
1800
- console.log(color.red("\u2717") + " Not a monorepo workspace");
1801
- console.log(color.dim(" Run this command from within a monorepo"));
1802
- process.exit(1);
1013
+ async function handleFixCommand(options) {
1014
+ const monorepoRoot = await detectMonorepoRoot();
1015
+ if (!monorepoRoot) {
1016
+ console.log(color.red("\u2717") + " Not a monorepo workspace");
1017
+ console.log(color.dim(" Run this command from within a monorepo"));
1018
+ process.exit(1);
1803
1019
  }
1804
1020
  const { valid, errors } = await validateWorkspace(monorepoRoot);
1805
1021
  if (valid) {
@@ -1875,39 +1091,33 @@ async function handleFixCommand(options) {
1875
1091
  spinner.start("Fixing workspace...");
1876
1092
  try {
1877
1093
  const files = {};
1878
- const tsConfigExists = await fileExists(
1879
- join$1(monorepoRoot, ".config/typescript/package.json")
1880
- );
1094
+ const tsConfigExists = await fileExists$1(join$1(monorepoRoot, ".config/typescript/package.json"));
1881
1095
  if (!tsConfigExists) {
1882
- generateTypescriptConfigPackage(files);
1096
+ renderTypescriptConfigPackage(files);
1883
1097
  }
1884
1098
  if (linter === "oxlint") {
1885
- const oxlintExists = await fileExists(join$1(monorepoRoot, ".config/oxlint/package.json"));
1886
- if (!oxlintExists) generateOxlintConfigPackage(files);
1099
+ const oxlintExists = await fileExists$1(join$1(monorepoRoot, ".config/oxlint/package.json"));
1100
+ if (!oxlintExists) renderOxlintConfigPackage(files);
1887
1101
  } else if (linter === "eslint") {
1888
- const eslintPkgExists = await fileExists(
1889
- join$1(monorepoRoot, ".config/eslint/package.json")
1890
- );
1102
+ const eslintPkgExists = await fileExists$1(join$1(monorepoRoot, ".config/eslint/package.json"));
1891
1103
  if (!eslintPkgExists) {
1892
1104
  if (existingConfigs.eslintConfigPath) {
1893
1105
  await migrateEslintConfig(monorepoRoot, files);
1894
1106
  } else {
1895
- generateEslintConfigPackage(files);
1107
+ renderEslintConfigPackage(files);
1896
1108
  }
1897
1109
  }
1898
1110
  }
1899
1111
  if (formatter === "oxfmt") {
1900
- const oxfmtExists = await fileExists(join$1(monorepoRoot, ".config/oxfmt/package.json"));
1901
- if (!oxfmtExists) generateOxfmtConfigPackage(files);
1112
+ const oxfmtExists = await fileExists$1(join$1(monorepoRoot, ".config/oxfmt/package.json"));
1113
+ if (!oxfmtExists) renderOxfmtConfigPackage(files);
1902
1114
  } else if (formatter === "prettier") {
1903
- const prettierPkgExists = await fileExists(
1904
- join$1(monorepoRoot, ".config/prettier/package.json")
1905
- );
1115
+ const prettierPkgExists = await fileExists$1(join$1(monorepoRoot, ".config/prettier/package.json"));
1906
1116
  if (!prettierPkgExists) {
1907
1117
  if (existingConfigs.prettierConfigPath) {
1908
1118
  await migratePrettierConfig(monorepoRoot, files);
1909
1119
  } else {
1910
- generatePrettierConfigPackage(files);
1120
+ renderPrettierConfigPackage(files);
1911
1121
  }
1912
1122
  }
1913
1123
  }
@@ -1941,8 +1151,8 @@ async function handleFixCommand(options) {
1941
1151
  }
1942
1152
  for (const [filePath, file] of Object.entries(files)) {
1943
1153
  const fullPath = join$1(monorepoRoot, filePath);
1944
- await mkdir$1(dirname$1(fullPath), { recursive: true });
1945
- await writeFile$1(fullPath, file.content);
1154
+ await mkdir(dirname(fullPath), { recursive: true });
1155
+ await writeFile(fullPath, file.content);
1946
1156
  }
1947
1157
  await ensureConfigInWorkspace(monorepoRoot);
1948
1158
  if (existingConfigs.eslintConfigPath && linter === "eslint") {
@@ -1958,15 +1168,13 @@ async function handleFixCommand(options) {
1958
1168
  }
1959
1169
  }
1960
1170
  spinner.stop(color.green("\u2713") + " Workspace fixed!");
1961
- const generated = Object.keys(files).filter((f) => f.endsWith("package.json"));
1171
+ const generated = Object.keys(files).filter((file) => file.endsWith("package.json"));
1962
1172
  for (const pkgFile of generated) {
1963
1173
  const pkgName = pkgFile.replace("/package.json", "");
1964
1174
  console.log(color.dim(` Generated ${pkgName}`));
1965
1175
  }
1966
- const vscodeSettingsExists = await fileExists(join$1(monorepoRoot, ".vscode/settings.json"));
1967
- const vscodeExtensionsExists = await fileExists(
1968
- join$1(monorepoRoot, ".vscode/extensions.json")
1969
- );
1176
+ const vscodeSettingsExists = await fileExists$1(join$1(monorepoRoot, ".vscode/settings.json"));
1177
+ const vscodeExtensionsExists = await fileExists$1(join$1(monorepoRoot, ".vscode/extensions.json"));
1970
1178
  const vscodeExists = vscodeSettingsExists && vscodeExtensionsExists;
1971
1179
  if (!vscodeExists) {
1972
1180
  let addVscode = false;
@@ -1981,34 +1189,35 @@ async function handleFixCommand(options) {
1981
1189
  }
1982
1190
  if (addVscode) {
1983
1191
  const vscodeFiles = {};
1984
- generateVscodeFiles(vscodeFiles, linter, formatter);
1192
+ renderVscodeFiles(vscodeFiles, linter, formatter);
1985
1193
  for (const [filePath, file] of Object.entries(vscodeFiles)) {
1986
1194
  const fullPath = join$1(monorepoRoot, filePath);
1987
- await mkdir$1(dirname$1(fullPath), { recursive: true });
1988
- await writeFile$1(fullPath, file.content);
1195
+ await mkdir(dirname(fullPath), { recursive: true });
1196
+ await writeFile(fullPath, file.content);
1989
1197
  }
1990
1198
  console.log(color.dim(" Generated .vscode/settings.json"));
1991
1199
  console.log(color.dim(" Generated .vscode/extensions.json"));
1992
1200
  }
1993
1201
  }
1994
- const aiRulesExist = await fileExists(join$1(monorepoRoot, ".ai/workspace.md"));
1202
+ const aiRulesExist = await fileExists$1(join$1(monorepoRoot, ".ai/workspace.md"));
1995
1203
  if (!aiRulesExist) {
1996
- const platforms = await promptForAiPlatforms(isNonInteractive);
1204
+ const platforms = await promptForAiAgentPlatforms(isNonInteractive);
1997
1205
  if (platforms.length > 0) {
1998
1206
  const scope = await getMonorepoScope(monorepoRoot);
1999
1207
  const aiFilesOutput = {};
2000
- generateAiFiles(aiFilesOutput, {
1208
+ renderAiFiles(aiFilesOutput, {
2001
1209
  name: scope,
2002
1210
  packageManager: "pnpm",
2003
1211
  linter,
2004
1212
  formatter,
2005
1213
  isMonorepo: true,
1214
+ hasTypecheck: false,
2006
1215
  platforms
2007
1216
  });
2008
1217
  for (const [filePath, file] of Object.entries(aiFilesOutput)) {
2009
1218
  const fullPath = join$1(monorepoRoot, filePath);
2010
- await mkdir$1(dirname$1(fullPath), { recursive: true });
2011
- await writeFile$1(fullPath, file.content);
1219
+ await mkdir(dirname(fullPath), { recursive: true });
1220
+ await writeFile(fullPath, file.content);
2012
1221
  console.log(color.dim(` Generated ${filePath}`));
2013
1222
  }
2014
1223
  }
@@ -2020,290 +1229,800 @@ async function handleFixCommand(options) {
2020
1229
  process.exit(1);
2021
1230
  }
2022
1231
  }
2023
- async function handleMigration(config, target, root, options) {
2024
- const plan = await getMigrationPlan(config, target, root);
2025
- console.log(color.cyan("Migration:"));
2026
- if (plan.fromLinter !== plan.toLinter) {
2027
- console.log(` Linter: ${color.dim(plan.fromLinter)} \u2192 ${color.green(plan.toLinter)}`);
2028
- }
2029
- if (plan.fromFormatter !== plan.toFormatter) {
2030
- console.log(
2031
- ` Formatter: ${color.dim(plan.fromFormatter)} \u2192 ${color.green(plan.toFormatter)}`
2032
- );
1232
+
1233
+ function detectViteTemplate(pkg) {
1234
+ if (!hasPackage(pkg, "vite")) return void 0;
1235
+ if (hasPackage(pkg, "@react-three/fiber")) return "r3f";
1236
+ if (hasPackage(pkg, "react") || hasPackage(pkg, "@vitejs/plugin-react")) return "react";
1237
+ return "vanilla";
1238
+ }
1239
+ function renderExpectedViteConfig(template) {
1240
+ const isReact = template === "react" || template === "r3f";
1241
+ const codeSnippets = isReact ? { "vite-config-import": ["import react from '@vitejs/plugin-react';"] } : {};
1242
+ const viteConfig = {
1243
+ base: "./"
1244
+ };
1245
+ if (isReact) {
1246
+ viteConfig.plugins = ["$raw:react()"];
2033
1247
  }
2034
- console.log();
2035
- console.log(color.cyan("Changes:"));
2036
- for (const change of plan.changes) {
2037
- console.log(formatMigrationChange(change));
1248
+ if (template === "r3f") {
1249
+ viteConfig.resolve = { dedupe: ["three"] };
2038
1250
  }
2039
- if (plan.subPackageUpdates.length > 0) {
2040
- console.log();
2041
- console.log(color.cyan(`Sub-packages (${plan.subPackageUpdates.length}):`));
2042
- for (const update of plan.subPackageUpdates) {
2043
- const changes = [
2044
- ...update.remove.map((d) => `-${d}`),
2045
- ...update.add.map((d) => `+${d}`)
2046
- ].join(", ");
2047
- console.log(` ~ ${update.path} (${changes})`);
1251
+ return renderViteConfig({ viteConfig, codeSnippets });
1252
+ }
1253
+ async function detectCurrentConfig(root, isMonorepo = true) {
1254
+ let name = root.split(/[/\\]/).pop() ?? "workspace";
1255
+ let packageManager = "pnpm";
1256
+ let hasTypecheck = false;
1257
+ let viteTemplate;
1258
+ try {
1259
+ const pkgPath = join(root, "package.json");
1260
+ const content = await readFile$1(pkgPath, "utf-8");
1261
+ const pkgJson = JSON.parse(content);
1262
+ if (pkgJson.name) {
1263
+ name = pkgJson.name.replace(/^@/, "").replace(/\/.*$/, "");
2048
1264
  }
2049
- }
2050
- console.log();
2051
- if (!options.yes) {
2052
- const confirm = await p.confirm({
2053
- message: "Apply migration?",
2054
- initialValue: true
2055
- });
2056
- if (p.isCancel(confirm) || !confirm) {
2057
- console.log(color.dim(" Migration cancelled"));
2058
- process.exit(0);
1265
+ if (pkgJson.packageManager) {
1266
+ packageManager = pkgJson.packageManager.split("@")[0] ?? packageManager;
2059
1267
  }
1268
+ hasTypecheck = pkgJson.scripts?.typecheck != null;
1269
+ viteTemplate = detectViteTemplate(pkgJson);
1270
+ } catch {
2060
1271
  }
2061
- await applyMigration(plan, root);
2062
- const aiWorkspacePath = join$1(root, ".ai/workspace.md");
2063
- const aiRulesExist = await fileExists(aiWorkspacePath);
2064
- if (aiRulesExist) {
2065
- console.log();
2066
- console.log(color.cyan("Updating AI rules..."));
2067
- const scope = await getMonorepoScope(root);
2068
- const existingPlatforms = [];
2069
- if (await fileExists(join$1(root, "AGENTS.md"))) {
2070
- existingPlatforms.push("agents");
2071
- }
2072
- if (await fileExists(join$1(root, "CLAUDE.md"))) {
2073
- existingPlatforms.push("claude");
2074
- }
2075
- const aiFilesOutput = {};
2076
- generateAiFiles(aiFilesOutput, {
2077
- name: scope,
2078
- packageManager: "pnpm",
2079
- linter: plan.toLinter,
2080
- formatter: plan.toFormatter,
2081
- isMonorepo: true,
2082
- platforms: existingPlatforms.length > 0 ? existingPlatforms : ["agents"]
2083
- });
2084
- for (const [filePath, file] of Object.entries(aiFilesOutput)) {
2085
- const fullPath = join$1(root, filePath);
2086
- await mkdir$1(dirname$1(fullPath), { recursive: true });
2087
- await writeFile$1(fullPath, file.content);
2088
- console.log(color.dim(` ${filePath}`));
1272
+ const tooling = await detectTooling(root);
1273
+ const configStrategy = isMonorepo ? void 0 : await detectSinglePackageConfigStrategy(root);
1274
+ return {
1275
+ name,
1276
+ linter: tooling.linter ?? "oxlint",
1277
+ formatter: tooling.formatter ?? "prettier",
1278
+ packageManager,
1279
+ isMonorepo,
1280
+ configStrategy,
1281
+ hasTypecheck,
1282
+ viteTemplate
1283
+ };
1284
+ }
1285
+ async function detectSinglePackageConfigStrategy(root) {
1286
+ const hasStealthConfig = await Promise.all([
1287
+ fileExists(join(root, ".config/tsconfig.app.json")),
1288
+ fileExists(join(root, ".config/tsconfig.node.json")),
1289
+ fileExists(join(root, ".config/prettier.json")),
1290
+ fileExists(join(root, ".config/oxlint.json"))
1291
+ ]).then((matches) => matches.some(Boolean));
1292
+ return hasStealthConfig ? "stealth" : "root";
1293
+ }
1294
+ async function planExpectedFiles(config) {
1295
+ const { name, linter, formatter, packageManager, isMonorepo, configStrategy, hasTypecheck } = config;
1296
+ const versions = linter === "biome" || formatter === "biome" ? await resolveMonorepoRootPackageVersions({ linter, formatter }) : {};
1297
+ const aiFilesMap = {};
1298
+ renderAiFiles(aiFilesMap, {
1299
+ name,
1300
+ packageManager,
1301
+ linter,
1302
+ formatter,
1303
+ isMonorepo,
1304
+ configStrategy,
1305
+ hasTypecheck,
1306
+ platforms: ALL_AI_PLATFORMS
1307
+ });
1308
+ const vscodeFiles = renderVscodeFiles$1({
1309
+ linter,
1310
+ formatter,
1311
+ configStrategy,
1312
+ isMonorepo,
1313
+ packageManager: isPackageManagerName(packageManager) ? packageManager : void 0
1314
+ });
1315
+ const configPackages = {};
1316
+ if (isMonorepo) {
1317
+ renderTypescriptConfigPackage(configPackages);
1318
+ if (linter === "oxlint") {
1319
+ renderOxlintConfigPackage(configPackages);
1320
+ } else if (linter === "eslint") {
1321
+ renderEslintConfigPackage(configPackages);
1322
+ }
1323
+ if (formatter === "oxfmt") {
1324
+ renderOxfmtConfigPackage(configPackages);
1325
+ } else if (formatter === "prettier") {
1326
+ renderPrettierConfigPackage(configPackages);
2089
1327
  }
2090
1328
  }
2091
- console.log();
2092
- console.log(color.green("\u2713") + ` Migrated to ${plan.toLinter}/${plan.toFormatter}`);
2093
- console.log(color.dim(" Run `pnpm install` to update dependencies"));
2094
- process.exit(0);
1329
+ const workspaceConfig = {};
1330
+ const rootConfig = {};
1331
+ rootConfig[".editorconfig"] = renderEditorConfig();
1332
+ rootConfig[".gitignore"] = renderGitignore(isMonorepo ? "workspace-root" : "standalone");
1333
+ rootConfig[".gitattributes"] = {
1334
+ type: "text",
1335
+ content: `* text=auto eol=lf
1336
+ *.{cmd,[cC][mM][dD]} text eol=crlf
1337
+ *.{bat,[bB][aA][tT]} text eol=crlf
1338
+ `
1339
+ };
1340
+ if (!isMonorepo && formatter === "prettier") {
1341
+ rootConfig[configStrategy === "root" ? ".prettierignore" : ".config/prettierignore"] = {
1342
+ type: "text",
1343
+ content: toPrettierIgnoreContent()
1344
+ };
1345
+ }
1346
+ if (!isMonorepo && config.viteTemplate != null) {
1347
+ rootConfig["vite.config.ts"] = renderExpectedViteConfig(config.viteTemplate);
1348
+ }
1349
+ if (linter === "biome" || formatter === "biome") {
1350
+ const biomeVersion = getResolvedPackageVersion(versions, "@biomejs/biome");
1351
+ const biomeConfig = {
1352
+ $schema: `https://biomejs.dev/schemas/${biomeVersion}/schema.json`,
1353
+ vcs: {
1354
+ enabled: true,
1355
+ clientKind: "git",
1356
+ useIgnoreFile: true
1357
+ },
1358
+ linter: {
1359
+ enabled: linter === "biome",
1360
+ rules: {
1361
+ recommended: true
1362
+ }
1363
+ },
1364
+ formatter: {
1365
+ enabled: formatter === "biome"
1366
+ }
1367
+ };
1368
+ rootConfig["biome.json"] = {
1369
+ type: "text",
1370
+ content: JSON.stringify(biomeConfig, null, 2)
1371
+ };
1372
+ }
1373
+ return {
1374
+ "ai-files": aiFilesMap,
1375
+ vscode: vscodeFiles,
1376
+ "package-json": {},
1377
+ "config-packages": configPackages,
1378
+ "tooling-config": {},
1379
+ "workspace-config": workspaceConfig,
1380
+ "root-config": rootConfig
1381
+ };
2095
1382
  }
2096
- async function handleUpdateCommand(options) {
2097
- const monorepoRoot = await detectMonorepoRoot();
2098
- const projectRoot = monorepoRoot ?? await detectPackageRoot();
2099
- if (!projectRoot) {
2100
- console.log(color.red("\u2717") + " Could not find a project root");
2101
- console.log(color.dim(" Run this command from inside a generated project"));
2102
- process.exit(1);
1383
+ async function fileExists(path) {
1384
+ try {
1385
+ await access(path, constants$2.F_OK);
1386
+ return true;
1387
+ } catch {
1388
+ return false;
2103
1389
  }
2104
- const isMonorepo = monorepoRoot != null;
2105
- if (isMonorepo) {
2106
- const { valid, errors } = await validateWorkspace(projectRoot);
2107
- if (!valid) {
2108
- console.log(color.yellow("!") + " Workspace has issues:");
2109
- for (const error of errors) {
2110
- console.log(color.dim(` \u2022 ${error}`));
1390
+ }
1391
+ function stripJsonComments(content) {
1392
+ let output = "";
1393
+ let inString = false;
1394
+ let inLineComment = false;
1395
+ let inBlockComment = false;
1396
+ let escaped = false;
1397
+ for (let index = 0; index < content.length; index++) {
1398
+ const char = content[index];
1399
+ const next = content[index + 1];
1400
+ if (inLineComment) {
1401
+ if (char === "\n" || char === "\r") {
1402
+ inLineComment = false;
1403
+ output += char;
2111
1404
  }
2112
- console.log();
2113
- const shouldFix = options.yes || await p.confirm({
2114
- message: "Run fix first to resolve these issues?",
2115
- initialValue: true
2116
- });
2117
- if (p.isCancel(shouldFix) || !shouldFix) {
2118
- console.log(color.dim(" Run `pnpm create krispya --fix` to fix manually"));
2119
- process.exit(1);
1405
+ continue;
1406
+ }
1407
+ if (inBlockComment) {
1408
+ if (char === "*" && next === "/") {
1409
+ inBlockComment = false;
1410
+ index++;
2120
1411
  }
2121
- const preFixConfig = await detectCurrentConfig(projectRoot);
2122
- const fixOptions = {
2123
- ...options,
2124
- linter: options.linter ?? preFixConfig.linter,
2125
- formatter: options.formatter ?? preFixConfig.formatter
2126
- };
2127
- await handleFixCommand(fixOptions);
1412
+ continue;
2128
1413
  }
2129
- } else if (options.linter || options.formatter) {
2130
- console.log(
2131
- color.yellow("!") + " Linter/formatter migrations in --update are currently monorepo-only"
2132
- );
2133
- console.log(color.dim(" Continuing with standalone shared config updates"));
2134
- console.log();
2135
- }
2136
- const config = await detectCurrentConfig(projectRoot, isMonorepo);
2137
- const targetLinter = options.linter;
2138
- const targetFormatter = options.formatter;
2139
- const migrationTarget = { linter: targetLinter, formatter: targetFormatter };
2140
- if (isMonorepo && needsMigration(config, migrationTarget)) {
2141
- await handleMigration(config, migrationTarget, projectRoot, options);
2142
- return;
2143
- }
2144
- console.log(
2145
- color.cyan("Checking for updates...") + color.dim(` (${config.linter}/${config.formatter})`)
2146
- );
2147
- console.log();
2148
- const expected = await generateExpectedFiles(config);
2149
- const categories = await compareWithDisk(expected, projectRoot);
2150
- const allCategories = categories.filter((c) => c.category !== "workspace-config");
2151
- if (isMonorepo) {
2152
- const workspaceConfigChanges = await getWorkspaceConfigUpdates(projectRoot);
2153
- const workspaceCategory = {
2154
- category: "workspace-config",
2155
- label: "Workspace Config",
2156
- changes: workspaceConfigChanges,
2157
- hasUserModifications: workspaceConfigChanges.some((c) => c.status === "modified")
2158
- };
2159
- if (workspaceConfigChanges.length > 0) {
2160
- const configPkgIndex = allCategories.findIndex((c) => c.category === "config-packages");
2161
- if (configPkgIndex !== -1) {
2162
- allCategories.splice(configPkgIndex + 1, 0, workspaceCategory);
2163
- } else {
2164
- allCategories.push(workspaceCategory);
1414
+ if (inString) {
1415
+ output += char;
1416
+ if (escaped) {
1417
+ escaped = false;
1418
+ } else if (char === "\\") {
1419
+ escaped = true;
1420
+ } else if (char === '"') {
1421
+ inString = false;
2165
1422
  }
1423
+ continue;
1424
+ }
1425
+ if (char === '"') {
1426
+ inString = true;
1427
+ output += char;
1428
+ continue;
1429
+ }
1430
+ if (char === "/" && next === "/") {
1431
+ inLineComment = true;
1432
+ index++;
1433
+ continue;
1434
+ }
1435
+ if (char === "/" && next === "*") {
1436
+ inBlockComment = true;
1437
+ index++;
1438
+ continue;
2166
1439
  }
1440
+ output += char;
2167
1441
  }
2168
- let updatedCount = 0;
2169
- let skippedCount = 0;
2170
- for (const category of allCategories) {
2171
- const newChanges = category.changes.filter((c) => c.status === "added");
2172
- const modifiedChanges = category.changes.filter((c) => c.status === "modified");
2173
- const hasNew = newChanges.length > 0;
2174
- const hasModified = modifiedChanges.length > 0;
2175
- const hasChanges = hasNew || hasModified;
2176
- if (!hasChanges) {
2177
- console.log(color.green("\u2713") + ` ${category.label}: Up to date`);
1442
+ return output;
1443
+ }
1444
+ function stripTrailingJsonCommas(content) {
1445
+ let output = "";
1446
+ let inString = false;
1447
+ let escaped = false;
1448
+ for (let index = 0; index < content.length; index++) {
1449
+ const char = content[index];
1450
+ if (inString) {
1451
+ output += char;
1452
+ if (escaped) {
1453
+ escaped = false;
1454
+ } else if (char === "\\") {
1455
+ escaped = true;
1456
+ } else if (char === '"') {
1457
+ inString = false;
1458
+ }
2178
1459
  continue;
2179
1460
  }
2180
- if (category.category === "ai-files") {
2181
- if (hasNew) {
2182
- console.log(color.cyan(category.label + ":"));
2183
- console.log(color.dim(` ${newChanges.length} AI file(s) can be added`));
2184
- console.log();
2185
- const applyAi = options.yes ? true : await p.confirm({
2186
- message: "Add AI rules?",
2187
- initialValue: true
2188
- });
2189
- if (!p.isCancel(applyAi) && applyAi) {
2190
- await applyUpdates(newChanges, projectRoot);
2191
- console.log(color.green("\u2713") + ` Added ${newChanges.length} AI file(s)`);
2192
- updatedCount++;
2193
- } else {
2194
- console.log(color.dim(` Skipped ${category.label}`));
2195
- skippedCount++;
2196
- }
1461
+ if (char === '"') {
1462
+ inString = true;
1463
+ output += char;
1464
+ continue;
1465
+ }
1466
+ if (char === ",") {
1467
+ let lookahead = index + 1;
1468
+ while (/\s/.test(content[lookahead] ?? "")) lookahead++;
1469
+ if (content[lookahead] === "}" || content[lookahead] === "]") {
1470
+ continue;
2197
1471
  }
2198
- if (hasModified) {
2199
- console.log(color.cyan("AI Files (existing):"));
2200
- for (const change of modifiedChanges) {
2201
- console.log(formatFileChange(change));
2202
- }
2203
- console.log();
2204
- if (options.yes) {
2205
- console.log(color.dim(" (--yes mode: keeping existing AI files)"));
1472
+ }
1473
+ output += char;
1474
+ }
1475
+ return output;
1476
+ }
1477
+ function parseJsonValue(content) {
1478
+ return JSON.parse(stripTrailingJsonCommas(stripJsonComments(content)));
1479
+ }
1480
+ function stableJsonValue(value) {
1481
+ if (Array.isArray(value)) {
1482
+ return value.map(stableJsonValue);
1483
+ }
1484
+ if (value != null && typeof value === "object") {
1485
+ return Object.fromEntries(
1486
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => [key, stableJsonValue(entryValue)])
1487
+ );
1488
+ }
1489
+ return value;
1490
+ }
1491
+ function jsonValuesEqual(currentContent, newContent) {
1492
+ try {
1493
+ return JSON.stringify(stableJsonValue(parseJsonValue(currentContent))) === JSON.stringify(stableJsonValue(parseJsonValue(newContent)));
1494
+ } catch {
1495
+ return false;
1496
+ }
1497
+ }
1498
+ function shouldCompareJsonValues(filePath) {
1499
+ return filePath.endsWith(".json") || filePath.endsWith(".jsonc");
1500
+ }
1501
+ function fileContentsEqual(filePath, currentContent, newContent) {
1502
+ if (shouldCompareJsonValues(filePath) && jsonValuesEqual(currentContent, newContent)) {
1503
+ return true;
1504
+ }
1505
+ return currentContent === newContent;
1506
+ }
1507
+ async function compareWithDisk(expected, root) {
1508
+ const categoryLabels = {
1509
+ "ai-files": "AI Files",
1510
+ "ai-files-install": "Install More AI Files",
1511
+ "ai-files-update": "Update Existing AI Files",
1512
+ vscode: "VS Code",
1513
+ "package-json": "package.json Scripts",
1514
+ "config-packages": "Config Packages",
1515
+ "tooling-config": "Tooling Config",
1516
+ "workspace-config": "Workspace Config",
1517
+ "root-config": "Root Config"
1518
+ };
1519
+ const categories = [];
1520
+ for (const [category, files] of Object.entries(expected)) {
1521
+ const changes = [];
1522
+ for (const [filePath, file] of Object.entries(files)) {
1523
+ if (file.type !== "text") continue;
1524
+ const fullPath = join(root, filePath);
1525
+ const newContent = file.content;
1526
+ if (await fileExists(fullPath)) {
1527
+ const currentContent = await readFile$1(fullPath, "utf-8");
1528
+ if (fileContentsEqual(filePath, currentContent, newContent)) {
1529
+ changes.push({
1530
+ path: filePath,
1531
+ status: "unchanged",
1532
+ currentContent,
1533
+ newContent
1534
+ });
2206
1535
  } else {
2207
- const updateExisting = await p.confirm({
2208
- message: "Update existing AI files to latest template?",
2209
- initialValue: false
1536
+ changes.push({
1537
+ path: filePath,
1538
+ status: "modified",
1539
+ currentContent,
1540
+ newContent
2210
1541
  });
2211
- if (!p.isCancel(updateExisting) && updateExisting) {
2212
- await applyUpdates(modifiedChanges, projectRoot);
2213
- console.log(color.green("\u2713") + " Updated existing AI files");
2214
- }
2215
1542
  }
1543
+ } else {
1544
+ changes.push({
1545
+ path: filePath,
1546
+ status: "added",
1547
+ newContent
1548
+ });
1549
+ }
1550
+ }
1551
+ if (category === "ai-files") {
1552
+ const newAiFiles = changes.filter((change) => change.status === "added");
1553
+ const modifiedAiFiles = changes.filter((change) => change.status === "modified");
1554
+ if (newAiFiles.length > 0) {
1555
+ categories.push({
1556
+ category: "ai-files-install",
1557
+ label: categoryLabels["ai-files-install"],
1558
+ changes: newAiFiles,
1559
+ hasUserModifications: false
1560
+ });
1561
+ }
1562
+ if (modifiedAiFiles.length > 0) {
1563
+ categories.push({
1564
+ category: "ai-files-update",
1565
+ label: categoryLabels["ai-files-update"],
1566
+ changes: modifiedAiFiles,
1567
+ hasUserModifications: true
1568
+ });
2216
1569
  }
2217
- console.log();
2218
1570
  continue;
2219
1571
  }
2220
- let changesToApply = [];
2221
- if (options.yes) {
2222
- console.log(color.cyan(category.label + ":"));
2223
- for (const change of [...newChanges, ...modifiedChanges]) {
2224
- console.log(formatFileChange(change));
1572
+ if (changes.length === 0) continue;
1573
+ const hasUserModifications = changes.some((c) => c.status === "modified");
1574
+ categories.push({
1575
+ category,
1576
+ label: categoryLabels[category],
1577
+ changes,
1578
+ hasUserModifications
1579
+ });
1580
+ }
1581
+ return categories;
1582
+ }
1583
+ function isPackageManagerName(value) {
1584
+ return value === "pnpm" || value === "npm" || value === "yarn";
1585
+ }
1586
+ function hasPackage(pkg, name) {
1587
+ return pkg.dependencies?.[name] != null || pkg.devDependencies?.[name] != null || pkg.peerDependencies?.[name] != null;
1588
+ }
1589
+ function sortPackageMap(packageMap) {
1590
+ return Object.fromEntries(Object.entries(packageMap).sort(([a], [b]) => a.localeCompare(b)));
1591
+ }
1592
+ async function detectTypeScriptPackage(root, pkg) {
1593
+ if (hasPackage(pkg, "typescript")) return true;
1594
+ return await fileExists(join(root, "tsconfig.json")) || await fileExists(join(root, "tsconfig.app.json")) || await fileExists(join(root, ".config/tsconfig.app.json"));
1595
+ }
1596
+ function detectLibraryPackage(pkg) {
1597
+ return pkg.exports != null || pkg.main?.includes("dist") === true || pkg.module?.includes("dist") === true || Array.isArray(pkg.files) && pkg.files.includes("dist");
1598
+ }
1599
+ function getPackageManagerForScripts(config, pkg) {
1600
+ const packageManager = pkg.packageManager?.split("@")[0] ?? config.packageManager;
1601
+ return isPackageManagerName(packageManager) ? packageManager : "pnpm";
1602
+ }
1603
+ function getSinglePackageToolScripts(config) {
1604
+ const isStealth = (config.configStrategy ?? "stealth") === "stealth";
1605
+ const linterScripts = config.linter === "oxlint" ? packageJsonScripts.lint.oxlint(isStealth ? ".config/oxlint.json" : void 0) : config.linter === "eslint" ? packageJsonScripts.lint.eslint(isStealth ? ".config/eslint.config.js" : void 0) : packageJsonScripts.lint.biome(isStealth ? ".config" : void 0);
1606
+ const formatterScripts = config.formatter === "prettier" ? packageJsonScripts.format.prettier(
1607
+ isStealth ? ".config/prettier.json" : void 0,
1608
+ isStealth ? ".config/prettierignore" : void 0
1609
+ ) : config.formatter === "oxfmt" ? packageJsonScripts.format.oxfmt(isStealth ? ".config/oxfmt.json" : "oxfmt.json") : packageJsonScripts.format.biome(isStealth ? ".config" : void 0);
1610
+ return mergePackageJsonScripts(linterScripts, formatterScripts);
1611
+ }
1612
+ function getLibraryBuildScripts(pkg) {
1613
+ if (!detectLibraryPackage(pkg)) return void 0;
1614
+ if (hasPackage(pkg, "tsdown") || pkg.scripts?.build === "tsdown") {
1615
+ return packageJsonScripts.build.tsdown;
1616
+ }
1617
+ return packageJsonScripts.build.unbuild();
1618
+ }
1619
+ function getTestingScripts(pkg) {
1620
+ if (hasPackage(pkg, "vitest") || pkg.scripts?.test === "vitest") {
1621
+ return packageJsonScripts.test.vitest;
1622
+ }
1623
+ return void 0;
1624
+ }
1625
+ function scriptsEqual(left, right) {
1626
+ const leftEntries = Object.entries(left);
1627
+ if (leftEntries.length !== Object.keys(right).length) return false;
1628
+ return leftEntries.every(([key, value]) => right[key] === value);
1629
+ }
1630
+ async function getExpectedPackageScripts(root, config, pkg) {
1631
+ if (config.isMonorepo) {
1632
+ return packageJsonScripts.monorepoRoot(config.linter, config.formatter);
1633
+ }
1634
+ const language = await detectTypeScriptPackage(root, pkg) ? "typescript" : "javascript";
1635
+ const isLibrary = detectLibraryPackage(pkg);
1636
+ const packageManagerName = getPackageManagerForScripts(config, pkg);
1637
+ return mergePackageJsonScripts(
1638
+ resolveDefaultPackageJsonScripts({
1639
+ language,
1640
+ isLibrary,
1641
+ packageManagerName
1642
+ }),
1643
+ getLibraryBuildScripts(pkg),
1644
+ getTestingScripts(pkg),
1645
+ getSinglePackageToolScripts(config)
1646
+ );
1647
+ }
1648
+ async function getExpectedPackageDevDependencies(root, config, pkg) {
1649
+ const nextDevDependencies = { ...pkg.devDependencies };
1650
+ const shouldAddOxlintTypeAwareBackend = config.linter === "oxlint" && (config.isMonorepo || await detectTypeScriptPackage(root, pkg)) && !hasPackage(pkg, "oxlint-tsgolint");
1651
+ if (shouldAddOxlintTypeAwareBackend) {
1652
+ nextDevDependencies["oxlint-tsgolint"] = formatResolvedPackageVersion({}, "oxlint-tsgolint");
1653
+ }
1654
+ return sortPackageMap(nextDevDependencies);
1655
+ }
1656
+ async function getPackageJsonScriptUpdates(root, config) {
1657
+ const packageJsonPath = join(root, "package.json");
1658
+ let currentContent;
1659
+ try {
1660
+ currentContent = await readFile$1(packageJsonPath, "utf-8");
1661
+ } catch {
1662
+ return [];
1663
+ }
1664
+ const pkg = JSON.parse(currentContent);
1665
+ const currentScripts = pkg.scripts ?? {};
1666
+ const expectedScripts = await getExpectedPackageScripts(root, config, pkg);
1667
+ const nextScripts = mergePackageJsonScripts(currentScripts, expectedScripts);
1668
+ const currentDevDependencies = pkg.devDependencies ?? {};
1669
+ const nextDevDependencies = await getExpectedPackageDevDependencies(root, config, pkg);
1670
+ if (scriptsEqual(currentScripts, nextScripts) && scriptsEqual(currentDevDependencies, nextDevDependencies)) {
1671
+ return [
1672
+ {
1673
+ path: "package.json",
1674
+ status: "unchanged",
1675
+ currentContent,
1676
+ newContent: currentContent
2225
1677
  }
2226
- console.log();
2227
- if (category.category === "workspace-config") {
2228
- changesToApply = [...newChanges, ...modifiedChanges];
2229
- if (changesToApply.length > 0) {
2230
- console.log(color.dim(" (--yes mode: applying merge updates)"));
2231
- }
2232
- } else {
2233
- changesToApply = newChanges;
2234
- if (newChanges.length > 0) {
2235
- console.log(color.dim(" (--yes mode: adding new files only)"));
2236
- }
1678
+ ];
1679
+ }
1680
+ const nextPackageJson = {
1681
+ ...pkg,
1682
+ scripts: nextScripts
1683
+ };
1684
+ if (Object.keys(nextDevDependencies).length > 0 || pkg.devDependencies != null) {
1685
+ nextPackageJson.devDependencies = nextDevDependencies;
1686
+ }
1687
+ const newContent = `${JSON.stringify(nextPackageJson, null, 2)}
1688
+ `;
1689
+ return [
1690
+ {
1691
+ path: "package.json",
1692
+ status: "modified",
1693
+ currentContent,
1694
+ newContent
1695
+ }
1696
+ ];
1697
+ }
1698
+ function planSinglePackageOxlintConfig(config) {
1699
+ if (config.linter !== "oxlint" || config.isMonorepo) return void 0;
1700
+ const isStealth = (config.configStrategy ?? "stealth") === "stealth";
1701
+ const path = isStealth ? ".config/oxlint.json" : "oxlint.json";
1702
+ const oxlintConfig = renderOxlintConfig({
1703
+ schemaPath: isStealth ? "../node_modules/oxlint/configuration_schema.json" : "./node_modules/oxlint/configuration_schema.json",
1704
+ typescript: true
1705
+ });
1706
+ return {
1707
+ path,
1708
+ status: "added",
1709
+ newContent: `${JSON.stringify(oxlintConfig, null, 2)}
1710
+ `
1711
+ };
1712
+ }
1713
+ async function getOxlintConfigReplacementUpdates(root, config) {
1714
+ const expected = planSinglePackageOxlintConfig(config);
1715
+ if (expected == null) return [];
1716
+ const fullPath = join(root, expected.path);
1717
+ let currentContent;
1718
+ try {
1719
+ currentContent = await readFile$1(fullPath, "utf-8");
1720
+ } catch {
1721
+ return [expected];
1722
+ }
1723
+ if (fileContentsEqual(expected.path, currentContent, expected.newContent)) {
1724
+ return [
1725
+ {
1726
+ ...expected,
1727
+ status: "unchanged",
1728
+ currentContent,
1729
+ newContent: currentContent
2237
1730
  }
2238
- } else if (hasNew && hasModified) {
2239
- const allChanges = [...newChanges, ...modifiedChanges];
2240
- const selectedFiles = await p.multiselect({
2241
- message: `${category.label} (+ new, ~ changed)`,
2242
- options: allChanges.map((change) => ({
2243
- value: change.path,
2244
- label: change.status === "added" ? `+ ${change.path}` : `~ ${change.path}`
2245
- })),
2246
- initialValues: newChanges.map((c) => c.path),
2247
- // Pre-select new files
2248
- required: false
1731
+ ];
1732
+ }
1733
+ return [
1734
+ {
1735
+ ...expected,
1736
+ status: "modified",
1737
+ currentContent
1738
+ }
1739
+ ];
1740
+ }
1741
+ async function getWorkspaceConfigUpdates(root) {
1742
+ const workspacePath = join(root, "pnpm-workspace.yaml");
1743
+ const changes = [];
1744
+ let currentContent = "";
1745
+ let exists = false;
1746
+ try {
1747
+ currentContent = await readFile$1(workspacePath, "utf-8");
1748
+ exists = true;
1749
+ } catch {
1750
+ }
1751
+ if (!exists) {
1752
+ const newContent = `manage-package-manager-versions: true
1753
+
1754
+ packages:
1755
+ - '.config/*'
1756
+ - 'apps/*'
1757
+ - 'packages/*'
1758
+
1759
+ onlyBuiltDependencies:
1760
+ - esbuild
1761
+ `;
1762
+ changes.push({
1763
+ path: "pnpm-workspace.yaml",
1764
+ status: "added",
1765
+ newContent
1766
+ });
1767
+ return changes;
1768
+ }
1769
+ let updatedContent = currentContent;
1770
+ let needsUpdate = false;
1771
+ if (!currentContent.includes("manage-package-manager-versions")) {
1772
+ updatedContent = `manage-package-manager-versions: true
1773
+
1774
+ ${updatedContent}`;
1775
+ needsUpdate = true;
1776
+ }
1777
+ if (!currentContent.includes("onlyBuiltDependencies")) {
1778
+ updatedContent = `${updatedContent.trimEnd()}
1779
+
1780
+ onlyBuiltDependencies:
1781
+ - esbuild
1782
+ `;
1783
+ needsUpdate = true;
1784
+ }
1785
+ if (!currentContent.includes(".config/*")) {
1786
+ const lines = updatedContent.split("\n");
1787
+ const packagesIndex = lines.findIndex((line) => line.trim().startsWith("packages:"));
1788
+ if (packagesIndex !== -1) {
1789
+ lines.splice(packagesIndex + 1, 0, " - '.config/*'");
1790
+ updatedContent = lines.join("\n");
1791
+ needsUpdate = true;
1792
+ }
1793
+ }
1794
+ if (needsUpdate) {
1795
+ changes.push({
1796
+ path: "pnpm-workspace.yaml",
1797
+ status: "modified",
1798
+ currentContent,
1799
+ newContent: updatedContent
1800
+ });
1801
+ } else {
1802
+ changes.push({
1803
+ path: "pnpm-workspace.yaml",
1804
+ status: "unchanged",
1805
+ currentContent,
1806
+ newContent: currentContent
1807
+ });
1808
+ }
1809
+ return changes;
1810
+ }
1811
+ async function applyUpdates(changes, root) {
1812
+ for (const change of changes) {
1813
+ if (change.status === "unchanged") continue;
1814
+ const fullPath = join(root, change.path);
1815
+ await mkdir$1(dirname$1(fullPath), { recursive: true });
1816
+ await writeFile$1(fullPath, change.newContent);
1817
+ }
1818
+ }
1819
+ function formatFileChange(change) {
1820
+ const icon = change.status === "added" ? "+" : change.status === "modified" ? "~" : "=";
1821
+ return ` ${icon} ${change.path}`;
1822
+ }
1823
+
1824
+ const UPDATE_CATEGORY_ORDER = [
1825
+ "root-config",
1826
+ "config-packages",
1827
+ "tooling-config",
1828
+ "workspace-config",
1829
+ "vscode",
1830
+ "package-json",
1831
+ "ai-files",
1832
+ "ai-files-install",
1833
+ "ai-files-update"
1834
+ ];
1835
+ function isMergeUpdateCategory(category) {
1836
+ return category === "workspace-config" || category === "package-json";
1837
+ }
1838
+ function getUpdateHint(category, status) {
1839
+ if (status === "added") return "new file";
1840
+ if (category === "package-json") return "merge update";
1841
+ if (category === "workspace-config") return "merge update";
1842
+ return "changed; overwrites if selected";
1843
+ }
1844
+ function isSelectableFileChange(change) {
1845
+ return change.status === "added" || change.status === "modified";
1846
+ }
1847
+ function getInitialUpdateSelections(category) {
1848
+ return category.changes.filter(
1849
+ (change) => change.status === "added" || change.status === "modified" && isMergeUpdateCategory(category.category)
1850
+ ).map((change) => change.path);
1851
+ }
1852
+ async function promptForUpdateSelections(category) {
1853
+ const selectableChanges = category.changes.filter(isSelectableFileChange);
1854
+ const selectedFiles = await p.multiselect({
1855
+ message: category.label,
1856
+ options: selectableChanges.map((change) => ({
1857
+ value: change.path,
1858
+ label: change.path,
1859
+ hint: getUpdateHint(category.category, change.status)
1860
+ })),
1861
+ initialValues: getInitialUpdateSelections(category),
1862
+ required: false
1863
+ });
1864
+ if (p.isCancel(selectedFiles)) {
1865
+ p.cancel("Operation cancelled.");
1866
+ process.exit(0);
1867
+ }
1868
+ return selectableChanges.filter((change) => selectedFiles.includes(change.path));
1869
+ }
1870
+ async function promptForAiFileInstall(category) {
1871
+ const newChanges = category.changes.filter((change) => change.status === "added");
1872
+ const fileList = newChanges.map((change) => change.path).join(", ");
1873
+ const shouldInstall = await p.confirm({
1874
+ message: fileList ? `Install more AI files? (${fileList})` : "Install more AI files?",
1875
+ initialValue: true
1876
+ });
1877
+ if (p.isCancel(shouldInstall)) {
1878
+ p.cancel("Operation cancelled.");
1879
+ process.exit(0);
1880
+ }
1881
+ return shouldInstall ? newChanges : [];
1882
+ }
1883
+ function getCategoryOrder(category) {
1884
+ const index = UPDATE_CATEGORY_ORDER.indexOf(category);
1885
+ return index === -1 ? UPDATE_CATEGORY_ORDER.length : index;
1886
+ }
1887
+ function orderUpdateCategories(categories) {
1888
+ return [...categories].sort(
1889
+ (left, right) => getCategoryOrder(left.category) - getCategoryOrder(right.category)
1890
+ );
1891
+ }
1892
+ async function collectUpdateCategories(projectRoot, config, isMonorepo) {
1893
+ const expected = await planExpectedFiles(config);
1894
+ const categories = await compareWithDisk(expected, projectRoot);
1895
+ const allCategories = categories.filter((category) => category.category !== "workspace-config");
1896
+ const packageJsonScriptChanges = await getPackageJsonScriptUpdates(projectRoot, config);
1897
+ if (packageJsonScriptChanges.length > 0) {
1898
+ allCategories.push({
1899
+ category: "package-json",
1900
+ label: "package.json",
1901
+ changes: packageJsonScriptChanges,
1902
+ hasUserModifications: packageJsonScriptChanges.some((change) => change.status === "modified")
1903
+ });
1904
+ }
1905
+ const oxlintConfigChanges = await getOxlintConfigReplacementUpdates(projectRoot, config);
1906
+ if (oxlintConfigChanges.length > 0) {
1907
+ allCategories.push({
1908
+ category: "tooling-config",
1909
+ label: "Tooling Config",
1910
+ changes: oxlintConfigChanges,
1911
+ hasUserModifications: oxlintConfigChanges.some((change) => change.status === "modified")
1912
+ });
1913
+ }
1914
+ if (isMonorepo) {
1915
+ const workspaceConfigChanges = await getWorkspaceConfigUpdates(projectRoot);
1916
+ if (workspaceConfigChanges.length > 0) {
1917
+ allCategories.push({
1918
+ category: "workspace-config",
1919
+ label: "Workspace Config",
1920
+ changes: workspaceConfigChanges,
1921
+ hasUserModifications: workspaceConfigChanges.some((change) => change.status === "modified")
2249
1922
  });
2250
- if (p.isCancel(selectedFiles)) {
2251
- p.cancel("Operation cancelled.");
2252
- process.exit(0);
1923
+ }
1924
+ }
1925
+ return orderUpdateCategories(allCategories);
1926
+ }
1927
+ async function processUpdateCategory(category, projectRoot, options) {
1928
+ const newChanges = category.changes.filter((change) => change.status === "added");
1929
+ const modifiedChanges = category.changes.filter((change) => change.status === "modified");
1930
+ const hasNew = newChanges.length > 0;
1931
+ const hasModified = modifiedChanges.length > 0;
1932
+ const hasChanges = hasNew || hasModified;
1933
+ if (!hasChanges) {
1934
+ console.log(color.green("\u2713") + ` ${category.label}: Up to date`);
1935
+ return "unchanged";
1936
+ }
1937
+ let changesToApply = [];
1938
+ if (options.yes) {
1939
+ console.log(color.cyan(`${category.label}:`));
1940
+ for (const change of [...newChanges, ...modifiedChanges]) {
1941
+ console.log(formatFileChange(change));
1942
+ }
1943
+ console.log();
1944
+ if (isMergeUpdateCategory(category.category)) {
1945
+ changesToApply = [...newChanges, ...modifiedChanges];
1946
+ if (changesToApply.length > 0) {
1947
+ console.log(color.dim(" (--yes mode: applying merge updates)"));
2253
1948
  }
2254
- if (selectedFiles.length > 0) {
2255
- changesToApply = allChanges.filter((c) => selectedFiles.includes(c.path));
1949
+ } else {
1950
+ changesToApply = newChanges;
1951
+ if (newChanges.length > 0) {
1952
+ console.log(color.dim(" (--yes mode: adding new files only)"));
2256
1953
  }
2257
- } else if (hasNew) {
2258
- console.log(color.cyan(category.label + ":"));
2259
- for (const change of newChanges) {
2260
- console.log(formatFileChange(change));
1954
+ }
1955
+ } else {
1956
+ changesToApply = category.category === "ai-files-install" ? await promptForAiFileInstall(category) : await promptForUpdateSelections(category);
1957
+ }
1958
+ if (changesToApply.length > 0) {
1959
+ await applyUpdates(changesToApply, projectRoot);
1960
+ const addedCount = changesToApply.filter((change) => change.status === "added").length;
1961
+ const updatedFilesCount = changesToApply.filter((change) => change.status === "modified").length;
1962
+ const parts = [];
1963
+ if (addedCount > 0) parts.push(`added ${addedCount}`);
1964
+ if (updatedFilesCount > 0) parts.push(`updated ${updatedFilesCount}`);
1965
+ console.log(color.green("\u2713") + ` ${category.label}: ${parts.join(", ")}`);
1966
+ console.log();
1967
+ return "updated";
1968
+ }
1969
+ console.log(color.dim(` Skipped ${category.label}`));
1970
+ console.log();
1971
+ return "skipped";
1972
+ }
1973
+ async function handleUpdateCommand(options, handleFixCommand) {
1974
+ const monorepoRoot = await detectMonorepoRoot();
1975
+ const projectRoot = monorepoRoot ?? await detectPackageRoot();
1976
+ if (!projectRoot) {
1977
+ console.log(color.red("\u2717") + " Could not find a project root");
1978
+ console.log(color.dim(" Run this command from inside a generated project"));
1979
+ process.exit(1);
1980
+ }
1981
+ const isMonorepo = monorepoRoot != null;
1982
+ if (isMonorepo) {
1983
+ const { valid, errors } = await validateWorkspace(projectRoot);
1984
+ if (!valid) {
1985
+ console.log(color.yellow("!") + " Workspace has issues:");
1986
+ for (const error of errors) {
1987
+ console.log(color.dim(` \u2022 ${error}`));
2261
1988
  }
2262
1989
  console.log();
2263
- const shouldAdd = await p.confirm({
2264
- message: `Add ${newChanges.length} new file(s)?`,
1990
+ const shouldFix = options.yes || await p.confirm({
1991
+ message: "Run fix first to resolve these issues?",
2265
1992
  initialValue: true
2266
1993
  });
2267
- if (p.isCancel(shouldAdd)) {
2268
- p.cancel("Operation cancelled.");
2269
- process.exit(0);
2270
- }
2271
- if (shouldAdd) {
2272
- changesToApply = newChanges;
2273
- }
2274
- } else if (hasModified) {
2275
- console.log(color.cyan(category.label + ":"));
2276
- for (const change of modifiedChanges) {
2277
- console.log(formatFileChange(change));
1994
+ if (p.isCancel(shouldFix) || !shouldFix) {
1995
+ console.log(color.dim(" Run `pnpm create krispya --fix` to fix manually"));
1996
+ process.exit(1);
2278
1997
  }
2279
- console.log();
2280
- const shouldUpdate = await p.confirm({
2281
- message: `Update ${modifiedChanges.length} file(s)? (will overwrite)`,
2282
- initialValue: false
1998
+ const preFixConfig = await detectCurrentConfig(projectRoot);
1999
+ await handleFixCommand({
2000
+ ...options,
2001
+ linter: options.linter ?? preFixConfig.linter,
2002
+ formatter: options.formatter ?? preFixConfig.formatter
2283
2003
  });
2284
- if (p.isCancel(shouldUpdate)) {
2285
- p.cancel("Operation cancelled.");
2286
- process.exit(0);
2287
- }
2288
- if (shouldUpdate) {
2289
- changesToApply = modifiedChanges;
2290
- }
2291
- }
2292
- if (changesToApply.length > 0) {
2293
- await applyUpdates(changesToApply, projectRoot);
2294
- const addedCount = changesToApply.filter((c) => c.status === "added").length;
2295
- const updatedFilesCount = changesToApply.filter((c) => c.status === "modified").length;
2296
- const parts = [];
2297
- if (addedCount > 0) parts.push(`added ${addedCount}`);
2298
- if (updatedFilesCount > 0) parts.push(`updated ${updatedFilesCount}`);
2299
- console.log(color.green("\u2713") + ` ${category.label}: ${parts.join(", ")}`);
2300
- updatedCount++;
2301
- } else {
2302
- console.log(color.dim(` Skipped ${category.label}`));
2303
- skippedCount++;
2304
2004
  }
2005
+ }
2006
+ const config = await detectCurrentConfig(projectRoot, isMonorepo);
2007
+ if (options.linter || options.formatter) {
2008
+ console.log(
2009
+ color.yellow("!") + " Linter/formatter migration is not part of --update in this architecture pass"
2010
+ );
2011
+ console.log(color.dim(" Continuing with updates for the detected current tooling"));
2305
2012
  console.log();
2306
2013
  }
2014
+ console.log(
2015
+ color.cyan("Checking for updates...") + color.dim(` (${config.linter}/${config.formatter})`)
2016
+ );
2017
+ console.log();
2018
+ const categories = await collectUpdateCategories(projectRoot, config, isMonorepo);
2019
+ let updatedCount = 0;
2020
+ let skippedCount = 0;
2021
+ for (const category of categories) {
2022
+ const result = await processUpdateCategory(category, projectRoot, options);
2023
+ if (result === "updated") updatedCount++;
2024
+ if (result === "skipped") skippedCount++;
2025
+ }
2307
2026
  if (updatedCount === 0 && skippedCount === 0) {
2308
2027
  console.log(color.green("\u2713") + " Everything is up to date!");
2309
2028
  } else if (updatedCount > 0) {
@@ -2316,7 +2035,103 @@ async function handleUpdateCommand(options) {
2316
2035
  }
2317
2036
  process.exit(0);
2318
2037
  }
2319
- async function handleWorkspaceCommand(name, options) {
2038
+
2039
+ const require$2 = createRequire(import.meta.url);
2040
+ async function createPackageInWorkspace(monorepoRoot, packageManager, inheritedSettings, scope, writeGeneratedFiles) {
2041
+ const workspaceDirectories = await parseWorkspaceDirectories(monorepoRoot);
2042
+ const defaultDirectories = ["apps", "packages"];
2043
+ const hasCustomDirectories = workspaceDirectories.length > 0 && !workspaceDirectories.every((dir) => defaultDirectories.includes(dir));
2044
+ const packageType = await promptForInitialPackage();
2045
+ if (packageType === "skip") {
2046
+ return false;
2047
+ }
2048
+ const defaultDir = packageType === "app" ? "apps" : "packages";
2049
+ const packageNameInput = await p.text({
2050
+ message: "Package name?",
2051
+ initialValue: `@${scope}/`,
2052
+ validate: (value) => {
2053
+ const validationError = validatePackageName(value);
2054
+ if (validationError) return validationError;
2055
+ const dirName = value.includes("/") ? value.split("/").pop() : value;
2056
+ if (!dirName) return "Package name is required";
2057
+ if (!hasCustomDirectories) {
2058
+ const targetPath = join$1(monorepoRoot, defaultDir, dirName);
2059
+ try {
2060
+ const { statSync } = require$2("fs");
2061
+ statSync(targetPath);
2062
+ return `Directory ${defaultDir}/${dirName} already exists`;
2063
+ } catch {
2064
+ }
2065
+ }
2066
+ }
2067
+ });
2068
+ if (p.isCancel(packageNameInput)) {
2069
+ return false;
2070
+ }
2071
+ const scopedName = packageNameInput;
2072
+ const shortName = scopedName.includes("/") ? scopedName.split("/").pop() : scopedName;
2073
+ const packageOptions = await promptForPackageOptions(scopedName, packageType, inheritedSettings);
2074
+ let targetDir = defaultDir;
2075
+ if (hasCustomDirectories && workspaceDirectories.length > 0) {
2076
+ const dirChoice = await p.select({
2077
+ message: "Target directory",
2078
+ options: workspaceDirectories.map((dir) => ({
2079
+ value: dir,
2080
+ label: dir
2081
+ })),
2082
+ initialValue: workspaceDirectories.includes(defaultDir) ? defaultDir : workspaceDirectories[0]
2083
+ });
2084
+ if (p.isCancel(dirChoice)) {
2085
+ return false;
2086
+ }
2087
+ targetDir = dirChoice;
2088
+ const targetPath = join$1(monorepoRoot, targetDir, shortName);
2089
+ try {
2090
+ const { statSync } = require$2("fs");
2091
+ statSync(targetPath);
2092
+ p.log.error(`Directory ${targetDir}/${shortName} already exists`);
2093
+ return false;
2094
+ } catch {
2095
+ }
2096
+ }
2097
+ const relativePkgPath = join$1(targetDir, shortName);
2098
+ const workspaceRoot = calculateWorkspaceRoot(relativePkgPath);
2099
+ packageOptions.workspaceRoot = workspaceRoot;
2100
+ packageOptions.name = scopedName;
2101
+ const workspacePackages = packageType === "app" ? await getWorkspacePackages(monorepoRoot) : [];
2102
+ if (workspacePackages.length > 0) {
2103
+ const selectedDeps = await p.multiselect({
2104
+ message: "Add workspace dependencies?",
2105
+ options: workspacePackages.map((name) => ({ value: name, label: name })),
2106
+ required: false
2107
+ });
2108
+ if (!p.isCancel(selectedDeps) && selectedDeps.length > 0) {
2109
+ packageOptions.workspaceDependencies = selectedDeps;
2110
+ }
2111
+ }
2112
+ const outputPath = join$1(monorepoRoot, relativePkgPath);
2113
+ const spinner = p.spinner();
2114
+ spinner.start("Creating package...");
2115
+ try {
2116
+ const { files } = await planProject(resolveProjectPlanInput(packageOptions));
2117
+ await writeGeneratedFiles(outputPath, files);
2118
+ spinner.stop(color.green.inverse(` \u2713 Package created at ${relativePkgPath}! `));
2119
+ const addAnother = await p.select({
2120
+ message: "Add another package?",
2121
+ options: [
2122
+ { value: "no", label: "No, I'm done" },
2123
+ { value: "yes", label: "Yes, add another" }
2124
+ ],
2125
+ initialValue: "no"
2126
+ });
2127
+ return !p.isCancel(addAnother) && addAnother === "yes";
2128
+ } catch (error) {
2129
+ spinner.stop("Failed to create package");
2130
+ p.log.error(String(error));
2131
+ return false;
2132
+ }
2133
+ }
2134
+ async function handleWorkspaceCommand(name, options, writeGeneratedFiles) {
2320
2135
  const monorepoRoot = await detectMonorepoRoot();
2321
2136
  if (!monorepoRoot) {
2322
2137
  console.error(color.red("Error:") + " --workspace flag requires being inside a monorepo");
@@ -2337,7 +2152,7 @@ async function handleWorkspaceCommand(name, options) {
2337
2152
  const scopedName = name.startsWith("@") ? name : `@${scope}/${name}`;
2338
2153
  const fullPackagePath = join$1(monorepoRoot, targetDir, name);
2339
2154
  try {
2340
- await access$1(fullPackagePath, constants$2.F_OK);
2155
+ await access$1(fullPackagePath, constants$1.F_OK);
2341
2156
  console.error(color.red("Error:") + ` Directory ${targetDir}/${name} already exists`);
2342
2157
  process.exit(1);
2343
2158
  } catch {
@@ -2353,7 +2168,7 @@ async function handleWorkspaceCommand(name, options) {
2353
2168
  const isLibrary = projectType === "library";
2354
2169
  const relativePkgPath = join$1(targetDir, name);
2355
2170
  const workspaceRoot = calculateWorkspaceRoot(relativePkgPath);
2356
- const generateOptions = {
2171
+ const projectOptions = {
2357
2172
  name: scopedName,
2358
2173
  projectType,
2359
2174
  libraryBundler: isLibrary ? options.bundler ?? "unbuild" : void 0,
@@ -2379,12 +2194,9 @@ async function handleWorkspaceCommand(name, options) {
2379
2194
  triplex: options.triplex ? {} : void 0
2380
2195
  }
2381
2196
  };
2382
- generateOptions.packageManager = await resolvePackageManager(generateOptions);
2383
- generateOptions.engine = await resolveEngine(generateOptions);
2384
- generateOptions.versions = await resolveProjectPackageVersions(generateOptions);
2385
2197
  console.log(color.cyan("Creating") + ` ${scopedName} in ${targetDir}/${name}...`);
2386
2198
  try {
2387
- const files = generate(generateOptions);
2199
+ const { files } = await planProject(resolveProjectPlanInput(projectOptions));
2388
2200
  await writeGeneratedFiles(fullPackagePath, files);
2389
2201
  console.log(color.green("\u2713") + ` Created ${scopedName} at ${targetDir}/${name}`);
2390
2202
  process.exit(0);
@@ -2394,68 +2206,126 @@ async function handleWorkspaceCommand(name, options) {
2394
2206
  process.exit(1);
2395
2207
  }
2396
2208
  }
2397
- async function handleMonorepoCreation(generateOptions, isNonInteractive) {
2398
- const { generateMonorepo } = await import('./chunks/index.mjs').then(function (n) { return n.J; });
2399
- const packageManager = getPackageManagerName(generateOptions.packageManager);
2400
- generateOptions.packageManager = await resolvePackageManager(generateOptions);
2401
- generateOptions.engine = await resolveEngine(generateOptions);
2402
- generateOptions.versions = await resolveMonorepoRootPackageVersions({
2403
- linter: generateOptions.linter ?? "oxlint",
2404
- formatter: generateOptions.formatter ?? "prettier",
2405
- engine: generateOptions.engine,
2406
- versions: generateOptions.versions
2209
+ async function handleInteractiveMonorepoMode(monorepoRoot, writeGeneratedFiles) {
2210
+ const choice = await p.select({
2211
+ message: "Detected monorepo workspace",
2212
+ options: [
2213
+ { value: "add", label: "Add new package to this workspace" },
2214
+ { value: "standalone", label: "Create single-package workspace" }
2215
+ ],
2216
+ initialValue: "add"
2407
2217
  });
2408
- const aiPlatforms = await promptForAiPlatforms(isNonInteractive);
2409
- const projectPath = join$1(cwd(), generateOptions.name);
2218
+ if (p.isCancel(choice)) {
2219
+ p.cancel("Operation cancelled.");
2220
+ process.exit(0);
2221
+ }
2222
+ if (choice === "add") {
2223
+ const inheritedSettings = await detectWorkspaceSettings(monorepoRoot);
2224
+ const hasSettings = Object.values(inheritedSettings).some(Boolean);
2225
+ if (hasSettings) {
2226
+ const settingsInfo = [
2227
+ inheritedSettings.linter && `linter: ${inheritedSettings.linter}`,
2228
+ inheritedSettings.formatter && `formatter: ${inheritedSettings.formatter}`,
2229
+ inheritedSettings.packageManager && `pm: ${inheritedSettings.packageManager.name}`
2230
+ ].filter(Boolean).join(", ");
2231
+ p.log.info(`Using workspace settings (${settingsInfo})`);
2232
+ }
2233
+ const scope = await getMonorepoScope(monorepoRoot);
2234
+ let addMore = true;
2235
+ while (addMore) {
2236
+ addMore = await createPackageInWorkspace(
2237
+ monorepoRoot,
2238
+ inheritedSettings.packageManager?.name ?? "pnpm",
2239
+ inheritedSettings,
2240
+ scope,
2241
+ writeGeneratedFiles
2242
+ );
2243
+ }
2244
+ p.note([`cd ${monorepoRoot}`, "pnpm install", "pnpm run dev"].join("\n"), "Next steps");
2245
+ p.outro(color.green("Happy coding! \u2728"));
2246
+ process.exit(0);
2247
+ }
2248
+ }
2249
+
2250
+ const require$1 = createRequire(import.meta.url);
2251
+ const pkg = require$1("../package.json");
2252
+ const META_OPTIONS = [
2253
+ "clearConfig",
2254
+ "configPath",
2255
+ "check",
2256
+ "fix",
2257
+ "update",
2258
+ "yes",
2259
+ "workspace",
2260
+ "path",
2261
+ "dir"
2262
+ ];
2263
+ function hasConfigOptions(options) {
2264
+ return Object.keys(options).some(
2265
+ (key) => !META_OPTIONS.includes(key)
2266
+ );
2267
+ }
2268
+ async function writeGeneratedFiles(basePath, files) {
2269
+ const filePaths = Object.keys(files).sort();
2270
+ for (const filePath of filePaths) {
2271
+ const fullFilePath = join$1(basePath, filePath);
2272
+ await mkdir(dirname(fullFilePath), { recursive: true });
2273
+ const file = files[filePath];
2274
+ if (file.type === "text") {
2275
+ await writeFile(fullFilePath, file.content);
2276
+ } else {
2277
+ const response = await fetch(file.url);
2278
+ await writeFile(fullFilePath, response.body);
2279
+ }
2280
+ }
2281
+ }
2282
+ async function handleMonorepoCreation(projectOptions, isNonInteractive) {
2283
+ const packageManager = getPackageManagerName(projectOptions.packageManager);
2284
+ const projectPath = join$1(cwd(), projectOptions.name);
2410
2285
  const spinner = p.spinner();
2411
2286
  spinner.start("Creating monorepo workspace...");
2412
2287
  try {
2413
- const { files } = generateMonorepo({
2414
- name: generateOptions.name,
2415
- linter: generateOptions.linter ?? "oxlint",
2416
- formatter: generateOptions.formatter ?? "prettier",
2417
- packageManager: generateOptions.packageManager ?? {
2288
+ const planInput = resolveWorkspacePlanInput({
2289
+ name: projectOptions.name,
2290
+ linter: projectOptions.linter ?? "oxlint",
2291
+ formatter: projectOptions.formatter ?? "prettier",
2292
+ packageManager: projectOptions.packageManager ?? {
2418
2293
  name: packageManager
2419
2294
  },
2420
- pnpmManageVersions: generateOptions.pnpmManageVersions,
2421
- engine: generateOptions.engine,
2422
- versions: generateOptions.versions,
2423
- aiPlatforms: aiPlatforms.length > 0 ? aiPlatforms : void 0
2295
+ pnpmManageVersions: projectOptions.pnpmManageVersions,
2296
+ engine: projectOptions.engine,
2297
+ versions: projectOptions.versions,
2298
+ aiPlatforms: projectOptions.aiPlatforms,
2299
+ ide: projectOptions.ide ?? "vscode"
2424
2300
  });
2425
- const filePaths = Object.keys(files).sort();
2426
- for (const filePath of filePaths) {
2427
- const fullFilePath = join$1(projectPath, filePath);
2428
- await mkdir$1(dirname$1(fullFilePath), { recursive: true });
2429
- const file = files[filePath];
2430
- if (file.type === "text") {
2431
- await writeFile$1(fullFilePath, file.content);
2432
- }
2433
- }
2301
+ const { files } = await planWorkspace(planInput);
2302
+ await writeGeneratedFiles(projectPath, files);
2434
2303
  spinner.stop(color.green.inverse(" \u2713 Monorepo workspace created! "));
2435
2304
  if (isNonInteractive) {
2436
2305
  process.exit(0);
2437
2306
  }
2438
2307
  const newWorkspaceSettings = {
2439
- linter: generateOptions.linter,
2440
- formatter: generateOptions.formatter,
2441
- packageManager: generateOptions.packageManager ?? {
2308
+ linter: projectOptions.linter,
2309
+ formatter: projectOptions.formatter,
2310
+ packageManager: projectOptions.packageManager ?? {
2442
2311
  name: packageManager
2443
2312
  },
2444
- engine: generateOptions.engine,
2445
- pnpmManageVersions: generateOptions.pnpmManageVersions
2313
+ engine: projectOptions.engine,
2314
+ pnpmManageVersions: projectOptions.pnpmManageVersions
2446
2315
  };
2447
- const scope = generateOptions.name;
2316
+ const scope = projectOptions.name;
2448
2317
  let addMore = true;
2449
2318
  while (addMore) {
2450
2319
  addMore = await createPackageInWorkspace(
2451
2320
  projectPath,
2452
2321
  packageManager,
2453
2322
  newWorkspaceSettings,
2454
- scope
2323
+ scope,
2324
+ writeGeneratedFiles
2455
2325
  );
2456
2326
  }
2457
2327
  const nextSteps = [
2458
- `cd ${generateOptions.name}`,
2328
+ `cd ${projectOptions.name}`,
2459
2329
  `${packageManager} install`,
2460
2330
  `${packageManager} run dev`
2461
2331
  ].join("\n");
@@ -2468,33 +2338,26 @@ async function handleMonorepoCreation(generateOptions, isNonInteractive) {
2468
2338
  process.exit(1);
2469
2339
  }
2470
2340
  }
2471
- async function handleStandaloneProjectCreation(generateOptions, isNonInteractive) {
2472
- const base = generateOptions.template ? getBaseTemplate(generateOptions.template) : "vanilla";
2341
+ async function handleSingleWorkspaceCreation(projectOptions, isNonInteractive) {
2342
+ const base = projectOptions.template ? getBaseTemplate(projectOptions.template) : "vanilla";
2473
2343
  const defaultFallbackName = base === "vanilla" ? "vanilla-app" : base === "react" ? "react-app" : "react-three-app";
2474
- generateOptions.name ??= defaultFallbackName;
2475
- const aiPlatforms = await promptForAiPlatforms(isNonInteractive);
2476
- if (aiPlatforms.length > 0) {
2477
- generateOptions.aiPlatforms = aiPlatforms;
2478
- }
2479
- const packageManager = getPackageManagerName(generateOptions.packageManager);
2480
- const isLibrary = generateOptions.projectType === "library";
2481
- generateOptions.packageManager = await resolvePackageManager(generateOptions);
2482
- generateOptions.engine = await resolveEngine(generateOptions);
2483
- generateOptions.versions = await resolveProjectPackageVersions(generateOptions);
2484
- const projectPath = join$1(cwd(), generateOptions.name);
2344
+ projectOptions.name ??= defaultFallbackName;
2345
+ const packageManager = getPackageManagerName(projectOptions.packageManager);
2346
+ const projectPath = join$1(cwd(), projectOptions.name);
2485
2347
  const spinner = p.spinner();
2486
2348
  spinner.start("Creating project...");
2487
2349
  try {
2488
- const files = generate(generateOptions);
2350
+ const planInput = resolveProjectPlanInput(projectOptions);
2351
+ const { files } = await planProject(planInput);
2489
2352
  await writeGeneratedFiles(projectPath, files);
2490
2353
  spinner.stop(color.green.inverse(" \u2713 Project created! "));
2491
2354
  if (isNonInteractive) process.exit(0);
2492
- const nextSteps = isLibrary ? [
2493
- `cd ${generateOptions.name}`,
2355
+ const nextSteps = projectOptions.projectType === "library" ? [
2356
+ `cd ${projectOptions.name}`,
2494
2357
  `${packageManager} install`,
2495
2358
  `${packageManager} run build`
2496
2359
  ].join("\n") : [
2497
- `cd ${generateOptions.name}`,
2360
+ `cd ${projectOptions.name}`,
2498
2361
  `${packageManager} install`,
2499
2362
  `${packageManager} run dev`
2500
2363
  ].join("\n");
@@ -2506,45 +2369,6 @@ async function handleStandaloneProjectCreation(generateOptions, isNonInteractive
2506
2369
  process.exit(1);
2507
2370
  }
2508
2371
  }
2509
- async function handleInteractiveMonorepoMode(monorepoRoot) {
2510
- const choice = await p.select({
2511
- message: "Detected monorepo workspace",
2512
- options: [
2513
- { value: "add", label: "Add new package to this workspace" },
2514
- { value: "standalone", label: "Create standalone project" }
2515
- ],
2516
- initialValue: "add"
2517
- });
2518
- if (p.isCancel(choice)) {
2519
- p.cancel("Operation cancelled.");
2520
- process.exit(0);
2521
- }
2522
- if (choice === "add") {
2523
- const inheritedSettings = await detectWorkspaceSettings(monorepoRoot);
2524
- const hasSettings = Object.values(inheritedSettings).some(Boolean);
2525
- if (hasSettings) {
2526
- const settingsInfo = [
2527
- inheritedSettings.linter && `linter: ${inheritedSettings.linter}`,
2528
- inheritedSettings.formatter && `formatter: ${inheritedSettings.formatter}`,
2529
- inheritedSettings.packageManager && `pm: ${inheritedSettings.packageManager.name}`
2530
- ].filter(Boolean).join(", ");
2531
- p.log.info(`Using workspace settings (${settingsInfo})`);
2532
- }
2533
- const scope = await getMonorepoScope(monorepoRoot);
2534
- let addMore = true;
2535
- while (addMore) {
2536
- addMore = await createPackageInWorkspace(
2537
- monorepoRoot,
2538
- inheritedSettings.packageManager?.name ?? "pnpm",
2539
- inheritedSettings,
2540
- scope
2541
- );
2542
- }
2543
- p.note([`cd ${monorepoRoot}`, "pnpm install", "pnpm run dev"].join("\n"), "Next steps");
2544
- p.outro(color.green("Happy coding! \u2728"));
2545
- process.exit(0);
2546
- }
2547
- }
2548
2372
  async function main() {
2549
2373
  const program = new Command().name("create-krispya").description("CLI for creating Vanilla, React, and React Three Fiber projects").argument("[name]", "name for the project").option("--type <type>", "project type: app or library (default: app)").option(
2550
2374
  "--bundler <bundler>",
@@ -2552,7 +2376,7 @@ async function main() {
2552
2376
  ).option(
2553
2377
  "--template <type>",
2554
2378
  "project template: vanilla, vanilla-js, react, react-js, r3f, r3f-js (default: vanilla)"
2555
- ).option("--linter <type>", "linter: eslint, oxlint, or biome (default: oxlint)").option("--formatter <type>", "formatter: prettier, oxfmt, or biome (default: prettier)").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("--package-manager <manager>", "specify package manager (e.g. npm, yarn, pnpm)").option(
2379
+ ).option("--linter <type>", "linter: eslint, oxlint, or biome (default: oxlint)").option("--formatter <type>", "formatter: prettier, oxfmt, or biome (default: prettier)").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("--package-manager <manager>", "specify package manager (e.g. npm, yarn, pnpm)").option("--ide <ide>", "IDE files: vscode or none (default: vscode)").option(
2556
2380
  "--pnpm-manage-versions",
2557
2381
  "enable manage-package-manager-versions in pnpm-workspace.yaml (default: true)"
2558
2382
  ).option(
@@ -2561,10 +2385,7 @@ async function main() {
2561
2385
  ).option(
2562
2386
  "--node-version <version>",
2563
2387
  'set Node.js version for engines.node field (default: "latest")'
2564
- ).option("--workspace", "Add package to current monorepo workspace (non-interactive)").option("--dir <directory>", "Target directory for --workspace (default: apps/ or packages/)").option("--clear-config", "Clear saved preferences").option("--config-path", "Print the path to the config file").option("--check", "Check if current directory is in a valid monorepo workspace").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(
2565
- "--path <directory>",
2566
- "Run in specified directory instead of current working directory"
2567
- ).action(async (name, options) => {
2388
+ ).option("--workspace", "Add package to current monorepo workspace (non-interactive)").option("--dir <directory>", "Target directory for --workspace (default: apps/ or packages/)").option("--clear-config", "Clear saved preferences").option("--config-path", "Print the path to the config file").option("--check", "Check if current directory is in a valid monorepo workspace").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("--path <directory>", "Run in specified directory instead of current working directory").action(async (name, options) => {
2568
2389
  if (options.path) {
2569
2390
  process.chdir(options.path);
2570
2391
  }
@@ -2577,6 +2398,10 @@ async function main() {
2577
2398
  console.log(getConfigPath());
2578
2399
  process.exit(0);
2579
2400
  }
2401
+ if (options.ide && !["vscode", "none"].includes(options.ide)) {
2402
+ console.error(color.red("Error:") + ' --ide must be "vscode" or "none"');
2403
+ process.exit(1);
2404
+ }
2580
2405
  if (name?.startsWith("-")) {
2581
2406
  switch (name) {
2582
2407
  case "--version":
@@ -2618,37 +2443,36 @@ async function main() {
2618
2443
  await handleFixCommand(options);
2619
2444
  }
2620
2445
  if (options.update) {
2621
- await handleUpdateCommand(options);
2446
+ await handleUpdateCommand(options, handleFixCommand);
2622
2447
  }
2623
2448
  if (options.dir && !options.workspace) {
2624
2449
  console.error(color.red("Error:") + " --dir requires --workspace flag");
2625
- console.log(
2626
- color.dim(" Example: pnpm create krispya my-lib --workspace --dir examples")
2627
- );
2450
+ console.log(color.dim(" Example: pnpm create krispya my-lib --workspace --dir examples"));
2628
2451
  process.exit(1);
2629
2452
  }
2630
2453
  if (options.workspace) {
2631
- await handleWorkspaceCommand(name, options);
2454
+ await handleWorkspaceCommand(name, options, writeGeneratedFiles);
2632
2455
  }
2633
2456
  console.clear();
2634
2457
  p.intro(color.bgCyan(color.black(` create-krispya v${pkg.version} `)));
2635
2458
  const monorepoRoot = await detectMonorepoRoot();
2636
2459
  if (monorepoRoot && !hasConfigOptions(options)) {
2637
- await handleInteractiveMonorepoMode(monorepoRoot);
2460
+ await handleInteractiveMonorepoMode(monorepoRoot, writeGeneratedFiles);
2638
2461
  }
2639
- let generateOptions;
2462
+ let projectOptions;
2640
2463
  if (options.yes) {
2641
2464
  const template = options.template ?? "vanilla";
2642
2465
  const baseTemplate = getBaseTemplate(template);
2643
2466
  const defaultName = getDefaultProjectName(template);
2644
2467
  const projectType = options.type ?? "app";
2645
- generateOptions = {
2468
+ projectOptions = {
2646
2469
  name: name || defaultName,
2647
2470
  projectType,
2648
2471
  libraryBundler: projectType === "library" ? options.bundler ?? "unbuild" : void 0,
2649
2472
  template,
2650
2473
  linter: options.linter ?? "oxlint",
2651
2474
  formatter: options.formatter ?? "prettier",
2475
+ ide: options.ide ?? "vscode",
2652
2476
  ...baseTemplate === "r3f" && {
2653
2477
  drei: options.drei ? {} : void 0,
2654
2478
  handle: options.handle ? {} : void 0,
@@ -2675,6 +2499,7 @@ async function main() {
2675
2499
  linter: options.linter,
2676
2500
  formatter: options.formatter,
2677
2501
  packageManager: options.packageManager,
2502
+ ide: options.ide,
2678
2503
  engine: options.nodeVersion ? { name: "node", version: options.nodeVersion } : void 0,
2679
2504
  pnpmManageVersions: options.pnpmManageVersions,
2680
2505
  drei: options.drei,
@@ -2690,13 +2515,17 @@ async function main() {
2690
2515
  triplex: options.triplex,
2691
2516
  viverse: options.viverse
2692
2517
  } : void 0;
2693
- generateOptions = await promptForOptions(name, presets);
2518
+ projectOptions = await promptForOptions(name, presets);
2694
2519
  }
2695
2520
  const isNonInteractive = options.yes ?? false;
2696
- if (generateOptions.projectType === "monorepo") {
2697
- await handleMonorepoCreation(generateOptions, isNonInteractive);
2521
+ const aiAgentPlatforms = await promptForAiAgentPlatforms(isNonInteractive);
2522
+ if (aiAgentPlatforms.length > 0) {
2523
+ projectOptions.aiPlatforms = aiAgentPlatforms;
2524
+ }
2525
+ if (projectOptions.projectType === "monorepo") {
2526
+ await handleMonorepoCreation(projectOptions, isNonInteractive);
2698
2527
  } else {
2699
- await handleStandaloneProjectCreation(generateOptions, isNonInteractive);
2528
+ await handleSingleWorkspaceCreation(projectOptions, isNonInteractive);
2700
2529
  }
2701
2530
  });
2702
2531
  await program.parseAsync();