create-krispya 0.8.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,43 +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 { spawn } from 'child_process';
12
- 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';
13
11
  import Conf from 'conf';
14
- import { readFile, mkdir, writeFile, rm, access, readdir, constants as constants$1 } from 'fs/promises';
15
- import { constants } from 'fs';
16
- import { join, dirname } from 'path';
17
-
18
- const editorNames = {
19
- cursor: "Cursor",
20
- code: "VS Code",
21
- webstorm: "WebStorm",
22
- skip: "Skip"
23
- };
24
- function openInEditor(editor, path, reuseWindow) {
25
- return new Promise((resolve, reject) => {
26
- const isWindows = process.platform === "win32";
27
- const useReuseFlag = reuseWindow && (editor === "cursor" || editor === "code");
28
- const args = useReuseFlag ? ["-r", path] : [path];
29
- const child = isWindows ? spawn(`${editor} ${useReuseFlag ? "-r " : ""}"${path}"`, {
30
- detached: true,
31
- stdio: "ignore",
32
- shell: true
33
- }) : spawn(editor, args, {
34
- detached: true,
35
- stdio: "ignore"
36
- });
37
- child.on("error", reject);
38
- child.unref();
39
- setTimeout(resolve, 100);
40
- });
41
- }
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';
42
16
 
43
17
  function formatConfigSummary(options, inherited) {
44
18
  const lines = [];
@@ -95,6 +69,9 @@ function formatConfigSummary(options, inherited) {
95
69
  const testing = options.testing ?? (projectType === "library" ? "vitest" : "none");
96
70
  lines.push(formatRow("Testing", testing));
97
71
  if (!inherited) {
72
+ lines.push(
73
+ formatRow("Editor config", options.ide === "none" ? "generic" : "generic + vscode")
74
+ );
98
75
  const configStrategy = options.configStrategy ?? "stealth";
99
76
  lines.push(formatRow("Config strategy", configStrategy));
100
77
  }
@@ -114,7 +91,7 @@ function formatConfigSummary(options, inherited) {
114
91
  options.viverse && "viverse"
115
92
  ].filter(Boolean);
116
93
  lines.push("");
117
- lines.push(color.dim("Integrations"));
94
+ lines.push(color.dim("Features"));
118
95
  for (let i = 0; i < integrationNames.length; i += 2) {
119
96
  const left = `${color.green("\u25CF")} ${integrationNames[i]}`;
120
97
  const right = integrationNames[i + 1] ? `${color.green("\u25CF")} ${integrationNames[i + 1]}` : "";
@@ -143,24 +120,13 @@ function formatMonorepoConfigSummary(options) {
143
120
  }
144
121
  lines.push(formatRow("Linter", options.linter));
145
122
  lines.push(formatRow("Formatter", options.formatter));
123
+ lines.push(formatRow("Editor config", options.ide === "none" ? "generic" : "generic + vscode"));
146
124
  return lines.join("\n");
147
125
  }
148
126
 
149
127
  const config = new Conf({
150
128
  projectName: "create-krispya"
151
129
  });
152
- function getPreferredEditor() {
153
- return config.get("preferredEditor");
154
- }
155
- function setPreferredEditor(editor) {
156
- config.set("preferredEditor", editor);
157
- }
158
- function getReuseWindow() {
159
- return config.get("reuseWindow") ?? false;
160
- }
161
- function setReuseWindow(reuse) {
162
- config.set("reuseWindow", reuse);
163
- }
164
130
  function getAiPlatforms() {
165
131
  return config.get("aiPlatforms");
166
132
  }
@@ -173,11 +139,57 @@ function clearConfig() {
173
139
  function getConfigPath() {
174
140
  return config.path;
175
141
  }
176
- function getCustomTemplates() {
177
- return config.get("customTemplates") ?? {};
178
- }
179
142
 
180
- 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) {
181
193
  const baseTemplate = getBaseTemplate(template);
182
194
  const base = {
183
195
  name,
@@ -191,26 +203,13 @@ function getDefaultOptions(template, name, projectType = "app", libraryBundler,
191
203
  formatter: inheritedSettings?.formatter ?? "prettier",
192
204
  // Libraries get vitest by default, apps don't
193
205
  testing: projectType === "library" ? "vitest" : "none",
194
- configStrategy: getConfigStrategy()
206
+ configStrategy: getConfigStrategy(),
207
+ ide: "vscode"
208
+ };
209
+ return {
210
+ ...base,
211
+ ...baseTemplate === "r3f" ? getR3fIntegrationFlags(features) : {}
195
212
  };
196
- if (baseTemplate === "r3f" && integrations) {
197
- return {
198
- ...base,
199
- drei: integrations.includes("drei") ? {} : void 0,
200
- handle: integrations.includes("handle") ? {} : void 0,
201
- leva: integrations.includes("leva") ? {} : void 0,
202
- postprocessing: integrations.includes("postprocessing") ? {} : void 0,
203
- rapier: integrations.includes("rapier") ? {} : void 0,
204
- xr: integrations.includes("xr") ? {} : void 0,
205
- uikit: integrations.includes("uikit") ? {} : void 0,
206
- offscreen: integrations.includes("offscreen") ? {} : void 0,
207
- zustand: integrations.includes("zustand") ? {} : void 0,
208
- koota: integrations.includes("koota") ? {} : void 0,
209
- triplex: integrations.includes("triplex") ? {} : void 0,
210
- viverse: integrations.includes("viverse") ? {} : void 0
211
- };
212
- }
213
- return base;
214
213
  }
215
214
  function getDefaultProjectName(template) {
216
215
  const base = getBaseTemplate(template);
@@ -224,38 +223,10 @@ function getDefaultProjectName(template) {
224
223
  }
225
224
  }
226
225
  async function promptForR3fIntegrations(presets) {
227
- const initialValues = [];
228
- if (presets) {
229
- if (presets.drei) initialValues.push("drei");
230
- if (presets.handle) initialValues.push("handle");
231
- if (presets.leva) initialValues.push("leva");
232
- if (presets.postprocessing) initialValues.push("postprocessing");
233
- if (presets.rapier) initialValues.push("rapier");
234
- if (presets.xr) initialValues.push("xr");
235
- if (presets.uikit) initialValues.push("uikit");
236
- if (presets.offscreen) initialValues.push("offscreen");
237
- if (presets.zustand) initialValues.push("zustand");
238
- if (presets.koota) initialValues.push("koota");
239
- if (presets.triplex) initialValues.push("triplex");
240
- if (presets.viverse) initialValues.push("viverse");
241
- }
242
226
  const selected = await p.multiselect({
243
- message: "R3F integrations",
244
- options: [
245
- { value: "drei", label: "Drei" },
246
- { value: "handle", label: "Handle" },
247
- { value: "leva", label: "Leva" },
248
- { value: "postprocessing", label: "Postprocessing" },
249
- { value: "rapier", label: "Rapier" },
250
- { value: "xr", label: "XR" },
251
- { value: "uikit", label: "UIKit" },
252
- { value: "offscreen", label: "Offscreen" },
253
- { value: "zustand", label: "Zustand" },
254
- { value: "koota", label: "Koota" },
255
- { value: "triplex", label: "Triplex" },
256
- { value: "viverse", label: "Viverse" }
257
- ],
258
- initialValues: initialValues.length > 0 ? initialValues : ["drei"],
227
+ message: "R3F features",
228
+ options: [...R3F_INTEGRATION_OPTIONS],
229
+ initialValues: getInitialR3fIntegrations(presets),
259
230
  required: false
260
231
  });
261
232
  if (p.isCancel(selected)) {
@@ -264,7 +235,7 @@ async function promptForR3fIntegrations(presets) {
264
235
  }
265
236
  return selected;
266
237
  }
267
- async function promptForCustomization(template, name, projectType, integrations, inheritedSettings, presets) {
238
+ async function promptForCustomization(template, name, projectType, features, inheritedSettings, presets) {
268
239
  let libraryBundler;
269
240
  if (projectType === "library") {
270
241
  const bundler = await p.select({
@@ -399,6 +370,18 @@ async function promptForCustomization(template, name, projectType, integrations,
399
370
  p.cancel("Operation cancelled.");
400
371
  process.exit(0);
401
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
+ }
402
385
  const baseTemplate = getBaseTemplate(template);
403
386
  const finalTemplate = language === "javascript" ? `${baseTemplate}-js` : baseTemplate;
404
387
  const base = {
@@ -412,26 +395,13 @@ async function promptForCustomization(template, name, projectType, integrations,
412
395
  linter,
413
396
  formatter,
414
397
  testing,
415
- configStrategy: configStrategyChoice
398
+ configStrategy: configStrategyChoice,
399
+ ide: ideChoice
400
+ };
401
+ return {
402
+ ...base,
403
+ ...baseTemplate === "r3f" ? getR3fIntegrationFlags(features) : {}
416
404
  };
417
- if (baseTemplate === "r3f" && integrations) {
418
- return {
419
- ...base,
420
- drei: integrations.includes("drei") ? {} : void 0,
421
- handle: integrations.includes("handle") ? {} : void 0,
422
- leva: integrations.includes("leva") ? {} : void 0,
423
- postprocessing: integrations.includes("postprocessing") ? {} : void 0,
424
- rapier: integrations.includes("rapier") ? {} : void 0,
425
- xr: integrations.includes("xr") ? {} : void 0,
426
- uikit: integrations.includes("uikit") ? {} : void 0,
427
- offscreen: integrations.includes("offscreen") ? {} : void 0,
428
- zustand: integrations.includes("zustand") ? {} : void 0,
429
- koota: integrations.includes("koota") ? {} : void 0,
430
- triplex: integrations.includes("triplex") ? {} : void 0,
431
- viverse: integrations.includes("viverse") ? {} : void 0
432
- };
433
- }
434
- return base;
435
405
  }
436
406
  async function promptForInitialPackage() {
437
407
  const choice = await p.select({
@@ -457,7 +427,8 @@ function getDefaultMonorepoOptions(name) {
457
427
  pnpmManageVersions: true,
458
428
  engine: { name: "node", version: "latest" },
459
429
  linter: "oxlint",
460
- formatter: "prettier"
430
+ formatter: "prettier",
431
+ ide: "vscode"
461
432
  };
462
433
  }
463
434
  async function promptForMonorepoCustomization(name, presets) {
@@ -510,6 +481,18 @@ async function promptForMonorepoCustomization(name, presets) {
510
481
  p.cancel("Operation cancelled.");
511
482
  process.exit(0);
512
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
+ }
513
496
  return {
514
497
  name,
515
498
  projectType: "monorepo",
@@ -517,7 +500,8 @@ async function promptForMonorepoCustomization(name, presets) {
517
500
  packageManager: { name: "pnpm" },
518
501
  pnpmManageVersions: managePnpm,
519
502
  linter,
520
- formatter
503
+ formatter,
504
+ ide
521
505
  };
522
506
  }
523
507
  async function promptForMonorepo(workspaceName, presets) {
@@ -525,6 +509,7 @@ async function promptForMonorepo(workspaceName, presets) {
525
509
  if (presets) {
526
510
  if (presets.linter) defaultOptions.linter = presets.linter;
527
511
  if (presets.formatter) defaultOptions.formatter = presets.formatter;
512
+ if (presets.ide) defaultOptions.ide = presets.ide;
528
513
  if (presets.engine) defaultOptions.engine = presets.engine;
529
514
  if (presets.pnpmManageVersions !== void 0)
530
515
  defaultOptions.pnpmManageVersions = presets.pnpmManageVersions;
@@ -536,23 +521,12 @@ async function promptForMonorepo(workspaceName, presets) {
536
521
  packageManager: getPackageManagerName(defaultOptions.packageManager),
537
522
  pnpmManageVersions: defaultOptions.pnpmManageVersions,
538
523
  linter: defaultOptions.linter ?? "oxlint",
539
- formatter: defaultOptions.formatter ?? "prettier"
524
+ formatter: defaultOptions.formatter ?? "prettier",
525
+ ide: defaultOptions.ide ?? "vscode"
540
526
  }),
541
527
  "Workspace Configuration"
542
528
  );
543
- const proceed = await p.select({
544
- message: "Proceed with these settings?",
545
- options: [
546
- { value: "continue", label: "Yes, continue" },
547
- { value: "customize", label: "No, customize settings" }
548
- ],
549
- initialValue: "continue"
550
- });
551
- if (p.isCancel(proceed)) {
552
- p.cancel("Operation cancelled.");
553
- process.exit(0);
554
- }
555
- if (proceed === "continue") {
529
+ if (await promptForProceed()) {
556
530
  return defaultOptions;
557
531
  }
558
532
  return promptForMonorepoCustomization(workspaceName, presets);
@@ -579,7 +553,7 @@ async function promptForOptions(name, presets) {
579
553
  options: [
580
554
  { value: "app", label: "Application" },
581
555
  { value: "library", label: "Library" },
582
- { value: "monorepo", label: "Monorepo" }
556
+ { value: "monorepo", label: "Monorepo", hint: "experimental" }
583
557
  ],
584
558
  initialValue: presets?.type ?? "app"
585
559
  });
@@ -592,41 +566,6 @@ async function promptForOptions(name, presets) {
592
566
  }
593
567
  return promptForPackageOptions(projectName, projectType, void 0, presets);
594
568
  }
595
- function customTemplateToOptions(customTemplate, name, projectType, inheritedSettings) {
596
- const baseTemplate = customTemplate.baseTemplate;
597
- const template = baseTemplate;
598
- const base = {
599
- name,
600
- template,
601
- projectType,
602
- packageManager: inheritedSettings?.packageManager ?? { name: "pnpm" },
603
- pnpmManageVersions: inheritedSettings?.pnpmManageVersions ?? true,
604
- engine: inheritedSettings?.engine ?? { name: "node", version: "latest" },
605
- linter: inheritedSettings?.linter ?? customTemplate.linter,
606
- formatter: inheritedSettings?.formatter ?? customTemplate.formatter,
607
- testing: customTemplate.testing,
608
- configStrategy: customTemplate.configStrategy ?? getConfigStrategy()
609
- };
610
- if (baseTemplate === "r3f" && customTemplate.integrations) {
611
- const integrations = customTemplate.integrations;
612
- return {
613
- ...base,
614
- drei: integrations.includes("drei") ? {} : void 0,
615
- handle: integrations.includes("handle") ? {} : void 0,
616
- leva: integrations.includes("leva") ? {} : void 0,
617
- postprocessing: integrations.includes("postprocessing") ? {} : void 0,
618
- rapier: integrations.includes("rapier") ? {} : void 0,
619
- xr: integrations.includes("xr") ? {} : void 0,
620
- uikit: integrations.includes("uikit") ? {} : void 0,
621
- offscreen: integrations.includes("offscreen") ? {} : void 0,
622
- zustand: integrations.includes("zustand") ? {} : void 0,
623
- koota: integrations.includes("koota") ? {} : void 0,
624
- triplex: integrations.includes("triplex") ? {} : void 0,
625
- viverse: integrations.includes("viverse") ? {} : void 0
626
- };
627
- }
628
- return base;
629
- }
630
569
  function presetsToInheritedSettings(presets) {
631
570
  if (!presets) return void 0;
632
571
  return {
@@ -638,798 +577,138 @@ function presetsToInheritedSettings(presets) {
638
577
  };
639
578
  }
640
579
  async function promptForPackageOptions(projectName, projectType, inheritedSettings, presets) {
641
- const builtInOptions = [
642
- { value: "vanilla", label: "Vanilla" },
643
- { value: "react", label: "React" },
644
- { value: "r3f", label: "React Three Fiber" }
645
- ];
646
- const customTemplates = getCustomTemplates();
647
- const customOptions = Object.keys(customTemplates).map((name) => ({
648
- value: `custom:${name}`,
649
- label: name,
650
- hint: "saved template"
651
- }));
652
- const allOptions = [...builtInOptions, ...customOptions];
653
580
  const templateSelection = await p.select({
654
581
  message: "Select a template",
655
- options: allOptions,
656
- 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"
657
588
  });
658
589
  if (p.isCancel(templateSelection)) {
659
590
  p.cancel("Operation cancelled.");
660
591
  process.exit(0);
661
592
  }
662
- const selection = templateSelection;
663
- if (selection.startsWith("custom:")) {
664
- const customName = selection.slice(7);
665
- const customTemplate = customTemplates[customName];
666
- const defaultOptions2 = customTemplateToOptions(
667
- customTemplate,
668
- projectName,
669
- projectType,
670
- inheritedSettings
671
- );
672
- const configTitle2 = inheritedSettings ? `Template: ${customName} (using workspace settings)` : `Template: ${customName}`;
673
- p.note(formatConfigSummary(defaultOptions2, inheritedSettings), configTitle2);
674
- const proceed2 = await p.select({
675
- message: "Proceed with these settings?",
676
- options: [
677
- { value: "continue", label: "Yes, continue" },
678
- { value: "customize", label: "No, customize settings" }
679
- ],
680
- initialValue: "continue"
681
- });
682
- if (p.isCancel(proceed2)) {
683
- p.cancel("Operation cancelled.");
684
- process.exit(0);
685
- }
686
- if (proceed2 === "continue") {
687
- return defaultOptions2;
688
- }
689
- return promptForCustomization(
690
- customTemplate.baseTemplate,
691
- projectName,
692
- projectType,
693
- customTemplate.integrations,
694
- inheritedSettings
695
- );
696
- }
697
- const template = selection;
593
+ const template = templateSelection;
698
594
  const baseTemplate = getBaseTemplate(template);
699
- let integrations;
595
+ let features;
700
596
  if (baseTemplate === "r3f") {
701
- integrations = await promptForR3fIntegrations(presets);
597
+ features = await promptForR3fIntegrations(presets);
702
598
  }
703
599
  const defaultOptions = getDefaultOptions(
704
600
  template,
705
601
  projectName,
706
602
  projectType,
707
603
  presets?.bundler,
708
- integrations,
604
+ features,
709
605
  inheritedSettings ?? presetsToInheritedSettings(presets)
710
606
  );
607
+ if (presets?.ide && !inheritedSettings) {
608
+ defaultOptions.ide = presets.ide;
609
+ }
711
610
  const configTitle = inheritedSettings ? "Template Configuration (using workspace settings)" : "Template Configuration";
712
611
  p.note(formatConfigSummary(defaultOptions, inheritedSettings), configTitle);
713
- const proceed = await p.select({
714
- message: "Proceed with these settings?",
715
- options: [
716
- { value: "continue", label: "Yes, continue" },
717
- { value: "customize", label: "No, customize settings" }
718
- ],
719
- initialValue: "continue"
720
- });
721
- if (p.isCancel(proceed)) {
722
- p.cancel("Operation cancelled.");
723
- process.exit(0);
724
- }
725
- if (proceed === "continue") {
612
+ if (await promptForProceed()) {
726
613
  return defaultOptions;
727
614
  }
728
615
  return promptForCustomization(
729
616
  template,
730
617
  projectName,
731
618
  projectType,
732
- integrations,
619
+ features,
733
620
  inheritedSettings,
734
621
  presets
735
622
  );
736
623
  }
737
624
 
738
- async function detectCurrentConfig(root, isMonorepo = true) {
739
- let name = root.split(/[/\\]/).pop() ?? "workspace";
740
- let packageManager = "pnpm";
741
- try {
742
- const pkgPath = join(root, "package.json");
743
- const content = await readFile(pkgPath, "utf-8");
744
- const pkgJson = JSON.parse(content);
745
- if (pkgJson.name) {
746
- 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 [];
747
638
  }
748
- if (pkgJson.packageManager) {
749
- packageManager = pkgJson.packageManager.split("@")[0] ?? packageManager;
639
+ if (useDefault) {
640
+ return savedPlatforms;
750
641
  }
751
- } catch {
752
642
  }
753
- const tooling = await detectTooling(root);
754
- const configStrategy = isMonorepo ? void 0 : await detectStandaloneConfigStrategy(root);
755
- return {
756
- name,
757
- linter: tooling.linter ?? "oxlint",
758
- formatter: tooling.formatter ?? "prettier",
759
- packageManager,
760
- isMonorepo,
761
- configStrategy
762
- };
763
- }
764
- async function detectStandaloneConfigStrategy(root) {
765
- const hasStealthConfig = await Promise.all([
766
- fileExists$1(join(root, ".config/tsconfig.app.json")),
767
- fileExists$1(join(root, ".config/tsconfig.node.json")),
768
- fileExists$1(join(root, ".config/prettier.json")),
769
- fileExists$1(join(root, ".config/oxlint.json"))
770
- ]).then((matches) => matches.some(Boolean));
771
- return hasStealthConfig ? "stealth" : "root";
772
- }
773
- async function generateExpectedFiles(config) {
774
- const { name, linter, formatter, packageManager, isMonorepo, configStrategy } = config;
775
- const versions = linter === "biome" || formatter === "biome" ? await resolveMonorepoRootPackageVersions({ linter, formatter }) : {};
776
- const aiFilesMap = {};
777
- generateAiFiles(aiFilesMap, {
778
- name,
779
- packageManager,
780
- linter,
781
- formatter,
782
- isMonorepo,
783
- configStrategy,
784
- 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
785
652
  });
786
- const vscodeFiles = {};
787
- generateVscodeFiles(vscodeFiles, linter, formatter);
788
- const configPackages = {};
789
- if (isMonorepo) {
790
- generateTypescriptConfigPackage(configPackages);
791
- if (linter === "oxlint") {
792
- generateOxlintConfigPackage(configPackages);
793
- } else if (linter === "eslint") {
794
- generateEslintConfigPackage(configPackages);
795
- }
796
- if (formatter === "oxfmt") {
797
- generateOxfmtConfigPackage(configPackages);
798
- } else if (formatter === "prettier") {
799
- 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 {
800
665
  }
801
666
  }
802
- const workspaceConfig = {};
803
- const rootConfig = {};
804
- rootConfig[".gitignore"] = generateGitignore(isMonorepo ? "workspace-root" : "standalone");
805
- rootConfig[".gitattributes"] = {
806
- type: "text",
807
- content: `* text=auto eol=lf
808
- *.{cmd,[cC][mM][dD]} text eol=crlf
809
- *.{bat,[bB][aA][tT]} text eol=crlf
810
- `
811
- };
812
- if (linter === "biome" || formatter === "biome") {
813
- const biomeVersion = getResolvedPackageVersion(versions, "@biomejs/biome");
814
- const biomeConfig = {
815
- $schema: `https://biomejs.dev/schemas/${biomeVersion}/schema.json`,
816
- vcs: {
817
- enabled: true,
818
- clientKind: "git",
819
- useIgnoreFile: true
820
- },
821
- linter: {
822
- enabled: linter === "biome",
823
- rules: {
824
- recommended: true
825
- }
826
- },
827
- formatter: {
828
- enabled: formatter === "biome"
829
- }
830
- };
831
- rootConfig["biome.json"] = {
832
- type: "text",
833
- content: JSON.stringify(biomeConfig, null, 2)
834
- };
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");
835
676
  }
836
- return {
837
- "ai-files": aiFilesMap,
838
- vscode: vscodeFiles,
839
- "config-packages": configPackages,
840
- "workspace-config": workspaceConfig,
841
- "root-config": rootConfig
842
- };
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 };
843
702
  }
703
+
844
704
  async function fileExists$1(path) {
845
705
  try {
846
- await access(path, constants.F_OK);
706
+ await access$1(path, constants$1.F_OK);
847
707
  return true;
848
708
  } catch {
849
709
  return false;
850
710
  }
851
711
  }
852
- async function compareWithDisk(expected, root) {
853
- const categoryLabels = {
854
- "ai-files": "AI Files",
855
- vscode: "VS Code",
856
- "config-packages": "Config Packages",
857
- "workspace-config": "Workspace Config",
858
- "root-config": "Root Config"
859
- };
860
- const categories = [];
861
- for (const [category, files] of Object.entries(expected)) {
862
- const changes = [];
863
- for (const [filePath, file] of Object.entries(files)) {
864
- if (file.type !== "text") continue;
865
- const fullPath = join(root, filePath);
866
- const newContent = file.content;
867
- if (await fileExists$1(fullPath)) {
868
- const currentContent = await readFile(fullPath, "utf-8");
869
- if (currentContent === newContent) {
870
- changes.push({
871
- path: filePath,
872
- status: "unchanged",
873
- currentContent,
874
- newContent
875
- });
876
- } else {
877
- changes.push({
878
- path: filePath,
879
- status: "modified",
880
- currentContent,
881
- newContent
882
- });
883
- }
884
- } else {
885
- changes.push({
886
- path: filePath,
887
- status: "added",
888
- newContent
889
- });
890
- }
891
- }
892
- if (changes.length === 0) continue;
893
- const hasUserModifications = changes.some((c) => c.status === "modified");
894
- categories.push({
895
- category,
896
- label: categoryLabels[category],
897
- changes,
898
- hasUserModifications
899
- });
900
- }
901
- return categories;
902
- }
903
- async function getWorkspaceConfigUpdates(root) {
904
- const workspacePath = join(root, "pnpm-workspace.yaml");
905
- const changes = [];
906
- let currentContent = "";
907
- let exists = false;
908
- try {
909
- currentContent = await readFile(workspacePath, "utf-8");
910
- exists = true;
911
- } catch {
912
- }
913
- if (!exists) {
914
- const newContent = `manage-package-manager-versions: true
915
-
916
- packages:
917
- - ".config/*"
918
- - "apps/*"
919
- - "packages/*"
920
-
921
- onlyBuiltDependencies:
922
- - esbuild
923
- `;
924
- changes.push({
925
- path: "pnpm-workspace.yaml",
926
- status: "added",
927
- newContent
928
- });
929
- return changes;
930
- }
931
- let updatedContent = currentContent;
932
- let needsUpdate = false;
933
- if (!currentContent.includes("manage-package-manager-versions")) {
934
- updatedContent = `manage-package-manager-versions: true
935
-
936
- ${updatedContent}`;
937
- needsUpdate = true;
938
- }
939
- if (!currentContent.includes("onlyBuiltDependencies")) {
940
- updatedContent = `${updatedContent.trimEnd()}
941
-
942
- onlyBuiltDependencies:
943
- - esbuild
944
- `;
945
- needsUpdate = true;
946
- }
947
- if (!currentContent.includes(".config/*") && !currentContent.includes('".config/*"')) {
948
- const lines = updatedContent.split("\n");
949
- const packagesIndex = lines.findIndex((line) => line.trim().startsWith("packages:"));
950
- if (packagesIndex !== -1) {
951
- lines.splice(packagesIndex + 1, 0, ' - ".config/*"');
952
- updatedContent = lines.join("\n");
953
- needsUpdate = true;
954
- }
955
- }
956
- if (needsUpdate) {
957
- changes.push({
958
- path: "pnpm-workspace.yaml",
959
- status: "modified",
960
- currentContent,
961
- newContent: updatedContent
962
- });
963
- } else {
964
- changes.push({
965
- path: "pnpm-workspace.yaml",
966
- status: "unchanged",
967
- currentContent,
968
- newContent: currentContent
969
- });
970
- }
971
- return changes;
972
- }
973
- async function applyUpdates(changes, root) {
974
- for (const change of changes) {
975
- if (change.status === "unchanged") continue;
976
- const fullPath = join(root, change.path);
977
- await mkdir(dirname(fullPath), { recursive: true });
978
- await writeFile(fullPath, change.newContent);
979
- }
980
- }
981
- function formatFileChange(change) {
982
- const icon = change.status === "added" ? "+" : change.status === "modified" ? "~" : "=";
983
- return ` ${icon} ${change.path}`;
984
- }
985
- const LINTER_DEPS = {
986
- oxlint: "oxlint",
987
- eslint: "eslint",
988
- biome: "@biomejs/biome"
989
- };
990
- const FORMATTER_DEPS = {
991
- oxfmt: "oxfmt",
992
- prettier: "prettier",
993
- biome: "@biomejs/biome"
994
- };
995
- const LINTER_CONFIG_PACKAGES = {
996
- oxlint: "@config/oxlint",
997
- eslint: "@config/eslint",
998
- biome: null
999
- // biome uses root biome.json
1000
- };
1001
- const FORMATTER_CONFIG_PACKAGES = {
1002
- oxfmt: "@config/oxfmt",
1003
- prettier: "@config/prettier",
1004
- biome: null
1005
- // biome uses root biome.json
1006
- };
1007
- function needsMigration(current, target) {
1008
- const linterChange = target.linter && target.linter !== current.linter;
1009
- const formatterChange = target.formatter && target.formatter !== current.formatter;
1010
- return linterChange || formatterChange || false;
1011
- }
1012
- async function getMigrationPlan(current, target, root) {
1013
- const toLinter = target.linter ?? current.linter;
1014
- const toFormatter = target.formatter ?? current.formatter;
1015
- const targetVersions = toLinter === "biome" || toFormatter === "biome" ? await resolveMonorepoRootPackageVersions({
1016
- linter: toLinter,
1017
- formatter: toFormatter
1018
- }) : {};
1019
- const biomeSchemaUrl = toLinter === "biome" || toFormatter === "biome" ? `https://biomejs.dev/schemas/${getResolvedPackageVersion(
1020
- targetVersions,
1021
- "@biomejs/biome"
1022
- )}/schema.json` : "";
1023
- const changes = [];
1024
- if (toLinter !== current.linter) {
1025
- if (current.linter !== "biome") {
1026
- changes.push({
1027
- type: "remove-dir",
1028
- path: `.config/${current.linter}`,
1029
- description: `Remove @config/${current.linter} package`
1030
- });
1031
- }
1032
- if (toLinter !== "biome") {
1033
- const files = {};
1034
- if (toLinter === "oxlint") {
1035
- generateOxlintConfigPackage(files);
1036
- } else if (toLinter === "eslint") {
1037
- generateEslintConfigPackage(files);
1038
- }
1039
- for (const [path, file] of Object.entries(files)) {
1040
- if (file.type === "text") {
1041
- changes.push({
1042
- type: "add-file",
1043
- path,
1044
- description: `Add ${path}`,
1045
- content: file.content
1046
- });
1047
- }
1048
- }
1049
- }
1050
- if (toLinter === "biome" && toFormatter === "biome") {
1051
- changes.push({
1052
- type: "add-file",
1053
- path: "biome.json",
1054
- description: "Add biome.json config",
1055
- content: JSON.stringify(
1056
- {
1057
- $schema: biomeSchemaUrl,
1058
- vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
1059
- linter: { enabled: true, rules: { recommended: true } },
1060
- formatter: { enabled: true }
1061
- },
1062
- null,
1063
- 2
1064
- )
1065
- });
1066
- } else if (toLinter === "biome" && toFormatter !== "biome") {
1067
- changes.push({
1068
- type: "add-file",
1069
- path: "biome.json",
1070
- description: "Add biome.json config (linter only)",
1071
- content: JSON.stringify(
1072
- {
1073
- $schema: biomeSchemaUrl,
1074
- vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
1075
- linter: { enabled: true, rules: { recommended: true } },
1076
- formatter: { enabled: false }
1077
- },
1078
- null,
1079
- 2
1080
- )
1081
- });
1082
- }
1083
- if (current.linter === "biome" && toLinter !== "biome" && current.formatter !== "biome" && toFormatter !== "biome") {
1084
- changes.push({
1085
- type: "remove-file",
1086
- path: "biome.json",
1087
- description: "Remove biome.json"
1088
- });
1089
- }
1090
- }
1091
- if (toFormatter !== current.formatter) {
1092
- const formatterSameAsLinter = current.formatter === current.linter;
1093
- if (current.formatter !== "biome" && !formatterSameAsLinter) {
1094
- changes.push({
1095
- type: "remove-dir",
1096
- path: `.config/${current.formatter}`,
1097
- description: `Remove @config/${current.formatter} package`
1098
- });
1099
- }
1100
- const newFormatterSameAsLinter = toFormatter === toLinter;
1101
- if (toFormatter !== "biome" && !newFormatterSameAsLinter) {
1102
- const files = {};
1103
- if (toFormatter === "oxfmt") {
1104
- generateOxfmtConfigPackage(files);
1105
- } else if (toFormatter === "prettier") {
1106
- generatePrettierConfigPackage(files);
1107
- }
1108
- for (const [path, file] of Object.entries(files)) {
1109
- if (file.type === "text") {
1110
- changes.push({
1111
- type: "add-file",
1112
- path,
1113
- description: `Add ${path}`,
1114
- content: file.content
1115
- });
1116
- }
1117
- }
1118
- }
1119
- if (toFormatter === "biome" && toLinter !== "biome") {
1120
- changes.push({
1121
- type: "add-file",
1122
- path: "biome.json",
1123
- description: "Add biome.json config (formatter only)",
1124
- content: JSON.stringify(
1125
- {
1126
- $schema: biomeSchemaUrl,
1127
- vcs: { enabled: true, clientKind: "git", useIgnoreFile: true },
1128
- linter: { enabled: false },
1129
- formatter: { enabled: true }
1130
- },
1131
- null,
1132
- 2
1133
- )
1134
- });
1135
- }
1136
- if (current.formatter === "biome" && toFormatter !== "biome" && current.linter !== "biome" && toLinter !== "biome") {
1137
- changes.push({
1138
- type: "remove-file",
1139
- path: "biome.json",
1140
- description: "Remove biome.json"
1141
- });
1142
- }
1143
- }
1144
- changes.push({
1145
- type: "update-package-json",
1146
- path: "package.json",
1147
- description: "Update root package.json (devDependencies, scripts)"
1148
- });
1149
- const subPackageUpdates = await getSubPackageUpdates(root, current, toLinter, toFormatter);
1150
- return {
1151
- fromLinter: current.linter,
1152
- toLinter,
1153
- fromFormatter: current.formatter,
1154
- toFormatter,
1155
- changes,
1156
- subPackageUpdates
1157
- };
1158
- }
1159
- async function getSubPackageUpdates(root, current, toLinter, toFormatter) {
1160
- const updates = [];
1161
- const workspacePath = join(root, "pnpm-workspace.yaml");
1162
- let workspaceContent;
1163
- try {
1164
- workspaceContent = await readFile(workspacePath, "utf-8");
1165
- } catch {
1166
- return updates;
1167
- }
1168
- const packageGlobs = parseWorkspaceYamlContent(workspaceContent);
1169
- for (const glob of packageGlobs) {
1170
- if (glob.includes(".config")) continue;
1171
- const baseDir = glob.replace(/\/\*$/, "").replace(/^["']|["']$/g, "");
1172
- const basePath = join(root, baseDir);
1173
- try {
1174
- const entries = await readdir(basePath, { withFileTypes: true });
1175
- for (const entry of entries) {
1176
- if (!entry.isDirectory()) continue;
1177
- const pkgJsonPath = join(basePath, entry.name, "package.json");
1178
- try {
1179
- const content = await readFile(pkgJsonPath, "utf-8");
1180
- const pkg = JSON.parse(content);
1181
- const devDeps = pkg.devDependencies ?? {};
1182
- const remove = [];
1183
- const add = [];
1184
- const oldLinterPkg = LINTER_CONFIG_PACKAGES[current.linter];
1185
- const newLinterPkg = LINTER_CONFIG_PACKAGES[toLinter];
1186
- if (oldLinterPkg && oldLinterPkg !== newLinterPkg && devDeps[oldLinterPkg]) {
1187
- remove.push(oldLinterPkg);
1188
- }
1189
- if (newLinterPkg && newLinterPkg !== oldLinterPkg && oldLinterPkg && devDeps[oldLinterPkg]) {
1190
- add.push(newLinterPkg);
1191
- }
1192
- if (current.formatter !== current.linter) {
1193
- const oldFormatterPkg = FORMATTER_CONFIG_PACKAGES[current.formatter];
1194
- const newFormatterPkg = FORMATTER_CONFIG_PACKAGES[toFormatter];
1195
- if (oldFormatterPkg && oldFormatterPkg !== newFormatterPkg && devDeps[oldFormatterPkg]) {
1196
- remove.push(oldFormatterPkg);
1197
- }
1198
- if (newFormatterPkg && newFormatterPkg !== oldFormatterPkg && oldFormatterPkg && devDeps[oldFormatterPkg]) {
1199
- add.push(newFormatterPkg);
1200
- }
1201
- }
1202
- if (remove.length > 0 || add.length > 0) {
1203
- updates.push({
1204
- path: join(baseDir, entry.name, "package.json"),
1205
- remove,
1206
- add
1207
- });
1208
- }
1209
- } catch {
1210
- }
1211
- }
1212
- } catch {
1213
- }
1214
- }
1215
- return updates;
1216
- }
1217
- async function applyMigration(plan, root) {
1218
- for (const change of plan.changes) {
1219
- if (change.type === "remove-dir") {
1220
- const fullPath = join(root, change.path);
1221
- try {
1222
- await rm(fullPath, { recursive: true });
1223
- } catch {
1224
- }
1225
- }
1226
- }
1227
- for (const change of plan.changes) {
1228
- if (change.type === "remove-file") {
1229
- const fullPath = join(root, change.path);
1230
- try {
1231
- await rm(fullPath);
1232
- } catch {
1233
- }
1234
- }
1235
- }
1236
- for (const change of plan.changes) {
1237
- if (change.type === "add-file" && change.content) {
1238
- const fullPath = join(root, change.path);
1239
- await mkdir(dirname(fullPath), { recursive: true });
1240
- await writeFile(fullPath, change.content);
1241
- }
1242
- }
1243
- await updateRootPackageJson(root, plan);
1244
- for (const update of plan.subPackageUpdates) {
1245
- await updateSubPackageJson(root, update);
1246
- }
1247
- }
1248
- async function updateRootPackageJson(root, plan) {
1249
- const pkgPath = join(root, "package.json");
1250
- const content = await readFile(pkgPath, "utf-8");
1251
- const pkg = JSON.parse(content);
1252
- const devDeps = pkg.devDependencies ?? {};
1253
- const oldLinterDep = LINTER_DEPS[plan.fromLinter];
1254
- delete devDeps[oldLinterDep];
1255
- if (plan.fromFormatter !== plan.fromLinter) {
1256
- const oldFormatterDep = FORMATTER_DEPS[plan.fromFormatter];
1257
- delete devDeps[oldFormatterDep];
1258
- }
1259
- const resolvedVersions = await resolveMonorepoRootPackageVersions({
1260
- linter: plan.toLinter,
1261
- formatter: plan.toFormatter
1262
- });
1263
- const newLinterDep = LINTER_DEPS[plan.toLinter];
1264
- devDeps[newLinterDep] = formatResolvedPackageVersion(resolvedVersions, newLinterDep);
1265
- if (plan.toFormatter !== plan.toLinter) {
1266
- const newFormatterDep = FORMATTER_DEPS[plan.toFormatter];
1267
- devDeps[newFormatterDep] = formatResolvedPackageVersion(resolvedVersions, newFormatterDep);
1268
- }
1269
- pkg.devDependencies = Object.fromEntries(
1270
- Object.entries(devDeps).sort(([a], [b]) => a.localeCompare(b))
1271
- );
1272
- const scripts = pkg.scripts ?? {};
1273
- if (plan.toLinter === "oxlint") {
1274
- scripts.lint = "oxlint .";
1275
- } else if (plan.toLinter === "eslint") {
1276
- scripts.lint = "eslint .";
1277
- } else if (plan.toLinter === "biome") {
1278
- scripts.lint = "biome check .";
1279
- }
1280
- if (plan.toFormatter === "oxfmt") {
1281
- scripts.format = "oxfmt .";
1282
- } else if (plan.toFormatter === "prettier") {
1283
- scripts.format = "prettier --write .";
1284
- } else if (plan.toFormatter === "biome") {
1285
- scripts.format = "biome format . --write";
1286
- }
1287
- pkg.scripts = scripts;
1288
- await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1289
- }
1290
- async function updateSubPackageJson(root, update) {
1291
- const pkgPath = join(root, update.path);
1292
- const content = await readFile(pkgPath, "utf-8");
1293
- const pkg = JSON.parse(content);
1294
- const devDeps = pkg.devDependencies ?? {};
1295
- for (const dep of update.remove) {
1296
- delete devDeps[dep];
1297
- }
1298
- for (const dep of update.add) {
1299
- devDeps[dep] = "workspace:*";
1300
- }
1301
- pkg.devDependencies = Object.fromEntries(
1302
- Object.entries(devDeps).sort(([a], [b]) => a.localeCompare(b))
1303
- );
1304
- await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
1305
- }
1306
- function formatMigrationChange(change) {
1307
- const icon = change.type === "remove-dir" || change.type === "remove-file" ? "-" : change.type === "add-file" ? "+" : "~";
1308
- return ` ${icon} ${change.description}`;
1309
- }
1310
-
1311
- async function checkAnyExists(paths) {
1312
- for (const path of paths) {
1313
- try {
1314
- await access(path, constants$1.F_OK);
1315
- return true;
1316
- } catch {
1317
- }
1318
- }
1319
- return false;
1320
- }
1321
- async function validateWorkspace(monorepoRoot) {
1322
- const errors = [];
1323
- const tsConfigPath = join(monorepoRoot, ".config/typescript/package.json");
1324
- try {
1325
- await access(tsConfigPath, constants$1.F_OK);
1326
- } catch {
1327
- errors.push("Missing .config/typescript package");
1328
- }
1329
- const linterPaths = [
1330
- join(monorepoRoot, ".config/oxlint/package.json"),
1331
- join(monorepoRoot, ".config/eslint/package.json"),
1332
- join(monorepoRoot, "eslint.config.js"),
1333
- join(monorepoRoot, "biome.json")
1334
- ];
1335
- const hasLinter = await checkAnyExists(linterPaths);
1336
- if (!hasLinter) {
1337
- errors.push(
1338
- "Missing linter config (.config/oxlint, .config/eslint, eslint.config.js, or biome.json)"
1339
- );
1340
- }
1341
- const formatterPaths = [
1342
- join(monorepoRoot, ".config/oxfmt/package.json"),
1343
- join(monorepoRoot, ".config/prettier/package.json"),
1344
- join(monorepoRoot, ".prettierrc.json"),
1345
- join(monorepoRoot, "biome.json")
1346
- ];
1347
- const hasFormatter = await checkAnyExists(formatterPaths);
1348
- if (!hasFormatter) {
1349
- errors.push(
1350
- "Missing formatter config (.config/oxfmt, .config/prettier, .prettierrc.json, or biome.json)"
1351
- );
1352
- }
1353
- return { valid: errors.length === 0, errors };
1354
- }
1355
-
1356
- const require$1 = createRequire(import.meta.url);
1357
- const pkg = require$1("../package.json");
1358
- const META_OPTIONS = [
1359
- "clearConfig",
1360
- "configPath",
1361
- "check",
1362
- "fix",
1363
- "update",
1364
- "yes",
1365
- "workspace",
1366
- "path",
1367
- "dir"
1368
- ];
1369
- function hasConfigOptions(options) {
1370
- return Object.keys(options).some(
1371
- (key) => !META_OPTIONS.includes(key)
1372
- );
1373
- }
1374
- async function fileExists(path) {
1375
- try {
1376
- await access$1(path, constants$2.F_OK);
1377
- return true;
1378
- } catch {
1379
- return false;
1380
- }
1381
- }
1382
- async function promptForAiPlatforms(isNonInteractive) {
1383
- const savedPlatforms = getAiPlatforms();
1384
- if (isNonInteractive) {
1385
- return savedPlatforms ?? ALL_AI_PLATFORMS;
1386
- }
1387
- if (savedPlatforms && savedPlatforms.length > 0) {
1388
- const savedLabels = savedPlatforms.map((plat) => AI_PLATFORM_LABELS[plat]).join(", ");
1389
- const useDefault = await p.confirm({
1390
- message: `Add AI rules? ${color.dim(`(${savedLabels})`)}`,
1391
- initialValue: true
1392
- });
1393
- if (p.isCancel(useDefault)) {
1394
- return [];
1395
- }
1396
- if (useDefault) {
1397
- return savedPlatforms;
1398
- }
1399
- }
1400
- const selected = await p.multiselect({
1401
- message: "Add AI rules?",
1402
- options: ALL_AI_PLATFORMS.map((platform) => ({
1403
- value: platform,
1404
- label: AI_PLATFORM_LABELS[platform],
1405
- hint: AI_PLATFORM_HINTS[platform]
1406
- })),
1407
- initialValues: ["agents"],
1408
- required: false
1409
- });
1410
- if (p.isCancel(selected)) {
1411
- return [];
1412
- }
1413
- const platforms = selected;
1414
- if (platforms.length === 0) {
1415
- return [];
1416
- }
1417
- return platforms;
1418
- }
1419
- async function writeGeneratedFiles(basePath, files) {
1420
- const filePaths = Object.keys(files).sort();
1421
- for (const filePath of filePaths) {
1422
- const fullFilePath = join$1(basePath, filePath);
1423
- await mkdir$1(dirname$1(fullFilePath), { recursive: true });
1424
- const file = files[filePath];
1425
- if (file.type === "text") {
1426
- await writeFile$1(fullFilePath, file.content);
1427
- } else {
1428
- const response = await fetch(file.url);
1429
- await writeFile$1(fullFilePath, response.body);
1430
- }
1431
- }
1432
- }
1433
712
  function calculateWorkspaceRoot(packagePath) {
1434
713
  const segments = packagePath.split(/[/\\]/).filter(Boolean);
1435
714
  return segments.map(() => "..").join("/");
@@ -1440,14 +719,14 @@ async function detectMonorepoRoot() {
1440
719
  while (currentDir !== root) {
1441
720
  const workspaceFile = join$1(currentDir, "pnpm-workspace.yaml");
1442
721
  try {
1443
- await access$1(workspaceFile, constants$2.F_OK);
1444
- const content = await readFile$1(workspaceFile, "utf-8");
722
+ await access$1(workspaceFile, constants$1.F_OK);
723
+ const content = await readFile(workspaceFile, "utf-8");
1445
724
  if (content.includes("packages:")) {
1446
725
  return currentDir;
1447
726
  }
1448
727
  } catch {
1449
728
  }
1450
- currentDir = dirname$1(currentDir);
729
+ currentDir = dirname(currentDir);
1451
730
  }
1452
731
  return null;
1453
732
  }
@@ -1455,17 +734,17 @@ async function detectPackageRoot() {
1455
734
  let currentDir = cwd();
1456
735
  const root = resolve("/");
1457
736
  while (currentDir !== root) {
1458
- if (await fileExists(join$1(currentDir, "package.json"))) {
737
+ if (await fileExists$1(join$1(currentDir, "package.json"))) {
1459
738
  return currentDir;
1460
739
  }
1461
- currentDir = dirname$1(currentDir);
740
+ currentDir = dirname(currentDir);
1462
741
  }
1463
- return await fileExists(join$1(root, "package.json")) ? root : null;
742
+ return await fileExists$1(join$1(root, "package.json")) ? root : null;
1464
743
  }
1465
744
  async function parseWorkspaceDirectories(monorepoRoot) {
1466
745
  try {
1467
746
  const workspaceFile = join$1(monorepoRoot, "pnpm-workspace.yaml");
1468
- const content = await readFile$1(workspaceFile, "utf-8");
747
+ const content = await readFile(workspaceFile, "utf-8");
1469
748
  return parseWorkspaceYamlContent(content);
1470
749
  } catch {
1471
750
  return [];
@@ -1475,14 +754,14 @@ async function detectWorkspaceSettings(monorepoRoot) {
1475
754
  try {
1476
755
  const tooling = await detectTooling(monorepoRoot);
1477
756
  const pkgPath = join$1(monorepoRoot, "package.json");
1478
- const content = await readFile$1(pkgPath, "utf-8");
757
+ const content = await readFile(pkgPath, "utf-8");
1479
758
  const pkgJson = JSON.parse(content);
1480
759
  const packageManager = parsePackageManager(pkgJson.packageManager);
1481
760
  const engine = parseEngine(pkgJson.engines);
1482
761
  let pnpmManageVersions;
1483
762
  try {
1484
763
  const workspaceFile = join$1(monorepoRoot, "pnpm-workspace.yaml");
1485
- const workspaceContent = await readFile$1(workspaceFile, "utf-8");
764
+ const workspaceContent = await readFile(workspaceFile, "utf-8");
1486
765
  pnpmManageVersions = workspaceContent.includes("manage-package-manager-versions: true");
1487
766
  } catch {
1488
767
  }
@@ -1500,17 +779,17 @@ async function detectWorkspaceSettings(monorepoRoot) {
1500
779
  async function detectExistingConfigs(monorepoRoot) {
1501
780
  const configs = {};
1502
781
  const eslintPath = join$1(monorepoRoot, "eslint.config.js");
1503
- if (await fileExists(eslintPath)) {
782
+ if (await fileExists$1(eslintPath)) {
1504
783
  configs.linter = "eslint";
1505
784
  configs.eslintConfigPath = eslintPath;
1506
785
  }
1507
786
  const prettierPath = join$1(monorepoRoot, ".prettierrc.json");
1508
- if (await fileExists(prettierPath)) {
787
+ if (await fileExists$1(prettierPath)) {
1509
788
  configs.formatter = "prettier";
1510
789
  configs.prettierConfigPath = prettierPath;
1511
790
  }
1512
791
  const biomePath = join$1(monorepoRoot, "biome.json");
1513
- if (await fileExists(biomePath)) {
792
+ if (await fileExists$1(biomePath)) {
1514
793
  configs.biomeConfigPath = biomePath;
1515
794
  if (!configs.linter) configs.linter = "biome";
1516
795
  if (!configs.formatter) configs.formatter = "biome";
@@ -1520,7 +799,7 @@ async function detectExistingConfigs(monorepoRoot) {
1520
799
  async function getMonorepoScope(monorepoRoot) {
1521
800
  try {
1522
801
  const pkgPath = join$1(monorepoRoot, "package.json");
1523
- const content = await readFile$1(pkgPath, "utf-8");
802
+ const content = await readFile(pkgPath, "utf-8");
1524
803
  const pkgJson = JSON.parse(content);
1525
804
  if (pkgJson.name) {
1526
805
  return pkgJson.name.replace(/^@/, "").replace(/\/.*$/, "");
@@ -1532,18 +811,17 @@ async function getMonorepoScope(monorepoRoot) {
1532
811
  async function getWorkspacePackages(monorepoRoot) {
1533
812
  const packagesDir = join$1(monorepoRoot, "packages");
1534
813
  try {
1535
- const { readdir } = await import('fs/promises');
1536
814
  const entries = await readdir(packagesDir, { withFileTypes: true });
1537
815
  const names = [];
1538
816
  for (const entry of entries) {
1539
817
  if (!entry.isDirectory()) continue;
1540
818
  try {
1541
- const content = await readFile$1(
819
+ const content = await readFile(
1542
820
  join$1(packagesDir, entry.name, "package.json"),
1543
821
  "utf-8"
1544
822
  );
1545
- const pkg2 = JSON.parse(content);
1546
- if (pkg2.name) names.push(pkg2.name);
823
+ const pkg = JSON.parse(content);
824
+ if (pkg.name) names.push(pkg.name);
1547
825
  } catch {
1548
826
  }
1549
827
  }
@@ -1556,38 +834,59 @@ async function ensureConfigInWorkspace(monorepoRoot) {
1556
834
  const workspacePath = join$1(monorepoRoot, "pnpm-workspace.yaml");
1557
835
  let content;
1558
836
  try {
1559
- content = await readFile$1(workspacePath, "utf-8");
837
+ content = await readFile(workspacePath, "utf-8");
1560
838
  } catch {
1561
839
  content = `packages:
1562
- - ".config/*"
1563
- - "packages/*"
840
+ - '.config/*'
841
+ - 'packages/*'
1564
842
  `;
1565
- await writeFile$1(workspacePath, content);
843
+ await writeFile(workspacePath, content);
1566
844
  return;
1567
845
  }
1568
- if (content.includes(".config/*") || content.includes('".config/*"')) {
846
+ if (content.includes(".config/*")) {
1569
847
  return;
1570
848
  }
1571
849
  const lines = content.split("\n");
1572
850
  const packagesIndex = lines.findIndex((line) => line.trim().startsWith("packages:"));
1573
851
  if (packagesIndex === -1) {
1574
852
  content = `packages:
1575
- - ".config/*"
853
+ - '.config/*'
1576
854
  ${content}`;
1577
855
  } else {
1578
- lines.splice(packagesIndex + 1, 0, ' - ".config/*"');
856
+ lines.splice(packagesIndex + 1, 0, " - '.config/*'");
1579
857
  content = lines.join("\n");
1580
858
  }
1581
- await writeFile$1(workspacePath, content);
859
+ await writeFile(workspacePath, content);
860
+ }
861
+
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);
867
+ }
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}`));
872
+ } else {
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
+ }
878
+ }
879
+ process.exit(valid ? 0 : 1);
1582
880
  }
881
+
1583
882
  async function migrateEslintConfig(monorepoRoot, files) {
1584
883
  const configBasePath = ".config/eslint";
1585
884
  const existingConfigPath = join$1(monorepoRoot, "eslint.config.js");
1586
885
  let existingContent;
1587
886
  try {
1588
- existingContent = await readFile$1(existingConfigPath, "utf-8");
887
+ existingContent = await readFile(existingConfigPath, "utf-8");
1589
888
  } catch {
1590
- generateEslintConfigPackage(files);
889
+ renderEslintConfigPackage(files);
1591
890
  return;
1592
891
  }
1593
892
  files[`${configBasePath}/package.json`] = {
@@ -1658,240 +957,63 @@ export default [
1658
957
  ];
1659
958
  `
1660
959
  };
1661
- }
1662
- async function migratePrettierConfig(monorepoRoot, files) {
1663
- const configBasePath = ".config/prettier";
1664
- const existingConfigPath = join$1(monorepoRoot, ".prettierrc.json");
1665
- let existingContent;
1666
- try {
1667
- existingContent = await readFile$1(existingConfigPath, "utf-8");
1668
- } catch {
1669
- generatePrettierConfigPackage(files);
1670
- return;
1671
- }
1672
- files[`${configBasePath}/package.json`] = {
1673
- type: "text",
1674
- content: JSON.stringify(
1675
- {
1676
- name: "@config/prettier",
1677
- version: "0.1.0",
1678
- private: true,
1679
- exports: {
1680
- "./base": "./base.json"
1681
- }
1682
- },
1683
- null,
1684
- 2
1685
- )
1686
- };
1687
- files[`${configBasePath}/README.md`] = {
1688
- type: "text",
1689
- content: `# \`@config/prettier\`
1690
-
1691
- Shared Prettier configurations.
1692
-
1693
- ## Usage
1694
-
1695
- In your package's \`.prettierrc\`:
1696
-
1697
- \`\`\`json
1698
- "@config/prettier/base"
1699
- \`\`\`
1700
-
1701
- Or in \`package.json\`:
1702
-
1703
- \`\`\`json
1704
- {
1705
- "prettier": "@config/prettier/base"
1706
- }
1707
- \`\`\`
1708
-
1709
- ## Available Configs
1710
-
1711
- - \`base\` - Base Prettier rules (migrated from root)
1712
- `
1713
- };
1714
- files[`${configBasePath}/base.json`] = {
1715
- type: "text",
1716
- content: existingContent
1717
- };
1718
- }
1719
- async function createPackageInWorkspace(monorepoRoot, packageManager, inheritedSettings, scope) {
1720
- const workspaceDirectories = await parseWorkspaceDirectories(monorepoRoot);
1721
- const defaultDirectories = ["apps", "packages"];
1722
- const hasCustomDirectories = workspaceDirectories.length > 0 && !workspaceDirectories.every((dir) => defaultDirectories.includes(dir));
1723
- const packageType = await promptForInitialPackage();
1724
- if (packageType === "skip") {
1725
- return false;
1726
- }
1727
- const defaultDir = packageType === "app" ? "apps" : "packages";
1728
- const packageNameInput = await p.text({
1729
- message: "Package name?",
1730
- initialValue: `@${scope}/`,
1731
- validate: (value) => {
1732
- const validationError = validatePackageName(value);
1733
- if (validationError) return validationError;
1734
- const dirName = value.includes("/") ? value.split("/").pop() : value;
1735
- if (!dirName) return "Package name is required";
1736
- if (!hasCustomDirectories) {
1737
- const targetPath = join$1(monorepoRoot, defaultDir, dirName);
1738
- try {
1739
- const { statSync } = require$1("fs");
1740
- statSync(targetPath);
1741
- return `Directory ${defaultDir}/${dirName} already exists`;
1742
- } catch {
1743
- }
1744
- }
1745
- }
1746
- });
1747
- if (p.isCancel(packageNameInput)) {
1748
- return false;
1749
- }
1750
- const scopedName = packageNameInput;
1751
- const shortName = scopedName.includes("/") ? scopedName.split("/").pop() : scopedName;
1752
- const packageOptions = await promptForPackageOptions(scopedName, packageType, inheritedSettings);
1753
- let targetDir = defaultDir;
1754
- if (hasCustomDirectories && workspaceDirectories.length > 0) {
1755
- const dirChoice = await p.select({
1756
- message: "Target directory",
1757
- options: workspaceDirectories.map((dir) => ({
1758
- value: dir,
1759
- label: dir
1760
- })),
1761
- initialValue: workspaceDirectories.includes(defaultDir) ? defaultDir : workspaceDirectories[0]
1762
- });
1763
- if (p.isCancel(dirChoice)) {
1764
- return false;
1765
- }
1766
- targetDir = dirChoice;
1767
- const targetPath = join$1(monorepoRoot, targetDir, shortName);
1768
- try {
1769
- const { statSync } = require$1("fs");
1770
- statSync(targetPath);
1771
- p.log.error(`Directory ${targetDir}/${shortName} already exists`);
1772
- return false;
1773
- } catch {
1774
- }
1775
- }
1776
- const relativePkgPath = join$1(targetDir, shortName);
1777
- const workspaceRoot = calculateWorkspaceRoot(relativePkgPath);
1778
- packageOptions.workspaceRoot = workspaceRoot;
1779
- packageOptions.name = scopedName;
1780
- packageOptions.packageManager = await resolvePackageManager(packageOptions);
1781
- packageOptions.engine = await resolveEngine(packageOptions);
1782
- packageOptions.versions = await resolveProjectPackageVersions(packageOptions);
1783
- const workspacePackages = packageType === "app" ? await getWorkspacePackages(monorepoRoot) : [];
1784
- if (workspacePackages.length > 0) {
1785
- const selectedDeps = await p.multiselect({
1786
- message: "Add workspace dependencies?",
1787
- options: workspacePackages.map((name) => ({ value: name, label: name })),
1788
- required: false
1789
- });
1790
- if (!p.isCancel(selectedDeps) && selectedDeps.length > 0) {
1791
- packageOptions.workspaceDependencies = selectedDeps;
1792
- }
1793
- }
1794
- const outputPath = join$1(monorepoRoot, relativePkgPath);
1795
- const spinner = p.spinner();
1796
- spinner.start("Creating package...");
1797
- try {
1798
- const files = generate(packageOptions);
1799
- await writeGeneratedFiles(outputPath, files);
1800
- spinner.stop(color.green.inverse(` \u2713 Package created at ${relativePkgPath}! `));
1801
- const addAnother = await p.select({
1802
- message: "Add another package?",
1803
- options: [
1804
- { value: "no", label: "No, I'm done" },
1805
- { value: "yes", label: "Yes, add another" }
1806
- ],
1807
- initialValue: "no"
1808
- });
1809
- return !p.isCancel(addAnother) && addAnother === "yes";
1810
- } catch (error) {
1811
- spinner.stop("Failed to create package");
1812
- p.log.error(String(error));
1813
- return false;
1814
- }
1815
- }
1816
- async function promptAndOpenEditor(projectPath) {
1817
- const savedEditor = getPreferredEditor();
1818
- let selectedEditor;
1819
- if (savedEditor && savedEditor !== "skip") {
1820
- const useDefault = await p.confirm({
1821
- message: `Open in editor? ${color.dim(`(${editorNames[savedEditor]})`)}`,
1822
- initialValue: true
1823
- });
1824
- if (p.isCancel(useDefault)) {
1825
- selectedEditor = void 0;
1826
- } else if (useDefault) {
1827
- selectedEditor = savedEditor;
1828
- } else {
1829
- selectedEditor = "skip";
1830
- }
1831
- } else {
1832
- const openEditor = await p.select({
1833
- message: "Open project in editor?",
1834
- options: [
1835
- { value: "skip", label: "Skip" },
1836
- { value: "cursor", label: "Cursor" },
1837
- { value: "code", label: "VS Code" },
1838
- { value: "webstorm", label: "WebStorm" }
1839
- ],
1840
- initialValue: "skip"
1841
- });
1842
- if (!p.isCancel(openEditor)) {
1843
- selectedEditor = openEditor;
1844
- const saveChoice = await p.confirm({
1845
- message: `Save ${editorNames[selectedEditor] ?? "Skip"} as default editor?`,
1846
- initialValue: true
1847
- });
1848
- if (!p.isCancel(saveChoice) && saveChoice) {
1849
- setPreferredEditor(selectedEditor);
1850
- if (selectedEditor === "cursor" || selectedEditor === "code") {
1851
- const reuseChoice = await p.confirm({
1852
- message: "Reuse current window when opening projects?",
1853
- initialValue: false
1854
- });
1855
- if (!p.isCancel(reuseChoice)) {
1856
- setReuseWindow(reuseChoice);
1857
- }
1858
- }
1859
- }
1860
- }
1861
- }
1862
- if (selectedEditor && selectedEditor !== "skip") {
1863
- try {
1864
- await openInEditor(
1865
- selectedEditor,
1866
- projectPath,
1867
- getReuseWindow()
1868
- );
1869
- p.log.success(`Opening in ${editorNames[selectedEditor]}...`);
1870
- } catch {
1871
- p.log.warn(
1872
- `Could not open ${editorNames[selectedEditor]}. Make sure the CLI command is in your PATH.`
1873
- );
1874
- }
1875
- }
1876
- }
1877
- async function handleCheckCommand() {
1878
- const monorepoRoot = await detectMonorepoRoot();
1879
- if (!monorepoRoot) {
1880
- console.log(color.red("\u2717") + " Not a monorepo workspace");
1881
- process.exit(1);
1882
- }
1883
- const { valid, errors } = await validateWorkspace(monorepoRoot);
1884
- if (valid) {
1885
- console.log(color.green("\u2713") + " Valid monorepo workspace");
1886
- console.log(color.dim(` ${monorepoRoot}`));
1887
- } else {
1888
- console.log(color.red("\u2717") + " Invalid monorepo workspace");
1889
- console.log(color.dim(` ${monorepoRoot}`));
1890
- for (const error of errors) {
1891
- console.log(color.red(` \u2022 ${error}`));
1892
- }
960
+ }
961
+ async function migratePrettierConfig(monorepoRoot, files) {
962
+ const configBasePath = ".config/prettier";
963
+ const existingConfigPath = join$1(monorepoRoot, ".prettierrc.json");
964
+ let existingContent;
965
+ try {
966
+ existingContent = await readFile(existingConfigPath, "utf-8");
967
+ } catch {
968
+ renderPrettierConfigPackage(files);
969
+ return;
1893
970
  }
1894
- process.exit(valid ? 0 : 1);
971
+ files[`${configBasePath}/package.json`] = {
972
+ type: "text",
973
+ content: JSON.stringify(
974
+ {
975
+ name: "@config/prettier",
976
+ version: "0.1.0",
977
+ private: true,
978
+ exports: {
979
+ "./base": "./base.json"
980
+ }
981
+ },
982
+ null,
983
+ 2
984
+ )
985
+ };
986
+ files[`${configBasePath}/README.md`] = {
987
+ type: "text",
988
+ content: `# \`@config/prettier\`
989
+
990
+ Shared Prettier configurations.
991
+
992
+ ## Usage
993
+
994
+ In your package's \`.prettierrc\`:
995
+
996
+ \`\`\`json
997
+ "@config/prettier/base"
998
+ \`\`\`
999
+
1000
+ Or in \`package.json\`:
1001
+
1002
+ \`\`\`json
1003
+ {
1004
+ "prettier": "@config/prettier/base"
1005
+ }
1006
+ \`\`\`
1007
+
1008
+ ## Available Configs
1009
+
1010
+ - \`base\` - Base Prettier rules (migrated from root)
1011
+ `
1012
+ };
1013
+ files[`${configBasePath}/base.json`] = {
1014
+ type: "text",
1015
+ content: existingContent
1016
+ };
1895
1017
  }
1896
1018
  async function handleFixCommand(options) {
1897
1019
  const monorepoRoot = await detectMonorepoRoot();
@@ -1974,39 +1096,39 @@ async function handleFixCommand(options) {
1974
1096
  spinner.start("Fixing workspace...");
1975
1097
  try {
1976
1098
  const files = {};
1977
- const tsConfigExists = await fileExists(
1099
+ const tsConfigExists = await fileExists$1(
1978
1100
  join$1(monorepoRoot, ".config/typescript/package.json")
1979
1101
  );
1980
1102
  if (!tsConfigExists) {
1981
- generateTypescriptConfigPackage(files);
1103
+ renderTypescriptConfigPackage(files);
1982
1104
  }
1983
1105
  if (linter === "oxlint") {
1984
- const oxlintExists = await fileExists(join$1(monorepoRoot, ".config/oxlint/package.json"));
1985
- if (!oxlintExists) generateOxlintConfigPackage(files);
1106
+ const oxlintExists = await fileExists$1(join$1(monorepoRoot, ".config/oxlint/package.json"));
1107
+ if (!oxlintExists) renderOxlintConfigPackage(files);
1986
1108
  } else if (linter === "eslint") {
1987
- const eslintPkgExists = await fileExists(
1109
+ const eslintPkgExists = await fileExists$1(
1988
1110
  join$1(monorepoRoot, ".config/eslint/package.json")
1989
1111
  );
1990
1112
  if (!eslintPkgExists) {
1991
1113
  if (existingConfigs.eslintConfigPath) {
1992
1114
  await migrateEslintConfig(monorepoRoot, files);
1993
1115
  } else {
1994
- generateEslintConfigPackage(files);
1116
+ renderEslintConfigPackage(files);
1995
1117
  }
1996
1118
  }
1997
1119
  }
1998
1120
  if (formatter === "oxfmt") {
1999
- const oxfmtExists = await fileExists(join$1(monorepoRoot, ".config/oxfmt/package.json"));
2000
- if (!oxfmtExists) generateOxfmtConfigPackage(files);
1121
+ const oxfmtExists = await fileExists$1(join$1(monorepoRoot, ".config/oxfmt/package.json"));
1122
+ if (!oxfmtExists) renderOxfmtConfigPackage(files);
2001
1123
  } else if (formatter === "prettier") {
2002
- const prettierPkgExists = await fileExists(
1124
+ const prettierPkgExists = await fileExists$1(
2003
1125
  join$1(monorepoRoot, ".config/prettier/package.json")
2004
1126
  );
2005
1127
  if (!prettierPkgExists) {
2006
1128
  if (existingConfigs.prettierConfigPath) {
2007
1129
  await migratePrettierConfig(monorepoRoot, files);
2008
1130
  } else {
2009
- generatePrettierConfigPackage(files);
1131
+ renderPrettierConfigPackage(files);
2010
1132
  }
2011
1133
  }
2012
1134
  }
@@ -2040,8 +1162,8 @@ async function handleFixCommand(options) {
2040
1162
  }
2041
1163
  for (const [filePath, file] of Object.entries(files)) {
2042
1164
  const fullPath = join$1(monorepoRoot, filePath);
2043
- await mkdir$1(dirname$1(fullPath), { recursive: true });
2044
- await writeFile$1(fullPath, file.content);
1165
+ await mkdir(dirname(fullPath), { recursive: true });
1166
+ await writeFile(fullPath, file.content);
2045
1167
  }
2046
1168
  await ensureConfigInWorkspace(monorepoRoot);
2047
1169
  if (existingConfigs.eslintConfigPath && linter === "eslint") {
@@ -2057,13 +1179,13 @@ async function handleFixCommand(options) {
2057
1179
  }
2058
1180
  }
2059
1181
  spinner.stop(color.green("\u2713") + " Workspace fixed!");
2060
- const generated = Object.keys(files).filter((f) => f.endsWith("package.json"));
1182
+ const generated = Object.keys(files).filter((file) => file.endsWith("package.json"));
2061
1183
  for (const pkgFile of generated) {
2062
1184
  const pkgName = pkgFile.replace("/package.json", "");
2063
1185
  console.log(color.dim(` Generated ${pkgName}`));
2064
1186
  }
2065
- const vscodeSettingsExists = await fileExists(join$1(monorepoRoot, ".vscode/settings.json"));
2066
- const vscodeExtensionsExists = await fileExists(
1187
+ const vscodeSettingsExists = await fileExists$1(join$1(monorepoRoot, ".vscode/settings.json"));
1188
+ const vscodeExtensionsExists = await fileExists$1(
2067
1189
  join$1(monorepoRoot, ".vscode/extensions.json")
2068
1190
  );
2069
1191
  const vscodeExists = vscodeSettingsExists && vscodeExtensionsExists;
@@ -2072,337 +1194,828 @@ async function handleFixCommand(options) {
2072
1194
  if (isNonInteractive) {
2073
1195
  addVscode = true;
2074
1196
  } else {
2075
- const vscodeChoice = await p.confirm({
2076
- message: "Generate VS Code settings?",
2077
- initialValue: true
1197
+ const vscodeChoice = await p.confirm({
1198
+ message: "Generate VS Code settings?",
1199
+ initialValue: true
1200
+ });
1201
+ addVscode = !p.isCancel(vscodeChoice) && vscodeChoice;
1202
+ }
1203
+ if (addVscode) {
1204
+ const vscodeFiles = {};
1205
+ renderVscodeFiles(vscodeFiles, linter, formatter);
1206
+ for (const [filePath, file] of Object.entries(vscodeFiles)) {
1207
+ const fullPath = join$1(monorepoRoot, filePath);
1208
+ await mkdir(dirname(fullPath), { recursive: true });
1209
+ await writeFile(fullPath, file.content);
1210
+ }
1211
+ console.log(color.dim(" Generated .vscode/settings.json"));
1212
+ console.log(color.dim(" Generated .vscode/extensions.json"));
1213
+ }
1214
+ }
1215
+ const aiRulesExist = await fileExists$1(join$1(monorepoRoot, ".ai/workspace.md"));
1216
+ if (!aiRulesExist) {
1217
+ const platforms = await promptForAiAgentPlatforms(isNonInteractive);
1218
+ if (platforms.length > 0) {
1219
+ const scope = await getMonorepoScope(monorepoRoot);
1220
+ const aiFilesOutput = {};
1221
+ renderAiFiles(aiFilesOutput, {
1222
+ name: scope,
1223
+ packageManager: "pnpm",
1224
+ linter,
1225
+ formatter,
1226
+ isMonorepo: true,
1227
+ hasTypecheck: false,
1228
+ platforms
1229
+ });
1230
+ for (const [filePath, file] of Object.entries(aiFilesOutput)) {
1231
+ const fullPath = join$1(monorepoRoot, filePath);
1232
+ await mkdir(dirname(fullPath), { recursive: true });
1233
+ await writeFile(fullPath, file.content);
1234
+ console.log(color.dim(` Generated ${filePath}`));
1235
+ }
1236
+ }
1237
+ }
1238
+ process.exit(0);
1239
+ } catch (error) {
1240
+ spinner.stop(color.red("\u2717") + " Failed to fix workspace");
1241
+ console.error(error);
1242
+ process.exit(1);
1243
+ }
1244
+ }
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(/\/.*$/, "");
1256
+ }
1257
+ if (pkgJson.packageManager) {
1258
+ packageManager = pkgJson.packageManager.split("@")[0] ?? packageManager;
1259
+ }
1260
+ hasTypecheck = pkgJson.scripts?.typecheck != null;
1261
+ } catch {
1262
+ }
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);
1317
+ }
1318
+ }
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
+ };
1369
+ }
1370
+ async function fileExists(path) {
1371
+ try {
1372
+ await access(path, constants$2.F_OK);
1373
+ return true;
1374
+ } catch {
1375
+ return false;
1376
+ }
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;
1391
+ }
1392
+ continue;
1393
+ }
1394
+ if (inBlockComment) {
1395
+ if (char === "*" && next === "/") {
1396
+ inBlockComment = false;
1397
+ index++;
1398
+ }
1399
+ continue;
1400
+ }
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;
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;
1421
+ }
1422
+ if (char === "/" && next === "*") {
1423
+ inBlockComment = true;
1424
+ index++;
1425
+ continue;
1426
+ }
1427
+ output += char;
1428
+ }
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
+ }
1446
+ continue;
1447
+ }
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;
1458
+ }
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
+ });
1522
+ } else {
1523
+ changes.push({
1524
+ path: filePath,
1525
+ status: "modified",
1526
+ currentContent,
1527
+ newContent
1528
+ });
1529
+ }
1530
+ } else {
1531
+ changes.push({
1532
+ path: filePath,
1533
+ status: "added",
1534
+ newContent
2078
1535
  });
2079
- addVscode = !p.isCancel(vscodeChoice) && vscodeChoice;
2080
- }
2081
- if (addVscode) {
2082
- const vscodeFiles = {};
2083
- generateVscodeFiles(vscodeFiles, linter, formatter);
2084
- for (const [filePath, file] of Object.entries(vscodeFiles)) {
2085
- const fullPath = join$1(monorepoRoot, filePath);
2086
- await mkdir$1(dirname$1(fullPath), { recursive: true });
2087
- await writeFile$1(fullPath, file.content);
2088
- }
2089
- console.log(color.dim(" Generated .vscode/settings.json"));
2090
- console.log(color.dim(" Generated .vscode/extensions.json"));
2091
1536
  }
2092
1537
  }
2093
- const aiRulesExist = await fileExists(join$1(monorepoRoot, ".ai/workspace.md"));
2094
- if (!aiRulesExist) {
2095
- const platforms = await promptForAiPlatforms(isNonInteractive);
2096
- if (platforms.length > 0) {
2097
- const scope = await getMonorepoScope(monorepoRoot);
2098
- const aiFilesOutput = {};
2099
- generateAiFiles(aiFilesOutput, {
2100
- name: scope,
2101
- packageManager: "pnpm",
2102
- linter,
2103
- formatter,
2104
- isMonorepo: true,
2105
- platforms
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
2106
1555
  });
2107
- for (const [filePath, file] of Object.entries(aiFilesOutput)) {
2108
- const fullPath = join$1(monorepoRoot, filePath);
2109
- await mkdir$1(dirname$1(fullPath), { recursive: true });
2110
- await writeFile$1(fullPath, file.content);
2111
- console.log(color.dim(` Generated ${filePath}`));
2112
- }
2113
1556
  }
1557
+ continue;
2114
1558
  }
2115
- process.exit(0);
2116
- } catch (error) {
2117
- spinner.stop(color.red("\u2717") + " Failed to fix workspace");
2118
- console.error(error);
2119
- process.exit(1);
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
+ });
2120
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
+ );
2121
1634
  }
2122
- async function handleMigration(config, target, root, options) {
2123
- const plan = await getMigrationPlan(config, target, root);
2124
- console.log(color.cyan("Migration:"));
2125
- if (plan.fromLinter !== plan.toLinter) {
2126
- console.log(` Linter: ${color.dim(plan.fromLinter)} \u2192 ${color.green(plan.toLinter)}`);
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");
2127
1640
  }
2128
- if (plan.fromFormatter !== plan.toFormatter) {
2129
- console.log(
2130
- ` Formatter: ${color.dim(plan.fromFormatter)} \u2192 ${color.green(plan.toFormatter)}`
2131
- );
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 [];
2132
1650
  }
2133
- console.log();
2134
- console.log(color.cyan("Changes:"));
2135
- for (const change of plan.changes) {
2136
- console.log(formatMigrationChange(change));
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
1664
+ }
1665
+ ];
2137
1666
  }
2138
- if (plan.subPackageUpdates.length > 0) {
2139
- console.log();
2140
- console.log(color.cyan(`Sub-packages (${plan.subPackageUpdates.length}):`));
2141
- for (const update of plan.subPackageUpdates) {
2142
- const changes = [
2143
- ...update.remove.map((d) => `-${d}`),
2144
- ...update.add.map((d) => `+${d}`)
2145
- ].join(", ");
2146
- console.log(` ~ ${update.path} (${changes})`);
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
2147
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];
2148
1709
  }
2149
- console.log();
2150
- if (!options.yes) {
2151
- const confirm = await p.confirm({
2152
- message: "Apply migration?",
2153
- initialValue: true
2154
- });
2155
- if (p.isCancel(confirm) || !confirm) {
2156
- console.log(color.dim(" Migration cancelled"));
2157
- process.exit(0);
1710
+ if (fileContentsEqual(expected.path, currentContent, expected.newContent)) {
1711
+ return [
1712
+ {
1713
+ ...expected,
1714
+ status: "unchanged",
1715
+ currentContent,
1716
+ newContent: currentContent
1717
+ }
1718
+ ];
1719
+ }
1720
+ return [
1721
+ {
1722
+ ...expected,
1723
+ status: "modified",
1724
+ currentContent
2158
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 {
2159
1737
  }
2160
- await applyMigration(plan, root);
2161
- const aiWorkspacePath = join$1(root, ".ai/workspace.md");
2162
- const aiRulesExist = await fileExists(aiWorkspacePath);
2163
- if (aiRulesExist) {
2164
- console.log();
2165
- console.log(color.cyan("Updating AI rules..."));
2166
- const scope = await getMonorepoScope(root);
2167
- const existingPlatforms = [];
2168
- if (await fileExists(join$1(root, "AGENTS.md"))) {
2169
- existingPlatforms.push("agents");
2170
- }
2171
- if (await fileExists(join$1(root, "CLAUDE.md"))) {
2172
- existingPlatforms.push("claude");
2173
- }
2174
- const aiFilesOutput = {};
2175
- generateAiFiles(aiFilesOutput, {
2176
- name: scope,
2177
- packageManager: "pnpm",
2178
- linter: plan.toLinter,
2179
- formatter: plan.toFormatter,
2180
- isMonorepo: true,
2181
- platforms: existingPlatforms.length > 0 ? existingPlatforms : ["agents"]
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
2182
1753
  });
2183
- for (const [filePath, file] of Object.entries(aiFilesOutput)) {
2184
- const fullPath = join$1(root, filePath);
2185
- await mkdir$1(dirname$1(fullPath), { recursive: true });
2186
- await writeFile$1(fullPath, file.content);
2187
- console.log(color.dim(` ${filePath}`));
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;
2188
1779
  }
2189
1780
  }
2190
- console.log();
2191
- console.log(color.green("\u2713") + ` Migrated to ${plan.toLinter}/${plan.toFormatter}`);
2192
- console.log(color.dim(" Run `pnpm install` to update dependencies"));
2193
- process.exit(0);
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;
2194
1797
  }
2195
- async function handleUpdateCommand(options) {
2196
- const monorepoRoot = await detectMonorepoRoot();
2197
- const projectRoot = monorepoRoot ?? await detectPackageRoot();
2198
- if (!projectRoot) {
2199
- console.log(color.red("\u2717") + " Could not find a project root");
2200
- console.log(color.dim(" Run this command from inside a generated project"));
2201
- process.exit(1);
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);
2202
1804
  }
2203
- const isMonorepo = monorepoRoot != null;
2204
- if (isMonorepo) {
2205
- const { valid, errors } = await validateWorkspace(projectRoot);
2206
- if (!valid) {
2207
- console.log(color.yellow("!") + " Workspace has issues:");
2208
- for (const error of errors) {
2209
- console.log(color.dim(` \u2022 ${error}`));
2210
- }
2211
- console.log();
2212
- const shouldFix = options.yes || await p.confirm({
2213
- message: "Run fix first to resolve these issues?",
2214
- initialValue: true
2215
- });
2216
- if (p.isCancel(shouldFix) || !shouldFix) {
2217
- console.log(color.dim(" Run `pnpm create krispya --fix` to fix manually"));
2218
- process.exit(1);
2219
- }
2220
- const preFixConfig = await detectCurrentConfig(projectRoot);
2221
- const fixOptions = {
2222
- ...options,
2223
- linter: options.linter ?? preFixConfig.linter,
2224
- formatter: options.formatter ?? preFixConfig.formatter
2225
- };
2226
- await handleFixCommand(fixOptions);
2227
- }
2228
- } else if (options.linter || options.formatter) {
2229
- console.log(
2230
- color.yellow("!") + " Linter/formatter migrations in --update are currently monorepo-only"
2231
- );
2232
- console.log(color.dim(" Continuing with standalone shared config updates"));
2233
- console.log();
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);
2234
1854
  }
2235
- const config = await detectCurrentConfig(projectRoot, isMonorepo);
2236
- const targetLinter = options.linter;
2237
- const targetFormatter = options.formatter;
2238
- const migrationTarget = { linter: targetLinter, formatter: targetFormatter };
2239
- if (isMonorepo && needsMigration(config, migrationTarget)) {
2240
- await handleMigration(config, migrationTarget, projectRoot, options);
2241
- return;
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);
2242
1867
  }
2243
- console.log(
2244
- color.cyan("Checking for updates...") + color.dim(` (${config.linter}/${config.formatter})`)
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)
2245
1877
  );
2246
- console.log();
2247
- const expected = await generateExpectedFiles(config);
1878
+ }
1879
+ async function collectUpdateCategories(projectRoot, config, isMonorepo) {
1880
+ const expected = await planExpectedFiles(config);
2248
1881
  const categories = await compareWithDisk(expected, projectRoot);
2249
- const allCategories = categories.filter((c) => c.category !== "workspace-config");
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
+ }
2250
1903
  if (isMonorepo) {
2251
1904
  const workspaceConfigChanges = await getWorkspaceConfigUpdates(projectRoot);
2252
- const workspaceCategory = {
2253
- category: "workspace-config",
2254
- label: "Workspace Config",
2255
- changes: workspaceConfigChanges,
2256
- hasUserModifications: workspaceConfigChanges.some((c) => c.status === "modified")
2257
- };
2258
1905
  if (workspaceConfigChanges.length > 0) {
2259
- const configPkgIndex = allCategories.findIndex((c) => c.category === "config-packages");
2260
- if (configPkgIndex !== -1) {
2261
- allCategories.splice(configPkgIndex + 1, 0, workspaceCategory);
2262
- } else {
2263
- allCategories.push(workspaceCategory);
2264
- }
2265
- }
2266
- }
2267
- let updatedCount = 0;
2268
- let skippedCount = 0;
2269
- for (const category of allCategories) {
2270
- const newChanges = category.changes.filter((c) => c.status === "added");
2271
- const modifiedChanges = category.changes.filter((c) => c.status === "modified");
2272
- const hasNew = newChanges.length > 0;
2273
- const hasModified = modifiedChanges.length > 0;
2274
- const hasChanges = hasNew || hasModified;
2275
- if (!hasChanges) {
2276
- console.log(color.green("\u2713") + ` ${category.label}: Up to date`);
2277
- continue;
2278
- }
2279
- if (category.category === "ai-files") {
2280
- if (hasNew) {
2281
- console.log(color.cyan(category.label + ":"));
2282
- console.log(color.dim(` ${newChanges.length} AI file(s) can be added`));
2283
- console.log();
2284
- const applyAi = options.yes ? true : await p.confirm({
2285
- message: "Add AI rules?",
2286
- initialValue: true
2287
- });
2288
- if (!p.isCancel(applyAi) && applyAi) {
2289
- await applyUpdates(newChanges, projectRoot);
2290
- console.log(color.green("\u2713") + ` Added ${newChanges.length} AI file(s)`);
2291
- updatedCount++;
2292
- } else {
2293
- console.log(color.dim(` Skipped ${category.label}`));
2294
- skippedCount++;
2295
- }
2296
- }
2297
- if (hasModified) {
2298
- console.log(color.cyan("AI Files (existing):"));
2299
- for (const change of modifiedChanges) {
2300
- console.log(formatFileChange(change));
2301
- }
2302
- console.log();
2303
- if (options.yes) {
2304
- console.log(color.dim(" (--yes mode: keeping existing AI files)"));
2305
- } else {
2306
- const updateExisting = await p.confirm({
2307
- message: "Update existing AI files to latest template?",
2308
- initialValue: false
2309
- });
2310
- if (!p.isCancel(updateExisting) && updateExisting) {
2311
- await applyUpdates(modifiedChanges, projectRoot);
2312
- console.log(color.green("\u2713") + " Updated existing AI files");
2313
- }
2314
- }
2315
- }
2316
- console.log();
2317
- continue;
2318
- }
2319
- let changesToApply = [];
2320
- if (options.yes) {
2321
- console.log(color.cyan(category.label + ":"));
2322
- for (const change of [...newChanges, ...modifiedChanges]) {
2323
- console.log(formatFileChange(change));
2324
- }
2325
- console.log();
2326
- if (category.category === "workspace-config") {
2327
- changesToApply = [...newChanges, ...modifiedChanges];
2328
- if (changesToApply.length > 0) {
2329
- console.log(color.dim(" (--yes mode: applying merge updates)"));
2330
- }
2331
- } else {
2332
- changesToApply = newChanges;
2333
- if (newChanges.length > 0) {
2334
- console.log(color.dim(" (--yes mode: adding new files only)"));
2335
- }
2336
- }
2337
- } else if (hasNew && hasModified) {
2338
- const allChanges = [...newChanges, ...modifiedChanges];
2339
- const selectedFiles = await p.multiselect({
2340
- message: `${category.label} (+ new, ~ changed)`,
2341
- options: allChanges.map((change) => ({
2342
- value: change.path,
2343
- label: change.status === "added" ? `+ ${change.path}` : `~ ${change.path}`
2344
- })),
2345
- initialValues: newChanges.map((c) => c.path),
2346
- // Pre-select new files
2347
- required: false
1906
+ allCategories.push({
1907
+ category: "workspace-config",
1908
+ label: "Workspace Config",
1909
+ changes: workspaceConfigChanges,
1910
+ hasUserModifications: workspaceConfigChanges.some(
1911
+ (change) => change.status === "modified"
1912
+ )
2348
1913
  });
2349
- if (p.isCancel(selectedFiles)) {
2350
- p.cancel("Operation cancelled.");
2351
- process.exit(0);
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)"));
2352
1939
  }
2353
- if (selectedFiles.length > 0) {
2354
- 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)"));
2355
1944
  }
2356
- } else if (hasNew) {
2357
- console.log(color.cyan(category.label + ":"));
2358
- for (const change of newChanges) {
2359
- 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}`));
2360
1981
  }
2361
1982
  console.log();
2362
- const shouldAdd = await p.confirm({
2363
- message: `Add ${newChanges.length} new file(s)?`,
1983
+ const shouldFix = options.yes || await p.confirm({
1984
+ message: "Run fix first to resolve these issues?",
2364
1985
  initialValue: true
2365
1986
  });
2366
- if (p.isCancel(shouldAdd)) {
2367
- p.cancel("Operation cancelled.");
2368
- process.exit(0);
2369
- }
2370
- if (shouldAdd) {
2371
- changesToApply = newChanges;
2372
- }
2373
- } else if (hasModified) {
2374
- console.log(color.cyan(category.label + ":"));
2375
- for (const change of modifiedChanges) {
2376
- 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);
2377
1990
  }
2378
- console.log();
2379
- const shouldUpdate = await p.confirm({
2380
- message: `Update ${modifiedChanges.length} file(s)? (will overwrite)`,
2381
- 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
2382
1996
  });
2383
- if (p.isCancel(shouldUpdate)) {
2384
- p.cancel("Operation cancelled.");
2385
- process.exit(0);
2386
- }
2387
- if (shouldUpdate) {
2388
- changesToApply = modifiedChanges;
2389
- }
2390
- }
2391
- if (changesToApply.length > 0) {
2392
- await applyUpdates(changesToApply, projectRoot);
2393
- const addedCount = changesToApply.filter((c) => c.status === "added").length;
2394
- const updatedFilesCount = changesToApply.filter((c) => c.status === "modified").length;
2395
- const parts = [];
2396
- if (addedCount > 0) parts.push(`added ${addedCount}`);
2397
- if (updatedFilesCount > 0) parts.push(`updated ${updatedFilesCount}`);
2398
- console.log(color.green("\u2713") + ` ${category.label}: ${parts.join(", ")}`);
2399
- updatedCount++;
2400
- } else {
2401
- console.log(color.dim(` Skipped ${category.label}`));
2402
- skippedCount++;
2403
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"));
2404
2005
  console.log();
2405
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
+ }
2406
2019
  if (updatedCount === 0 && skippedCount === 0) {
2407
2020
  console.log(color.green("\u2713") + " Everything is up to date!");
2408
2021
  } else if (updatedCount > 0) {
@@ -2415,7 +2028,103 @@ async function handleUpdateCommand(options) {
2415
2028
  }
2416
2029
  process.exit(0);
2417
2030
  }
2418
- 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) {
2419
2128
  const monorepoRoot = await detectMonorepoRoot();
2420
2129
  if (!monorepoRoot) {
2421
2130
  console.error(color.red("Error:") + " --workspace flag requires being inside a monorepo");
@@ -2436,7 +2145,7 @@ async function handleWorkspaceCommand(name, options) {
2436
2145
  const scopedName = name.startsWith("@") ? name : `@${scope}/${name}`;
2437
2146
  const fullPackagePath = join$1(monorepoRoot, targetDir, name);
2438
2147
  try {
2439
- await access$1(fullPackagePath, constants$2.F_OK);
2148
+ await access$1(fullPackagePath, constants$1.F_OK);
2440
2149
  console.error(color.red("Error:") + ` Directory ${targetDir}/${name} already exists`);
2441
2150
  process.exit(1);
2442
2151
  } catch {
@@ -2452,7 +2161,7 @@ async function handleWorkspaceCommand(name, options) {
2452
2161
  const isLibrary = projectType === "library";
2453
2162
  const relativePkgPath = join$1(targetDir, name);
2454
2163
  const workspaceRoot = calculateWorkspaceRoot(relativePkgPath);
2455
- const generateOptions = {
2164
+ const projectOptions = {
2456
2165
  name: scopedName,
2457
2166
  projectType,
2458
2167
  libraryBundler: isLibrary ? options.bundler ?? "unbuild" : void 0,
@@ -2478,12 +2187,9 @@ async function handleWorkspaceCommand(name, options) {
2478
2187
  triplex: options.triplex ? {} : void 0
2479
2188
  }
2480
2189
  };
2481
- generateOptions.packageManager = await resolvePackageManager(generateOptions);
2482
- generateOptions.engine = await resolveEngine(generateOptions);
2483
- generateOptions.versions = await resolveProjectPackageVersions(generateOptions);
2484
2190
  console.log(color.cyan("Creating") + ` ${scopedName} in ${targetDir}/${name}...`);
2485
2191
  try {
2486
- const files = generate(generateOptions);
2192
+ const { files } = await planProject(resolveProjectPlanInput(projectOptions));
2487
2193
  await writeGeneratedFiles(fullPackagePath, files);
2488
2194
  console.log(color.green("\u2713") + ` Created ${scopedName} at ${targetDir}/${name}`);
2489
2195
  process.exit(0);
@@ -2493,72 +2199,130 @@ async function handleWorkspaceCommand(name, options) {
2493
2199
  process.exit(1);
2494
2200
  }
2495
2201
  }
2496
- async function handleMonorepoCreation(generateOptions, isNonInteractive) {
2497
- const { generateMonorepo } = await import('./chunks/index.mjs').then(function (n) { return n.I; });
2498
- const packageManager = getPackageManagerName(generateOptions.packageManager);
2499
- generateOptions.packageManager = await resolvePackageManager(generateOptions);
2500
- generateOptions.engine = await resolveEngine(generateOptions);
2501
- generateOptions.versions = await resolveMonorepoRootPackageVersions({
2502
- linter: generateOptions.linter ?? "oxlint",
2503
- formatter: generateOptions.formatter ?? "prettier",
2504
- 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"
2505
2210
  });
2506
- const aiPlatforms = await promptForAiPlatforms(isNonInteractive);
2507
- 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);
2508
2278
  const spinner = p.spinner();
2509
2279
  spinner.start("Creating monorepo workspace...");
2510
2280
  try {
2511
- const { files } = generateMonorepo({
2512
- name: generateOptions.name,
2513
- linter: generateOptions.linter ?? "oxlint",
2514
- formatter: generateOptions.formatter ?? "prettier",
2515
- 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 ?? {
2516
2286
  name: packageManager
2517
2287
  },
2518
- pnpmManageVersions: generateOptions.pnpmManageVersions,
2519
- engine: generateOptions.engine,
2520
- versions: generateOptions.versions,
2521
- 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"
2522
2293
  });
2523
- const filePaths = Object.keys(files).sort();
2524
- for (const filePath of filePaths) {
2525
- const fullFilePath = join$1(projectPath, filePath);
2526
- await mkdir$1(dirname$1(fullFilePath), { recursive: true });
2527
- const file = files[filePath];
2528
- if (file.type === "text") {
2529
- await writeFile$1(fullFilePath, file.content);
2530
- }
2531
- }
2294
+ const { files } = await planWorkspace(planInput);
2295
+ await writeGeneratedFiles(projectPath, files);
2532
2296
  spinner.stop(color.green.inverse(" \u2713 Monorepo workspace created! "));
2533
2297
  if (isNonInteractive) {
2534
2298
  process.exit(0);
2535
2299
  }
2536
2300
  const newWorkspaceSettings = {
2537
- linter: generateOptions.linter,
2538
- formatter: generateOptions.formatter,
2539
- packageManager: generateOptions.packageManager ?? {
2301
+ linter: projectOptions.linter,
2302
+ formatter: projectOptions.formatter,
2303
+ packageManager: projectOptions.packageManager ?? {
2540
2304
  name: packageManager
2541
2305
  },
2542
- engine: generateOptions.engine,
2543
- pnpmManageVersions: generateOptions.pnpmManageVersions
2306
+ engine: projectOptions.engine,
2307
+ pnpmManageVersions: projectOptions.pnpmManageVersions
2544
2308
  };
2545
- const scope = generateOptions.name;
2309
+ const scope = projectOptions.name;
2546
2310
  let addMore = true;
2547
2311
  while (addMore) {
2548
2312
  addMore = await createPackageInWorkspace(
2549
2313
  projectPath,
2550
2314
  packageManager,
2551
2315
  newWorkspaceSettings,
2552
- scope
2316
+ scope,
2317
+ writeGeneratedFiles
2553
2318
  );
2554
2319
  }
2555
2320
  const nextSteps = [
2556
- `cd ${generateOptions.name}`,
2321
+ `cd ${projectOptions.name}`,
2557
2322
  `${packageManager} install`,
2558
2323
  `${packageManager} run dev`
2559
2324
  ].join("\n");
2560
2325
  p.note(nextSteps, "Next steps");
2561
- await promptAndOpenEditor(projectPath);
2562
2326
  p.outro(color.green("Happy coding! \u2728"));
2563
2327
  process.exit(0);
2564
2328
  } catch (error) {
@@ -2567,38 +2331,30 @@ async function handleMonorepoCreation(generateOptions, isNonInteractive) {
2567
2331
  process.exit(1);
2568
2332
  }
2569
2333
  }
2570
- async function handleStandaloneProjectCreation(generateOptions, isNonInteractive) {
2571
- const base = generateOptions.template ? getBaseTemplate(generateOptions.template) : "vanilla";
2334
+ async function handleSingleWorkspaceCreation(projectOptions, isNonInteractive) {
2335
+ const base = projectOptions.template ? getBaseTemplate(projectOptions.template) : "vanilla";
2572
2336
  const defaultFallbackName = base === "vanilla" ? "vanilla-app" : base === "react" ? "react-app" : "react-three-app";
2573
- generateOptions.name ??= defaultFallbackName;
2574
- const aiPlatforms = await promptForAiPlatforms(isNonInteractive);
2575
- if (aiPlatforms.length > 0) {
2576
- generateOptions.aiPlatforms = aiPlatforms;
2577
- }
2578
- const packageManager = getPackageManagerName(generateOptions.packageManager);
2579
- const isLibrary = generateOptions.projectType === "library";
2580
- generateOptions.packageManager = await resolvePackageManager(generateOptions);
2581
- generateOptions.engine = await resolveEngine(generateOptions);
2582
- generateOptions.versions = await resolveProjectPackageVersions(generateOptions);
2583
- 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);
2584
2340
  const spinner = p.spinner();
2585
2341
  spinner.start("Creating project...");
2586
2342
  try {
2587
- const files = generate(generateOptions);
2343
+ const planInput = resolveProjectPlanInput(projectOptions);
2344
+ const { files } = await planProject(planInput);
2588
2345
  await writeGeneratedFiles(projectPath, files);
2589
2346
  spinner.stop(color.green.inverse(" \u2713 Project created! "));
2590
2347
  if (isNonInteractive) process.exit(0);
2591
- const nextSteps = isLibrary ? [
2592
- `cd ${generateOptions.name}`,
2348
+ const nextSteps = projectOptions.projectType === "library" ? [
2349
+ `cd ${projectOptions.name}`,
2593
2350
  `${packageManager} install`,
2594
2351
  `${packageManager} run build`
2595
2352
  ].join("\n") : [
2596
- `cd ${generateOptions.name}`,
2353
+ `cd ${projectOptions.name}`,
2597
2354
  `${packageManager} install`,
2598
2355
  `${packageManager} run dev`
2599
2356
  ].join("\n");
2600
2357
  p.note(nextSteps, "Next steps");
2601
- await promptAndOpenEditor(projectPath);
2602
2358
  p.outro(color.green("Happy coding! \u2728"));
2603
2359
  } catch (error) {
2604
2360
  spinner.stop("Failed to create project");
@@ -2606,46 +2362,6 @@ async function handleStandaloneProjectCreation(generateOptions, isNonInteractive
2606
2362
  process.exit(1);
2607
2363
  }
2608
2364
  }
2609
- async function handleInteractiveMonorepoMode(monorepoRoot) {
2610
- const choice = await p.select({
2611
- message: "Detected monorepo workspace",
2612
- options: [
2613
- { value: "add", label: "Add new package to this workspace" },
2614
- { value: "standalone", label: "Create standalone project" }
2615
- ],
2616
- initialValue: "add"
2617
- });
2618
- if (p.isCancel(choice)) {
2619
- p.cancel("Operation cancelled.");
2620
- process.exit(0);
2621
- }
2622
- if (choice === "add") {
2623
- const inheritedSettings = await detectWorkspaceSettings(monorepoRoot);
2624
- const hasSettings = Object.values(inheritedSettings).some(Boolean);
2625
- if (hasSettings) {
2626
- const settingsInfo = [
2627
- inheritedSettings.linter && `linter: ${inheritedSettings.linter}`,
2628
- inheritedSettings.formatter && `formatter: ${inheritedSettings.formatter}`,
2629
- inheritedSettings.packageManager && `pm: ${inheritedSettings.packageManager.name}`
2630
- ].filter(Boolean).join(", ");
2631
- p.log.info(`Using workspace settings (${settingsInfo})`);
2632
- }
2633
- const scope = await getMonorepoScope(monorepoRoot);
2634
- let addMore = true;
2635
- while (addMore) {
2636
- addMore = await createPackageInWorkspace(
2637
- monorepoRoot,
2638
- inheritedSettings.packageManager?.name ?? "pnpm",
2639
- inheritedSettings,
2640
- scope
2641
- );
2642
- }
2643
- p.note([`cd ${monorepoRoot}`, "pnpm install", "pnpm run dev"].join("\n"), "Next steps");
2644
- await promptAndOpenEditor(monorepoRoot);
2645
- p.outro(color.green("Happy coding! \u2728"));
2646
- process.exit(0);
2647
- }
2648
- }
2649
2365
  async function main() {
2650
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(
2651
2367
  "--bundler <bundler>",
@@ -2653,7 +2369,7 @@ async function main() {
2653
2369
  ).option(
2654
2370
  "--template <type>",
2655
2371
  "project template: vanilla, vanilla-js, react, react-js, r3f, r3f-js (default: vanilla)"
2656
- ).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(
2657
2373
  "--pnpm-manage-versions",
2658
2374
  "enable manage-package-manager-versions in pnpm-workspace.yaml (default: true)"
2659
2375
  ).option(
@@ -2662,7 +2378,7 @@ async function main() {
2662
2378
  ).option(
2663
2379
  "--node-version <version>",
2664
2380
  'set Node.js version for engines.node field (default: "latest")'
2665
- ).option("--workspace", "Add package to current monorepo workspace (non-interactive)").option("--dir <directory>", "Target directory for --workspace (default: apps/ or packages/)").option("--clear-config", "Clear saved preferences (e.g. editor choice)").option("--config-path", "Print the path to the config file").option("--check", "Check if current directory is in a valid monorepo workspace").option("--fix", "Fix monorepo by generating missing .config packages").option("--update", "Update monorepo workspace to latest configuration").option("-y, --yes", "Non-interactive mode - accept all prompts").option(
2381
+ ).option("--workspace", "Add package to current monorepo workspace (non-interactive)").option("--dir <directory>", "Target directory for --workspace (default: apps/ or packages/)").option("--clear-config", "Clear saved preferences").option("--config-path", "Print the path to the config file").option("--check", "Check if current directory is in a valid monorepo workspace").option("--fix", "Fix monorepo by generating missing .config packages").option("--update", "Update monorepo workspace to latest configuration").option("-y, --yes", "Non-interactive mode - accept all prompts").option(
2666
2382
  "--path <directory>",
2667
2383
  "Run in specified directory instead of current working directory"
2668
2384
  ).action(async (name, options) => {
@@ -2678,6 +2394,10 @@ async function main() {
2678
2394
  console.log(getConfigPath());
2679
2395
  process.exit(0);
2680
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
+ }
2681
2401
  if (name?.startsWith("-")) {
2682
2402
  switch (name) {
2683
2403
  case "--version":
@@ -2719,7 +2439,7 @@ async function main() {
2719
2439
  await handleFixCommand(options);
2720
2440
  }
2721
2441
  if (options.update) {
2722
- await handleUpdateCommand(options);
2442
+ await handleUpdateCommand(options, handleFixCommand);
2723
2443
  }
2724
2444
  if (options.dir && !options.workspace) {
2725
2445
  console.error(color.red("Error:") + " --dir requires --workspace flag");
@@ -2729,27 +2449,28 @@ async function main() {
2729
2449
  process.exit(1);
2730
2450
  }
2731
2451
  if (options.workspace) {
2732
- await handleWorkspaceCommand(name, options);
2452
+ await handleWorkspaceCommand(name, options, writeGeneratedFiles);
2733
2453
  }
2734
2454
  console.clear();
2735
2455
  p.intro(color.bgCyan(color.black(` create-krispya v${pkg.version} `)));
2736
2456
  const monorepoRoot = await detectMonorepoRoot();
2737
2457
  if (monorepoRoot && !hasConfigOptions(options)) {
2738
- await handleInteractiveMonorepoMode(monorepoRoot);
2458
+ await handleInteractiveMonorepoMode(monorepoRoot, writeGeneratedFiles);
2739
2459
  }
2740
- let generateOptions;
2460
+ let projectOptions;
2741
2461
  if (options.yes) {
2742
2462
  const template = options.template ?? "vanilla";
2743
2463
  const baseTemplate = getBaseTemplate(template);
2744
2464
  const defaultName = getDefaultProjectName(template);
2745
2465
  const projectType = options.type ?? "app";
2746
- generateOptions = {
2466
+ projectOptions = {
2747
2467
  name: name || defaultName,
2748
2468
  projectType,
2749
2469
  libraryBundler: projectType === "library" ? options.bundler ?? "unbuild" : void 0,
2750
2470
  template,
2751
2471
  linter: options.linter ?? "oxlint",
2752
2472
  formatter: options.formatter ?? "prettier",
2473
+ ide: options.ide ?? "vscode",
2753
2474
  ...baseTemplate === "r3f" && {
2754
2475
  drei: options.drei ? {} : void 0,
2755
2476
  handle: options.handle ? {} : void 0,
@@ -2776,6 +2497,7 @@ async function main() {
2776
2497
  linter: options.linter,
2777
2498
  formatter: options.formatter,
2778
2499
  packageManager: options.packageManager,
2500
+ ide: options.ide,
2779
2501
  engine: options.nodeVersion ? { name: "node", version: options.nodeVersion } : void 0,
2780
2502
  pnpmManageVersions: options.pnpmManageVersions,
2781
2503
  drei: options.drei,
@@ -2791,13 +2513,17 @@ async function main() {
2791
2513
  triplex: options.triplex,
2792
2514
  viverse: options.viverse
2793
2515
  } : void 0;
2794
- generateOptions = await promptForOptions(name, presets);
2516
+ projectOptions = await promptForOptions(name, presets);
2795
2517
  }
2796
2518
  const isNonInteractive = options.yes ?? false;
2797
- if (generateOptions.projectType === "monorepo") {
2798
- 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);
2799
2525
  } else {
2800
- await handleStandaloneProjectCreation(generateOptions, isNonInteractive);
2526
+ await handleSingleWorkspaceCreation(projectOptions, isNonInteractive);
2801
2527
  }
2802
2528
  });
2803
2529
  await program.parseAsync();