create-krispya 0.9.0 → 0.10.0

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