create-krispya 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -2,14 +2,14 @@
2
2
  import { createRequire } from 'module';
3
3
  import { cwd } from 'process';
4
4
  import { join, dirname, resolve } from 'path';
5
- import { mkdir, writeFile, access, readFile } from 'fs/promises';
6
- import { constants } from 'fs';
5
+ import { access, constants, mkdir, writeFile, unlink, readFile } from 'fs/promises';
6
+ import { constants as constants$1 } from 'fs';
7
7
  import { Command } from 'commander';
8
8
  import * as p from '@clack/prompts';
9
9
  import color from 'chalk';
10
10
  import { fetch } from 'undici';
11
11
  import { spawn } from 'child_process';
12
- import { g as getBaseTemplate, a as getLanguageFromTemplate, b as generateRandomName, c as getLatestPnpmVersion, d as getLatestNodeVersion, e as getLatestNpmVersion, f as generate } from './chunks/index.mjs';
12
+ import { g as getBaseTemplate, a as getLanguageFromTemplate, b as generateRandomName, c as generateTypescriptConfigPackage, d as generateOxlintConfigPackage, e as generateEslintConfigPackage, f as generateOxfmtConfigPackage, h as generatePrettierConfigPackage, i as generateVscodeFiles, j as generateAiFiles, k as getLatestNpmVersion, l as generate, m as getLatestPnpmVersion, n as getLatestYarnVersion, o as getLatestNpmCliVersion, p as getLatestNodeVersion, v as validatePackageName, q as parseWorkspaceYamlContent } from './chunks/index.mjs';
13
13
  import Conf from 'conf';
14
14
 
15
15
  const editorNames = {
@@ -123,7 +123,38 @@ function formatMonorepoConfigSummary(options) {
123
123
  return lines.join("\n");
124
124
  }
125
125
 
126
- function getDefaultOptions(template, name, projectType = "app", libraryBundler) {
126
+ const config = new Conf({
127
+ projectName: "create-krispya"
128
+ });
129
+ function getPreferredEditor() {
130
+ return config.get("preferredEditor");
131
+ }
132
+ function setPreferredEditor(editor) {
133
+ config.set("preferredEditor", editor);
134
+ }
135
+ function getReuseWindow() {
136
+ return config.get("reuseWindow") ?? false;
137
+ }
138
+ function setReuseWindow(reuse) {
139
+ config.set("reuseWindow", reuse);
140
+ }
141
+ function getAiFiles() {
142
+ return config.get("aiFiles");
143
+ }
144
+ function setAiFiles(files) {
145
+ config.set("aiFiles", files);
146
+ }
147
+ function clearConfig() {
148
+ config.clear();
149
+ }
150
+ function getConfigPath() {
151
+ return config.path;
152
+ }
153
+ function getCustomTemplates() {
154
+ return config.get("customTemplates") ?? {};
155
+ }
156
+
157
+ function getDefaultOptions(template, name, projectType = "app", libraryBundler, integrations, inheritedTooling) {
127
158
  const baseTemplate = getBaseTemplate(template);
128
159
  const base = {
129
160
  name,
@@ -133,26 +164,26 @@ function getDefaultOptions(template, name, projectType = "app", libraryBundler)
133
164
  packageManager: "pnpm",
134
165
  pnpmManageVersions: true,
135
166
  nodeVersion: "latest",
136
- linter: "oxlint",
137
- formatter: "oxfmt",
167
+ linter: inheritedTooling?.linter ?? "oxlint",
168
+ formatter: inheritedTooling?.formatter ?? "oxfmt",
138
169
  // Libraries get vitest by default, apps don't
139
170
  testing: projectType === "library" ? "vitest" : "none"
140
171
  };
141
- if (baseTemplate === "r3f") {
172
+ if (baseTemplate === "r3f" && integrations) {
142
173
  return {
143
174
  ...base,
144
- drei: {},
145
- handle: {},
146
- leva: {},
147
- postprocessing: {},
148
- rapier: {},
149
- xr: {},
150
- uikit: {},
151
- offscreen: {},
152
- zustand: {},
153
- koota: {},
154
- triplex: {},
155
- viverse: {}
175
+ drei: integrations.includes("drei") ? {} : void 0,
176
+ handle: integrations.includes("handle") ? {} : void 0,
177
+ leva: integrations.includes("leva") ? {} : void 0,
178
+ postprocessing: integrations.includes("postprocessing") ? {} : void 0,
179
+ rapier: integrations.includes("rapier") ? {} : void 0,
180
+ xr: integrations.includes("xr") ? {} : void 0,
181
+ uikit: integrations.includes("uikit") ? {} : void 0,
182
+ offscreen: integrations.includes("offscreen") ? {} : void 0,
183
+ zustand: integrations.includes("zustand") ? {} : void 0,
184
+ koota: integrations.includes("koota") ? {} : void 0,
185
+ triplex: integrations.includes("triplex") ? {} : void 0,
186
+ viverse: integrations.includes("viverse") ? {} : void 0
156
187
  };
157
188
  }
158
189
  return base;
@@ -168,7 +199,33 @@ function getDefaultProjectName(template) {
168
199
  return `react-three-${generateRandomName()}`;
169
200
  }
170
201
  }
171
- async function promptForCustomization(template, name, projectType) {
202
+ async function promptForR3fIntegrations() {
203
+ const selected = await p.multiselect({
204
+ message: "R3F integrations",
205
+ options: [
206
+ { value: "drei", label: "Drei" },
207
+ { value: "handle", label: "Handle" },
208
+ { value: "leva", label: "Leva" },
209
+ { value: "postprocessing", label: "Postprocessing" },
210
+ { value: "rapier", label: "Rapier" },
211
+ { value: "xr", label: "XR" },
212
+ { value: "uikit", label: "UIKit" },
213
+ { value: "offscreen", label: "Offscreen" },
214
+ { value: "zustand", label: "Zustand" },
215
+ { value: "koota", label: "Koota" },
216
+ { value: "triplex", label: "Triplex" },
217
+ { value: "viverse", label: "Viverse" }
218
+ ],
219
+ initialValues: ["drei"],
220
+ required: false
221
+ });
222
+ if (p.isCancel(selected)) {
223
+ p.cancel("Operation cancelled.");
224
+ process.exit(0);
225
+ }
226
+ return selected;
227
+ }
228
+ async function promptForCustomization(template, name, projectType, integrations, inheritedTooling) {
172
229
  let libraryBundler;
173
230
  if (projectType === "library") {
174
231
  const bundler = await p.select({
@@ -240,31 +297,39 @@ async function promptForCustomization(template, name, projectType) {
240
297
  }
241
298
  pnpmManageVersions = managePnpm;
242
299
  }
243
- const linter = await p.select({
244
- message: "Linter",
245
- options: [
246
- { value: "oxlint", label: "Oxlint", hint: "fast, from OXC" },
247
- { value: "eslint", label: "ESLint", hint: "classic" },
248
- { value: "biome", label: "Biome", hint: "all-in-one" }
249
- ],
250
- initialValue: "oxlint"
251
- });
252
- if (p.isCancel(linter)) {
253
- p.cancel("Operation cancelled.");
254
- process.exit(0);
300
+ let linter = inheritedTooling?.linter ?? "oxlint";
301
+ let formatter = inheritedTooling?.formatter ?? "oxfmt";
302
+ if (!inheritedTooling?.linter) {
303
+ const linterChoice = await p.select({
304
+ message: "Linter",
305
+ options: [
306
+ { value: "oxlint", label: "Oxlint", hint: "fast, from OXC" },
307
+ { value: "eslint", label: "ESLint", hint: "classic" },
308
+ { value: "biome", label: "Biome", hint: "all-in-one" }
309
+ ],
310
+ initialValue: "oxlint"
311
+ });
312
+ if (p.isCancel(linterChoice)) {
313
+ p.cancel("Operation cancelled.");
314
+ process.exit(0);
315
+ }
316
+ linter = linterChoice;
255
317
  }
256
- const formatter = await p.select({
257
- message: "Formatter",
258
- options: [
259
- { value: "oxfmt", label: "Oxfmt", hint: "fast, Prettier-compatible" },
260
- { value: "prettier", label: "Prettier", hint: "classic" },
261
- { value: "biome", label: "Biome", hint: "all-in-one" }
262
- ],
263
- initialValue: "oxfmt"
264
- });
265
- if (p.isCancel(formatter)) {
266
- p.cancel("Operation cancelled.");
267
- process.exit(0);
318
+ if (!inheritedTooling?.formatter) {
319
+ const formatterChoice = await p.select({
320
+ message: "Formatter",
321
+ options: [
322
+ { value: "oxfmt", label: "Oxfmt", hint: "fast, Prettier-compatible" },
323
+ { value: "prettier", label: "Prettier", hint: "classic" },
324
+ { value: "biome", label: "Biome", hint: "all-in-one" }
325
+ ],
326
+ initialValue: "oxfmt"
327
+ });
328
+ if (p.isCancel(formatterChoice)) {
329
+ p.cancel("Operation cancelled.");
330
+ process.exit(0);
331
+ }
332
+ formatter = formatterChoice;
268
333
  }
269
334
  const testing = await p.select({
270
335
  message: "Testing",
@@ -292,47 +357,7 @@ async function promptForCustomization(template, name, projectType) {
292
357
  }
293
358
  const baseTemplate = getBaseTemplate(template);
294
359
  const finalTemplate = language === "javascript" ? `${baseTemplate}-js` : baseTemplate;
295
- let integrations = [];
296
- if (baseTemplate === "r3f") {
297
- const selected = await p.multiselect({
298
- message: "R3F integrations",
299
- options: [
300
- { value: "drei", label: "Drei" },
301
- { value: "handle", label: "Handle" },
302
- { value: "leva", label: "Leva" },
303
- { value: "postprocessing", label: "Postprocessing" },
304
- { value: "rapier", label: "Rapier" },
305
- { value: "xr", label: "XR" },
306
- { value: "uikit", label: "UIKit" },
307
- { value: "offscreen", label: "Offscreen" },
308
- { value: "zustand", label: "Zustand" },
309
- { value: "koota", label: "Koota" },
310
- { value: "triplex", label: "Triplex" },
311
- { value: "viverse", label: "Viverse" }
312
- ],
313
- initialValues: [
314
- "drei",
315
- "handle",
316
- "leva",
317
- "postprocessing",
318
- "rapier",
319
- "xr",
320
- "uikit",
321
- "offscreen",
322
- "zustand",
323
- "koota",
324
- "triplex",
325
- "viverse"
326
- ],
327
- required: false
328
- });
329
- if (p.isCancel(selected)) {
330
- p.cancel("Operation cancelled.");
331
- process.exit(0);
332
- }
333
- integrations = selected;
334
- }
335
- return {
360
+ const base = {
336
361
  name,
337
362
  template: finalTemplate,
338
363
  projectType,
@@ -342,8 +367,11 @@ async function promptForCustomization(template, name, projectType) {
342
367
  pnpmManageVersions,
343
368
  linter,
344
369
  formatter,
345
- testing,
346
- ...baseTemplate === "r3f" && {
370
+ testing
371
+ };
372
+ if (baseTemplate === "r3f" && integrations) {
373
+ return {
374
+ ...base,
347
375
  drei: integrations.includes("drei") ? {} : void 0,
348
376
  handle: integrations.includes("handle") ? {} : void 0,
349
377
  leva: integrations.includes("leva") ? {} : void 0,
@@ -356,8 +384,9 @@ async function promptForCustomization(template, name, projectType) {
356
384
  koota: integrations.includes("koota") ? {} : void 0,
357
385
  triplex: integrations.includes("triplex") ? {} : void 0,
358
386
  viverse: integrations.includes("viverse") ? {} : void 0
359
- }
360
- };
387
+ };
388
+ }
389
+ return base;
361
390
  }
362
391
  async function promptForInitialPackage() {
363
392
  const choice = await p.select({
@@ -402,31 +431,14 @@ async function promptForMonorepoCustomization(name) {
402
431
  p.cancel("Operation cancelled.");
403
432
  process.exit(0);
404
433
  }
405
- const packageManager = await p.select({
406
- message: "Package manager",
407
- options: [
408
- { value: "pnpm", label: "pnpm" },
409
- { value: "npm", label: "npm" },
410
- { value: "yarn", label: "yarn" }
411
- ],
412
- initialValue: "pnpm"
434
+ const managePnpm = await p.confirm({
435
+ message: "Enable manage-package-manager-versions?",
436
+ initialValue: true
413
437
  });
414
- if (p.isCancel(packageManager)) {
438
+ if (p.isCancel(managePnpm)) {
415
439
  p.cancel("Operation cancelled.");
416
440
  process.exit(0);
417
441
  }
418
- let pnpmManageVersions = true;
419
- if (packageManager === "pnpm") {
420
- const managePnpm = await p.confirm({
421
- message: "Enable manage-package-manager-versions?",
422
- initialValue: true
423
- });
424
- if (p.isCancel(managePnpm)) {
425
- p.cancel("Operation cancelled.");
426
- process.exit(0);
427
- }
428
- pnpmManageVersions = managePnpm;
429
- }
430
442
  const linter = await p.select({
431
443
  message: "Linter",
432
444
  options: [
@@ -457,8 +469,8 @@ async function promptForMonorepoCustomization(name) {
457
469
  name,
458
470
  projectType: "monorepo",
459
471
  nodeVersion,
460
- packageManager,
461
- pnpmManageVersions,
472
+ packageManager: "pnpm",
473
+ pnpmManageVersions: managePnpm,
462
474
  linter,
463
475
  formatter
464
476
  };
@@ -476,19 +488,15 @@ async function promptForMonorepo(workspaceName) {
476
488
  }),
477
489
  "Workspace Configuration"
478
490
  );
479
- const action = await p.select({
491
+ const proceed = await p.confirm({
480
492
  message: "Proceed with these settings?",
481
- options: [
482
- { value: "confirm", label: "Yes, create workspace" },
483
- { value: "customize", label: "No, let me customize" }
484
- ],
485
- initialValue: "confirm"
493
+ initialValue: true
486
494
  });
487
- if (p.isCancel(action)) {
495
+ if (p.isCancel(proceed)) {
488
496
  p.cancel("Operation cancelled.");
489
497
  process.exit(0);
490
498
  }
491
- if (action === "confirm") {
499
+ if (proceed) {
492
500
  return defaultOptions;
493
501
  }
494
502
  return promptForMonorepoCustomization(workspaceName);
@@ -528,76 +536,204 @@ async function promptForOptions(name) {
528
536
  }
529
537
  return promptForPackageOptions(projectName, projectType);
530
538
  }
531
- async function promptForPackageOptions(projectName, projectType) {
532
- const template = await p.select({
539
+ function customTemplateToOptions(customTemplate, name, projectType) {
540
+ const baseTemplate = customTemplate.baseTemplate;
541
+ const template = baseTemplate;
542
+ const base = {
543
+ name,
544
+ template,
545
+ projectType,
546
+ packageManager: "pnpm",
547
+ pnpmManageVersions: true,
548
+ nodeVersion: "latest",
549
+ linter: customTemplate.linter,
550
+ formatter: customTemplate.formatter,
551
+ testing: customTemplate.testing
552
+ };
553
+ if (baseTemplate === "r3f" && customTemplate.integrations) {
554
+ const integrations = customTemplate.integrations;
555
+ return {
556
+ ...base,
557
+ drei: integrations.includes("drei") ? {} : void 0,
558
+ handle: integrations.includes("handle") ? {} : void 0,
559
+ leva: integrations.includes("leva") ? {} : void 0,
560
+ postprocessing: integrations.includes("postprocessing") ? {} : void 0,
561
+ rapier: integrations.includes("rapier") ? {} : void 0,
562
+ xr: integrations.includes("xr") ? {} : void 0,
563
+ uikit: integrations.includes("uikit") ? {} : void 0,
564
+ offscreen: integrations.includes("offscreen") ? {} : void 0,
565
+ zustand: integrations.includes("zustand") ? {} : void 0,
566
+ koota: integrations.includes("koota") ? {} : void 0,
567
+ triplex: integrations.includes("triplex") ? {} : void 0,
568
+ viverse: integrations.includes("viverse") ? {} : void 0
569
+ };
570
+ }
571
+ return base;
572
+ }
573
+ async function promptForPackageOptions(projectName, projectType, inheritedTooling) {
574
+ const builtInOptions = [
575
+ { value: "vanilla", label: "Vanilla" },
576
+ { value: "react", label: "React" },
577
+ { value: "r3f", label: "React Three Fiber" }
578
+ ];
579
+ const customTemplates = getCustomTemplates();
580
+ const customOptions = Object.keys(customTemplates).map((name) => ({
581
+ value: `custom:${name}`,
582
+ label: name,
583
+ hint: "saved template"
584
+ }));
585
+ const allOptions = [...builtInOptions, ...customOptions];
586
+ const templateSelection = await p.select({
533
587
  message: "Select a template",
534
- options: [
535
- { value: "vanilla", label: "Vanilla" },
536
- { value: "react", label: "React" },
537
- { value: "r3f", label: "React Three Fiber" }
538
- ],
588
+ options: allOptions,
539
589
  initialValue: "vanilla"
540
590
  });
541
- if (p.isCancel(template)) {
591
+ if (p.isCancel(templateSelection)) {
542
592
  p.cancel("Operation cancelled.");
543
593
  process.exit(0);
544
594
  }
595
+ const selection = templateSelection;
596
+ if (selection.startsWith("custom:")) {
597
+ const customName = selection.slice(7);
598
+ const customTemplate = customTemplates[customName];
599
+ const defaultOptions2 = customTemplateToOptions(customTemplate, projectName, projectType);
600
+ if (inheritedTooling?.linter) {
601
+ defaultOptions2.linter = inheritedTooling.linter;
602
+ }
603
+ if (inheritedTooling?.formatter) {
604
+ defaultOptions2.formatter = inheritedTooling.formatter;
605
+ }
606
+ const configTitle2 = inheritedTooling ? `Template: ${customName} (using workspace tooling)` : `Template: ${customName}`;
607
+ p.note(formatConfigSummary(defaultOptions2), configTitle2);
608
+ const proceed2 = await p.confirm({
609
+ message: "Proceed with these settings?",
610
+ initialValue: true
611
+ });
612
+ if (p.isCancel(proceed2)) {
613
+ p.cancel("Operation cancelled.");
614
+ process.exit(0);
615
+ }
616
+ if (proceed2) {
617
+ return defaultOptions2;
618
+ }
619
+ return promptForCustomization(
620
+ customTemplate.baseTemplate,
621
+ projectName,
622
+ projectType,
623
+ customTemplate.integrations,
624
+ inheritedTooling
625
+ );
626
+ }
627
+ const template = selection;
628
+ const baseTemplate = getBaseTemplate(template);
629
+ let integrations;
630
+ if (baseTemplate === "r3f") {
631
+ integrations = await promptForR3fIntegrations();
632
+ }
545
633
  const defaultOptions = getDefaultOptions(
546
634
  template,
547
635
  projectName,
548
- projectType
636
+ projectType,
637
+ void 0,
638
+ integrations,
639
+ inheritedTooling
549
640
  );
550
- p.note(formatConfigSummary(defaultOptions), "Template Configuration");
551
- const action = await p.select({
641
+ const configTitle = inheritedTooling ? "Template Configuration (using workspace tooling)" : "Template Configuration";
642
+ p.note(formatConfigSummary(defaultOptions), configTitle);
643
+ const proceed = await p.confirm({
552
644
  message: "Proceed with these settings?",
553
- options: [
554
- { value: "confirm", label: "Yes, create project" },
555
- { value: "customize", label: "No, let me customize" }
556
- ],
557
- initialValue: "confirm"
645
+ initialValue: true
558
646
  });
559
- if (p.isCancel(action)) {
647
+ if (p.isCancel(proceed)) {
560
648
  p.cancel("Operation cancelled.");
561
649
  process.exit(0);
562
650
  }
563
- if (action === "confirm") {
651
+ if (proceed) {
564
652
  return defaultOptions;
565
653
  }
566
- return promptForCustomization(
567
- template,
568
- projectName,
569
- projectType
570
- );
654
+ return promptForCustomization(template, projectName, projectType, integrations, inheritedTooling);
571
655
  }
572
656
 
573
- const config = new Conf({
574
- projectName: "create-krispya"
575
- });
576
- function getPreferredEditor() {
577
- return config.get("preferredEditor");
578
- }
579
- function setPreferredEditor(editor) {
580
- config.set("preferredEditor", editor);
581
- }
582
- function getReuseWindow() {
583
- return config.get("reuseWindow") ?? false;
584
- }
585
- function setReuseWindow(reuse) {
586
- config.set("reuseWindow", reuse);
657
+ async function checkAnyExists(paths) {
658
+ for (const path of paths) {
659
+ try {
660
+ await access(path, constants.F_OK);
661
+ return true;
662
+ } catch {
663
+ }
664
+ }
665
+ return false;
587
666
  }
588
- function clearConfig() {
589
- config.clear();
667
+ async function validateWorkspace(monorepoRoot) {
668
+ const errors = [];
669
+ const tsConfigPath = join(monorepoRoot, ".config/typescript/package.json");
670
+ try {
671
+ await access(tsConfigPath, constants.F_OK);
672
+ } catch {
673
+ errors.push("Missing .config/typescript package");
674
+ }
675
+ const linterPaths = [
676
+ join(monorepoRoot, ".config/oxlint/package.json"),
677
+ join(monorepoRoot, ".config/eslint/package.json"),
678
+ join(monorepoRoot, "eslint.config.js"),
679
+ join(monorepoRoot, "biome.json")
680
+ ];
681
+ const hasLinter = await checkAnyExists(linterPaths);
682
+ if (!hasLinter) {
683
+ errors.push(
684
+ "Missing linter config (.config/oxlint, .config/eslint, eslint.config.js, or biome.json)"
685
+ );
686
+ }
687
+ const formatterPaths = [
688
+ join(monorepoRoot, ".config/oxfmt/package.json"),
689
+ join(monorepoRoot, ".config/prettier/package.json"),
690
+ join(monorepoRoot, ".prettierrc.json"),
691
+ join(monorepoRoot, "biome.json")
692
+ ];
693
+ const hasFormatter = await checkAnyExists(formatterPaths);
694
+ if (!hasFormatter) {
695
+ errors.push(
696
+ "Missing formatter config (.config/oxfmt, .config/prettier, .prettierrc.json, or biome.json)"
697
+ );
698
+ }
699
+ return { valid: errors.length === 0, errors };
590
700
  }
591
701
 
592
702
  const require$1 = createRequire(import.meta.url);
593
703
  const pkg = require$1("../package.json");
704
+ async function fileExists(path) {
705
+ try {
706
+ await access(path, constants$1.F_OK);
707
+ return true;
708
+ } catch {
709
+ return false;
710
+ }
711
+ }
712
+ async function writeGeneratedFiles(basePath, files) {
713
+ const filePaths = Object.keys(files).sort();
714
+ for (const filePath of filePaths) {
715
+ const fullFilePath = join(basePath, filePath);
716
+ await mkdir(dirname(fullFilePath), { recursive: true });
717
+ const file = files[filePath];
718
+ if (file.type === "text") {
719
+ await writeFile(fullFilePath, file.content);
720
+ } else {
721
+ const response = await fetch(file.url);
722
+ await writeFile(fullFilePath, response.body);
723
+ }
724
+ }
725
+ }
726
+ function calculateWorkspaceRoot(packagePath) {
727
+ const segments = packagePath.split(/[/\\]/).filter(Boolean);
728
+ return segments.map(() => "..").join("/");
729
+ }
594
730
  async function detectMonorepoRoot() {
595
731
  let currentDir = cwd();
596
732
  const root = resolve("/");
597
733
  while (currentDir !== root) {
598
734
  const workspaceFile = join(currentDir, "pnpm-workspace.yaml");
599
735
  try {
600
- await access(workspaceFile, constants.F_OK);
736
+ await access(workspaceFile, constants$1.F_OK);
601
737
  const content = await readFile(workspaceFile, "utf-8");
602
738
  if (content.includes("packages:")) {
603
739
  return currentDir;
@@ -608,473 +744,1280 @@ async function detectMonorepoRoot() {
608
744
  }
609
745
  return null;
610
746
  }
611
- async function main() {
612
- 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(
613
- "--bundler <bundler>",
614
- "library bundler: unbuild or tsdown (default: unbuild, only for libraries)"
615
- ).option(
616
- "--template <type>",
617
- "project template: vanilla, vanilla-js, react, react-js, r3f, r3f-js (default: vanilla)"
618
- ).option("--linter <type>", "linter: eslint, oxlint, or biome (default: oxlint)").option("--formatter <type>", "formatter: prettier, oxfmt, or biome (default: oxfmt)").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(
619
- "--pnpm-manage-versions",
620
- "enable manage-package-manager-versions in pnpm-workspace.yaml (default: true)"
621
- ).option(
622
- "--no-pnpm-manage-versions",
623
- "disable manage-package-manager-versions in pnpm-workspace.yaml"
624
- ).option(
625
- "--node-version <version>",
626
- 'set Node.js version for engines.node field (default: "latest")'
627
- ).option("-y, --yes", "Skip prompts and use default values").option("--clear-config", "Clear saved preferences (e.g. editor choice)").action(async (name, options) => {
628
- if (options.clearConfig) {
629
- clearConfig();
630
- console.log("Configuration cleared.");
631
- process.exit(0);
747
+ async function parseWorkspaceDirectories(monorepoRoot) {
748
+ try {
749
+ const workspaceFile = join(monorepoRoot, "pnpm-workspace.yaml");
750
+ const content = await readFile(workspaceFile, "utf-8");
751
+ return parseWorkspaceYamlContent(content);
752
+ } catch {
753
+ return [];
754
+ }
755
+ }
756
+ async function detectWorkspaceTooling(monorepoRoot) {
757
+ try {
758
+ const pkgPath = join(monorepoRoot, "package.json");
759
+ const content = await readFile(pkgPath, "utf-8");
760
+ const pkgJson = JSON.parse(content);
761
+ const devDeps = pkgJson.devDependencies ?? {};
762
+ const linter = devDeps.oxlint ? "oxlint" : devDeps.eslint ? "eslint" : devDeps["@biomejs/biome"] ? "biome" : void 0;
763
+ const formatter = devDeps.oxfmt ? "oxfmt" : devDeps.prettier ? "prettier" : devDeps["@biomejs/biome"] ? "biome" : void 0;
764
+ return { linter, formatter };
765
+ } catch {
766
+ return {};
767
+ }
768
+ }
769
+ async function detectExistingConfigs(monorepoRoot) {
770
+ const configs = {};
771
+ const eslintPath = join(monorepoRoot, "eslint.config.js");
772
+ if (await fileExists(eslintPath)) {
773
+ configs.linter = "eslint";
774
+ configs.eslintConfigPath = eslintPath;
775
+ }
776
+ const prettierPath = join(monorepoRoot, ".prettierrc.json");
777
+ if (await fileExists(prettierPath)) {
778
+ configs.formatter = "prettier";
779
+ configs.prettierConfigPath = prettierPath;
780
+ }
781
+ const biomePath = join(monorepoRoot, "biome.json");
782
+ if (await fileExists(biomePath)) {
783
+ configs.biomeConfigPath = biomePath;
784
+ if (!configs.linter) configs.linter = "biome";
785
+ if (!configs.formatter) configs.formatter = "biome";
786
+ }
787
+ return configs;
788
+ }
789
+ async function getMonorepoScope(monorepoRoot) {
790
+ try {
791
+ const pkgPath = join(monorepoRoot, "package.json");
792
+ const content = await readFile(pkgPath, "utf-8");
793
+ const pkgJson = JSON.parse(content);
794
+ if (pkgJson.name) {
795
+ return pkgJson.name.replace(/^@/, "").replace(/\/.*$/, "");
632
796
  }
633
- console.clear();
634
- p.intro(color.bgCyan(color.black(` create-krispya v${pkg.version} `)));
635
- const monorepoRoot = await detectMonorepoRoot();
636
- if (monorepoRoot && Object.keys(options).length === 0) {
637
- const choice = await p.select({
638
- message: "Detected monorepo workspace",
639
- options: [
640
- { value: "add", label: "Add new package to this workspace" },
641
- { value: "standalone", label: "Create standalone project" }
642
- ],
643
- initialValue: "add"
644
- });
645
- if (p.isCancel(choice)) {
646
- p.cancel("Operation cancelled.");
647
- process.exit(0);
648
- }
649
- if (choice === "add") {
650
- const packageType = await promptForInitialPackage();
651
- if (packageType === "skip") {
652
- p.cancel("Operation cancelled.");
653
- process.exit(0);
654
- }
655
- const packageName = await p.text({
656
- message: "Package name?",
657
- placeholder: packageType === "app" ? "my-app" : "my-package",
658
- validate: (value) => {
659
- if (!value.length) return "Package name is required";
660
- }
661
- });
662
- if (p.isCancel(packageName)) {
663
- p.cancel("Operation cancelled.");
664
- process.exit(0);
665
- }
666
- const targetDir = packageType === "app" ? "apps" : "packages";
667
- const packagePath = join(targetDir, packageName);
668
- const workspaceRoot = "../..";
669
- const packageOptions = await promptForPackageOptions(packageName, packageType);
670
- packageOptions.workspaceRoot = workspaceRoot;
671
- packageOptions.name = packageName;
672
- const packageManager2 = packageOptions.packageManager || "pnpm";
673
- if (packageManager2 === "pnpm") {
674
- packageOptions.pnpmVersion = await getLatestPnpmVersion();
675
- }
676
- const nodeVersion2 = packageOptions.nodeVersion ?? "latest";
677
- if (nodeVersion2 === "latest") {
678
- packageOptions.nodeVersion = await getLatestNodeVersion();
679
- }
680
- const versions2 = {};
681
- const versionPromises2 = [];
682
- const pkgIsLibrary = packageOptions.projectType === "library";
683
- const pkgTesting = packageOptions.testing ?? (pkgIsLibrary ? "vitest" : "none");
684
- if (pkgTesting === "vitest") {
685
- versionPromises2.push(
686
- getLatestNpmVersion("vitest", "4.0.0").then((v) => {
687
- versions2.vitest = v;
688
- })
689
- );
690
- }
691
- if (!pkgIsLibrary) {
692
- versionPromises2.push(
693
- getLatestNpmVersion("vite", "6.3.4").then((v) => {
694
- versions2.vite = v;
695
- })
696
- );
697
- }
698
- const linter2 = packageOptions.linter ?? "oxlint";
699
- if (linter2 === "eslint") {
700
- versionPromises2.push(
701
- getLatestNpmVersion("eslint", "9.17.0").then((v) => {
702
- versions2.eslint = v;
703
- })
704
- );
705
- } else if (linter2 === "oxlint") {
706
- versionPromises2.push(
707
- getLatestNpmVersion("oxlint", "0.16.0").then((v) => {
708
- versions2.oxlint = v;
709
- })
710
- );
711
- } else if (linter2 === "biome") {
712
- versionPromises2.push(
713
- getLatestNpmVersion("@biomejs/biome", "1.9.4").then((v) => {
714
- versions2.biome = v;
715
- })
716
- );
717
- }
718
- const formatter2 = packageOptions.formatter ?? "oxfmt";
719
- if (formatter2 === "prettier") {
720
- versionPromises2.push(
721
- getLatestNpmVersion("prettier", "3.4.2").then((v) => {
722
- versions2.prettier = v;
723
- })
724
- );
725
- } else if (formatter2 === "oxfmt") {
726
- versionPromises2.push(
727
- getLatestNpmVersion("oxfmt", "0.1.0").then((v) => {
728
- versions2.oxfmt = v;
729
- })
730
- );
731
- } else if (formatter2 === "biome" && linter2 !== "biome") {
732
- versionPromises2.push(
733
- getLatestNpmVersion("@biomejs/biome", "1.9.4").then((v) => {
734
- versions2.biome = v;
735
- })
736
- );
737
- }
738
- await Promise.all(versionPromises2);
739
- packageOptions.versions = versions2;
740
- const basePath2 = join(monorepoRoot, packagePath);
741
- const s2 = p.spinner();
742
- s2.start("Creating package...");
797
+ } catch {
798
+ }
799
+ return monorepoRoot.split(/[/\\]/).pop() ?? "workspace";
800
+ }
801
+ async function getWorkspacePackages(monorepoRoot) {
802
+ const packagesDir = join(monorepoRoot, "packages");
803
+ const packages = [];
804
+ try {
805
+ const { readdir } = await import('fs/promises');
806
+ const entries = await readdir(packagesDir, { withFileTypes: true });
807
+ for (const entry of entries) {
808
+ if (entry.isDirectory()) {
743
809
  try {
744
- const files = generate(packageOptions);
745
- const filePaths = Object.keys(files).sort();
746
- for (const filePath of filePaths) {
747
- const fullFilePath = join(basePath2, filePath);
748
- await mkdir(dirname(fullFilePath), { recursive: true });
749
- const file = files[filePath];
750
- if (file.type === "text") {
751
- await writeFile(fullFilePath, file.content);
752
- } else {
753
- const response = await fetch(file.url);
754
- await writeFile(fullFilePath, response.body);
755
- }
810
+ const pkgJsonPath = join(packagesDir, entry.name, "package.json");
811
+ const content = await readFile(pkgJsonPath, "utf-8");
812
+ const pkgJson = JSON.parse(content);
813
+ if (pkgJson.name) {
814
+ packages.push({
815
+ name: pkgJson.name,
816
+ path: `packages/${entry.name}`
817
+ });
756
818
  }
757
- s2.stop("Package created!");
758
- const isLibrary2 = packageOptions.projectType === "library";
759
- const nextSteps = isLibrary2 ? [
760
- `cd ${packagePath}`,
761
- `${packageManager2} install`,
762
- `${packageManager2} run build`
763
- ].join("\n") : [
764
- `cd ${packagePath}`,
765
- `${packageManager2} install`,
766
- `${packageManager2} run dev`
767
- ].join("\n");
768
- p.note(nextSteps, "Next steps");
769
- p.outro(color.green("Happy coding! \u2728"));
770
- process.exit(0);
771
- } catch (error) {
772
- s2.stop("Failed to create package");
773
- p.log.error(String(error));
774
- process.exit(1);
819
+ } catch {
775
820
  }
776
821
  }
777
822
  }
778
- let generateOptions;
779
- if (Object.keys(options).length > 0) {
780
- const template = options.template ?? "vanilla";
781
- const baseTemplate = getBaseTemplate(template);
782
- const defaultName = getDefaultProjectName(template);
783
- const projectType = options.type ?? "app";
784
- generateOptions = {
785
- name: name || defaultName,
786
- projectType,
787
- libraryBundler: projectType === "library" ? options.bundler ?? "unbuild" : void 0,
788
- template,
789
- linter: options.linter ?? "oxlint",
790
- formatter: options.formatter ?? "oxfmt",
791
- ...baseTemplate === "r3f" && {
792
- drei: options.drei ? {} : void 0,
793
- handle: options.handle ? {} : void 0,
794
- leva: options.leva ? {} : void 0,
795
- postprocessing: options.postprocessing ? {} : void 0,
796
- rapier: options.rapier ? {} : void 0,
797
- xr: options.xr ? {} : void 0,
798
- uikit: options.uikit ? {} : void 0,
799
- offscreen: options.offscreen ? {} : void 0,
800
- zustand: options.zustand ? {} : void 0,
801
- koota: options.koota ? {} : void 0,
802
- viverse: options.viverse ? {} : void 0,
803
- triplex: options.triplex ? {} : void 0
804
- },
805
- packageManager: options.packageManager,
806
- pnpmManageVersions: options.pnpmManageVersions,
807
- nodeVersion: options.nodeVersion ?? "latest"
808
- };
809
- } else {
810
- generateOptions = await promptForOptions(name);
811
- }
812
- if (generateOptions.projectType === "monorepo") {
813
- const { generateMonorepo } = await import('./chunks/index.mjs').then(function (n) { return n.m; });
814
- const packageManager2 = generateOptions.packageManager || "pnpm";
815
- if (packageManager2 === "pnpm") {
816
- generateOptions.pnpmVersion = await getLatestPnpmVersion();
823
+ } catch {
824
+ }
825
+ return packages;
826
+ }
827
+ async function ensureConfigInWorkspace(monorepoRoot) {
828
+ const workspacePath = join(monorepoRoot, "pnpm-workspace.yaml");
829
+ let content;
830
+ try {
831
+ content = await readFile(workspacePath, "utf-8");
832
+ } catch {
833
+ content = `packages:
834
+ - ".config/*"
835
+ - "packages/*"
836
+ `;
837
+ await writeFile(workspacePath, content);
838
+ return;
839
+ }
840
+ if (content.includes(".config/*") || content.includes('".config/*"')) {
841
+ return;
842
+ }
843
+ const lines = content.split("\n");
844
+ const packagesIndex = lines.findIndex(
845
+ (line) => line.trim().startsWith("packages:")
846
+ );
847
+ if (packagesIndex === -1) {
848
+ content = `packages:
849
+ - ".config/*"
850
+ ${content}`;
851
+ } else {
852
+ lines.splice(packagesIndex + 1, 0, ' - ".config/*"');
853
+ content = lines.join("\n");
854
+ }
855
+ await writeFile(workspacePath, content);
856
+ }
857
+ async function migrateEslintConfig(monorepoRoot, files) {
858
+ const configBasePath = ".config/eslint";
859
+ const existingConfigPath = join(monorepoRoot, "eslint.config.js");
860
+ let existingContent;
861
+ try {
862
+ existingContent = await readFile(existingConfigPath, "utf-8");
863
+ } catch {
864
+ generateEslintConfigPackage(files);
865
+ return;
866
+ }
867
+ files[`${configBasePath}/package.json`] = {
868
+ type: "text",
869
+ content: JSON.stringify(
870
+ {
871
+ name: "@config/eslint",
872
+ version: "0.1.0",
873
+ private: true,
874
+ type: "module",
875
+ exports: {
876
+ "./base": "./base.js",
877
+ "./react": "./react.js"
878
+ }
879
+ },
880
+ null,
881
+ 2
882
+ )
883
+ };
884
+ files[`${configBasePath}/README.md`] = {
885
+ type: "text",
886
+ content: `# \`@config/eslint\`
887
+
888
+ Shared ESLint configurations.
889
+
890
+ ## Usage
891
+
892
+ In your package's \`eslint.config.js\`:
893
+
894
+ \`\`\`js
895
+ import base from "@config/eslint/base";
896
+
897
+ export default [...base];
898
+ \`\`\`
899
+
900
+ ## Available Configs
901
+
902
+ - \`base\` - Base ESLint rules (migrated from root)
903
+ - \`react\` - React-specific rules
904
+ `
905
+ };
906
+ files[`${configBasePath}/base.js`] = {
907
+ type: "text",
908
+ content: existingContent
909
+ };
910
+ files[`${configBasePath}/react.js`] = {
911
+ type: "text",
912
+ content: `import react from "eslint-plugin-react";
913
+ import reactHooks from "eslint-plugin-react-hooks";
914
+
915
+ export default [
916
+ {
917
+ plugins: {
918
+ react,
919
+ "react-hooks": reactHooks,
920
+ },
921
+ rules: {
922
+ ...react.configs.recommended.rules,
923
+ ...reactHooks.configs.recommended.rules,
924
+ "react/react-in-jsx-scope": "off",
925
+ },
926
+ settings: {
927
+ react: {
928
+ version: "detect",
929
+ },
930
+ },
931
+ },
932
+ ];
933
+ `
934
+ };
935
+ }
936
+ async function migratePrettierConfig(monorepoRoot, files) {
937
+ const configBasePath = ".config/prettier";
938
+ const existingConfigPath = join(monorepoRoot, ".prettierrc.json");
939
+ let existingContent;
940
+ try {
941
+ existingContent = await readFile(existingConfigPath, "utf-8");
942
+ } catch {
943
+ generatePrettierConfigPackage(files);
944
+ return;
945
+ }
946
+ files[`${configBasePath}/package.json`] = {
947
+ type: "text",
948
+ content: JSON.stringify(
949
+ {
950
+ name: "@config/prettier",
951
+ version: "0.1.0",
952
+ private: true,
953
+ exports: {
954
+ "./base": "./base.json"
955
+ }
956
+ },
957
+ null,
958
+ 2
959
+ )
960
+ };
961
+ files[`${configBasePath}/README.md`] = {
962
+ type: "text",
963
+ content: `# \`@config/prettier\`
964
+
965
+ Shared Prettier configurations.
966
+
967
+ ## Usage
968
+
969
+ In your package's \`.prettierrc\`:
970
+
971
+ \`\`\`json
972
+ "@config/prettier/base"
973
+ \`\`\`
974
+
975
+ Or in \`package.json\`:
976
+
977
+ \`\`\`json
978
+ {
979
+ "prettier": "@config/prettier/base"
980
+ }
981
+ \`\`\`
982
+
983
+ ## Available Configs
984
+
985
+ - \`base\` - Base Prettier rules (migrated from root)
986
+ `
987
+ };
988
+ files[`${configBasePath}/base.json`] = {
989
+ type: "text",
990
+ content: existingContent
991
+ };
992
+ }
993
+ async function createPackageInWorkspace(monorepoRoot, packageManager, inheritedTooling, scope) {
994
+ const workspaceDirectories = await parseWorkspaceDirectories(monorepoRoot);
995
+ const defaultDirectories = ["apps", "packages"];
996
+ const hasCustomDirectories = workspaceDirectories.length > 0 && !workspaceDirectories.every((dir) => defaultDirectories.includes(dir));
997
+ const packageType = await promptForInitialPackage();
998
+ if (packageType === "skip") {
999
+ return false;
1000
+ }
1001
+ const defaultDir = packageType === "app" ? "apps" : "packages";
1002
+ const packageNameInput = await p.text({
1003
+ message: "Package name?",
1004
+ initialValue: `@${scope}/`,
1005
+ validate: (value) => {
1006
+ const validationError = validatePackageName(value);
1007
+ if (validationError) return validationError;
1008
+ const dirName = value.includes("/") ? value.split("/").pop() : value;
1009
+ if (!dirName) return "Package name is required";
1010
+ if (!hasCustomDirectories) {
1011
+ const targetPath = join(monorepoRoot, defaultDir, dirName);
1012
+ try {
1013
+ const { statSync } = require$1("fs");
1014
+ statSync(targetPath);
1015
+ return `Directory ${defaultDir}/${dirName} already exists`;
1016
+ } catch {
1017
+ }
817
1018
  }
818
- const nodeVersion2 = generateOptions.nodeVersion ?? "latest";
819
- if (nodeVersion2 === "latest") {
820
- generateOptions.nodeVersion = await getLatestNodeVersion();
1019
+ }
1020
+ });
1021
+ if (p.isCancel(packageNameInput)) {
1022
+ return false;
1023
+ }
1024
+ const scopedName = packageNameInput;
1025
+ const shortName = scopedName.includes("/") ? scopedName.split("/").pop() : scopedName;
1026
+ const packageOptions = await promptForPackageOptions(
1027
+ scopedName,
1028
+ packageType,
1029
+ inheritedTooling
1030
+ );
1031
+ let targetDir = defaultDir;
1032
+ if (hasCustomDirectories && workspaceDirectories.length > 0) {
1033
+ const dirChoice = await p.select({
1034
+ message: "Target directory",
1035
+ options: workspaceDirectories.map((dir) => ({
1036
+ value: dir,
1037
+ label: dir
1038
+ })),
1039
+ initialValue: workspaceDirectories.includes(defaultDir) ? defaultDir : workspaceDirectories[0]
1040
+ });
1041
+ if (p.isCancel(dirChoice)) {
1042
+ return false;
1043
+ }
1044
+ targetDir = dirChoice;
1045
+ const targetPath = join(monorepoRoot, targetDir, shortName);
1046
+ try {
1047
+ const { statSync } = require$1("fs");
1048
+ statSync(targetPath);
1049
+ p.log.error(`Directory ${targetDir}/${shortName} already exists`);
1050
+ return false;
1051
+ } catch {
1052
+ }
1053
+ }
1054
+ const relativePkgPath = join(targetDir, shortName);
1055
+ const workspaceRoot = calculateWorkspaceRoot(relativePkgPath);
1056
+ packageOptions.workspaceRoot = workspaceRoot;
1057
+ packageOptions.name = scopedName;
1058
+ if (packageManager === "pnpm") {
1059
+ packageOptions.pnpmVersion = await getLatestPnpmVersion();
1060
+ } else if (packageManager === "yarn") {
1061
+ packageOptions.yarnVersion = await getLatestYarnVersion();
1062
+ } else if (packageManager === "npm") {
1063
+ packageOptions.npmVersion = await getLatestNpmCliVersion();
1064
+ }
1065
+ const nodeVersion = packageOptions.nodeVersion ?? "latest";
1066
+ if (nodeVersion === "latest") {
1067
+ packageOptions.nodeVersion = await getLatestNodeVersion();
1068
+ }
1069
+ const versions = {};
1070
+ const versionPromises = [];
1071
+ const pkgIsLibrary = packageOptions.projectType === "library";
1072
+ const pkgTesting = packageOptions.testing ?? (pkgIsLibrary ? "vitest" : "none");
1073
+ if (pkgTesting === "vitest") {
1074
+ versionPromises.push(
1075
+ getLatestNpmVersion("vitest", "4.0.0").then((v) => {
1076
+ versions.vitest = v;
1077
+ })
1078
+ );
1079
+ }
1080
+ if (!pkgIsLibrary) {
1081
+ versionPromises.push(
1082
+ getLatestNpmVersion("vite", "6.3.4").then((v) => {
1083
+ versions.vite = v;
1084
+ })
1085
+ );
1086
+ }
1087
+ const linter = packageOptions.linter ?? "oxlint";
1088
+ if (linter === "eslint") {
1089
+ versionPromises.push(
1090
+ getLatestNpmVersion("eslint", "9.17.0").then((v) => {
1091
+ versions.eslint = v;
1092
+ })
1093
+ );
1094
+ } else if (linter === "oxlint") {
1095
+ versionPromises.push(
1096
+ getLatestNpmVersion("oxlint", "0.16.0").then((v) => {
1097
+ versions.oxlint = v;
1098
+ })
1099
+ );
1100
+ } else if (linter === "biome") {
1101
+ versionPromises.push(
1102
+ getLatestNpmVersion("@biomejs/biome", "1.9.4").then((v) => {
1103
+ versions.biome = v;
1104
+ })
1105
+ );
1106
+ }
1107
+ const formatter = packageOptions.formatter ?? "oxfmt";
1108
+ if (formatter === "prettier") {
1109
+ versionPromises.push(
1110
+ getLatestNpmVersion("prettier", "3.4.2").then((v) => {
1111
+ versions.prettier = v;
1112
+ })
1113
+ );
1114
+ } else if (formatter === "oxfmt") {
1115
+ versionPromises.push(
1116
+ getLatestNpmVersion("oxfmt", "0.1.0").then((v) => {
1117
+ versions.oxfmt = v;
1118
+ })
1119
+ );
1120
+ } else if (formatter === "biome" && linter !== "biome") {
1121
+ versionPromises.push(
1122
+ getLatestNpmVersion("@biomejs/biome", "1.9.4").then((v) => {
1123
+ versions.biome = v;
1124
+ })
1125
+ );
1126
+ }
1127
+ await Promise.all(versionPromises);
1128
+ packageOptions.versions = versions;
1129
+ if (packageType === "app") {
1130
+ const workspacePackages = await getWorkspacePackages(monorepoRoot);
1131
+ if (workspacePackages.length > 0) {
1132
+ const selectedDeps = await p.multiselect({
1133
+ message: "Add workspace dependencies?",
1134
+ options: workspacePackages.map((pkgInfo) => ({
1135
+ value: pkgInfo.name,
1136
+ label: pkgInfo.name.replace(/^@[^/]+\//, "")
1137
+ })),
1138
+ required: false
1139
+ });
1140
+ if (!p.isCancel(selectedDeps) && selectedDeps.length > 0) {
1141
+ packageOptions.workspaceDependencies = selectedDeps;
821
1142
  }
822
- const basePath2 = join(cwd(), generateOptions.name);
823
- const s2 = p.spinner();
824
- s2.start("Creating monorepo workspace...");
825
- try {
826
- const { files } = generateMonorepo({
827
- name: generateOptions.name,
828
- linter: generateOptions.linter ?? "oxlint",
829
- formatter: generateOptions.formatter ?? "oxfmt",
830
- packageManager: packageManager2,
831
- pnpmVersion: generateOptions.pnpmVersion,
832
- pnpmManageVersions: generateOptions.pnpmManageVersions,
833
- nodeVersion: generateOptions.nodeVersion
834
- });
835
- const filePaths = Object.keys(files).sort();
836
- for (const filePath of filePaths) {
837
- const fullFilePath = join(basePath2, filePath);
838
- await mkdir(dirname(fullFilePath), { recursive: true });
839
- const file = files[filePath];
840
- if (file.type === "text") {
841
- await writeFile(fullFilePath, file.content);
842
- }
843
- }
844
- s2.stop("Monorepo workspace created!");
845
- const initialPackage = await promptForInitialPackage();
846
- if (initialPackage !== "skip") {
847
- const packageName = await p.text({
848
- message: "Package name?",
849
- placeholder: initialPackage === "app" ? "my-app" : "my-package",
850
- validate: (value) => {
851
- if (!value.length) return "Package name is required";
852
- }
1143
+ }
1144
+ }
1145
+ const outputPath = join(monorepoRoot, relativePkgPath);
1146
+ const spinner = p.spinner();
1147
+ spinner.start("Creating package...");
1148
+ try {
1149
+ const files = generate(packageOptions);
1150
+ await writeGeneratedFiles(outputPath, files);
1151
+ spinner.stop(
1152
+ color.green.inverse(` \u2713 Package created at ${relativePkgPath}! `)
1153
+ );
1154
+ const addAnother = await p.select({
1155
+ message: "Add another package?",
1156
+ options: [
1157
+ { value: "no", label: "No, I'm done" },
1158
+ { value: "yes", label: "Yes, add another" }
1159
+ ],
1160
+ initialValue: "no"
1161
+ });
1162
+ return !p.isCancel(addAnother) && addAnother === "yes";
1163
+ } catch (error) {
1164
+ spinner.stop("Failed to create package");
1165
+ p.log.error(String(error));
1166
+ return false;
1167
+ }
1168
+ }
1169
+ async function promptAndOpenEditor(projectPath) {
1170
+ const savedEditor = getPreferredEditor();
1171
+ let selectedEditor;
1172
+ if (savedEditor && savedEditor !== "skip") {
1173
+ const useDefault = await p.confirm({
1174
+ message: `Open in editor? ${color.dim(`(${editorNames[savedEditor]})`)}`,
1175
+ initialValue: true
1176
+ });
1177
+ if (p.isCancel(useDefault)) {
1178
+ selectedEditor = void 0;
1179
+ } else if (useDefault) {
1180
+ selectedEditor = savedEditor;
1181
+ } else {
1182
+ selectedEditor = "skip";
1183
+ }
1184
+ } else {
1185
+ const openEditor = await p.select({
1186
+ message: "Open project in editor?",
1187
+ options: [
1188
+ { value: "skip", label: "Skip" },
1189
+ { value: "cursor", label: "Cursor" },
1190
+ { value: "code", label: "VS Code" },
1191
+ { value: "webstorm", label: "WebStorm" }
1192
+ ],
1193
+ initialValue: "skip"
1194
+ });
1195
+ if (!p.isCancel(openEditor)) {
1196
+ selectedEditor = openEditor;
1197
+ const saveChoice = await p.confirm({
1198
+ message: `Save ${editorNames[selectedEditor] ?? "Skip"} as default editor?`,
1199
+ initialValue: true
1200
+ });
1201
+ if (!p.isCancel(saveChoice) && saveChoice) {
1202
+ setPreferredEditor(selectedEditor);
1203
+ if (selectedEditor === "cursor" || selectedEditor === "code") {
1204
+ const reuseChoice = await p.confirm({
1205
+ message: "Reuse current window when opening projects?",
1206
+ initialValue: false
853
1207
  });
854
- if (!p.isCancel(packageName)) {
855
- const targetDir = initialPackage === "app" ? "apps" : "packages";
856
- const packagePath = join(targetDir, packageName);
857
- const packageOptions = await promptForPackageOptions(packageName, initialPackage);
858
- packageOptions.workspaceRoot = "../..";
859
- packageOptions.name = packageName;
860
- const pkgManager = packageOptions.packageManager || "pnpm";
861
- const versions2 = {};
862
- const versionPromises2 = [];
863
- const initPkgIsLibrary = packageOptions.projectType === "library";
864
- const initPkgTesting = packageOptions.testing ?? (initPkgIsLibrary ? "vitest" : "none");
865
- if (initPkgTesting === "vitest") {
866
- versionPromises2.push(
867
- getLatestNpmVersion("vitest", "4.0.0").then((v) => {
868
- versions2.vitest = v;
869
- })
870
- );
871
- }
872
- if (!initPkgIsLibrary) {
873
- versionPromises2.push(
874
- getLatestNpmVersion("vite", "6.3.4").then((v) => {
875
- versions2.vite = v;
876
- })
877
- );
878
- }
879
- await Promise.all(versionPromises2);
880
- packageOptions.versions = versions2;
881
- s2.start("Creating initial package...");
882
- const packageFiles = generate(packageOptions);
883
- const packageFilePaths = Object.keys(packageFiles).sort();
884
- const packageBasePath = join(basePath2, packagePath);
885
- for (const filePath of packageFilePaths) {
886
- const fullFilePath = join(packageBasePath, filePath);
887
- await mkdir(dirname(fullFilePath), { recursive: true });
888
- const file = packageFiles[filePath];
889
- if (file.type === "text") {
890
- await writeFile(fullFilePath, file.content);
891
- } else {
892
- const response = await fetch(file.url);
893
- await writeFile(fullFilePath, response.body);
894
- }
895
- }
896
- s2.stop("Initial package created!");
1208
+ if (!p.isCancel(reuseChoice)) {
1209
+ setReuseWindow(reuseChoice);
897
1210
  }
898
1211
  }
899
- const nextSteps = [
900
- `cd ${generateOptions.name}`,
901
- `${packageManager2} install`,
902
- `${packageManager2} run dev`
903
- ].join("\n");
904
- p.note(nextSteps, "Next steps");
905
- p.outro(color.green("Happy coding! \u2728"));
906
- process.exit(0);
907
- } catch (error) {
908
- s2.stop("Failed to create monorepo workspace");
909
- p.log.error(String(error));
910
- process.exit(1);
911
1212
  }
912
1213
  }
913
- const base = generateOptions.template ? getBaseTemplate(generateOptions.template) : "vanilla";
914
- const defaultFallbackName = base === "vanilla" ? "vanilla-app" : base === "react" ? "react-app" : "react-three-app";
915
- generateOptions.name ??= defaultFallbackName;
916
- const packageManager = generateOptions.packageManager || "pnpm";
917
- if (packageManager === "pnpm") {
918
- generateOptions.pnpmVersion = await getLatestPnpmVersion();
1214
+ }
1215
+ if (selectedEditor && selectedEditor !== "skip") {
1216
+ try {
1217
+ await openInEditor(
1218
+ selectedEditor,
1219
+ projectPath,
1220
+ getReuseWindow()
1221
+ );
1222
+ p.log.success(`Opening in ${editorNames[selectedEditor]}...`);
1223
+ } catch {
1224
+ p.log.warn(
1225
+ `Could not open ${editorNames[selectedEditor]}. Make sure the CLI command is in your PATH.`
1226
+ );
919
1227
  }
920
- const nodeVersion = generateOptions.nodeVersion ?? "latest";
921
- if (nodeVersion === "latest") {
922
- generateOptions.nodeVersion = await getLatestNodeVersion();
1228
+ }
1229
+ }
1230
+ async function handleCheckCommand() {
1231
+ const monorepoRoot = await detectMonorepoRoot();
1232
+ if (!monorepoRoot) {
1233
+ console.log(color.red("\u2717") + " Not a monorepo workspace");
1234
+ process.exit(1);
1235
+ }
1236
+ const { valid, errors } = await validateWorkspace(monorepoRoot);
1237
+ if (valid) {
1238
+ console.log(color.green("\u2713") + " Valid monorepo workspace");
1239
+ console.log(color.dim(` ${monorepoRoot}`));
1240
+ } else {
1241
+ console.log(color.red("\u2717") + " Invalid monorepo workspace");
1242
+ console.log(color.dim(` ${monorepoRoot}`));
1243
+ for (const error of errors) {
1244
+ console.log(color.red(` \u2022 ${error}`));
923
1245
  }
924
- const versions = {};
925
- const versionPromises = [];
926
- const isLibrary = generateOptions.projectType === "library";
927
- const testing = generateOptions.testing ?? (isLibrary ? "vitest" : "none");
928
- if (testing === "vitest") {
929
- versionPromises.push(
930
- getLatestNpmVersion("vitest", "4.0.0").then((v) => {
931
- versions.vitest = v;
932
- })
933
- );
1246
+ }
1247
+ process.exit(valid ? 0 : 1);
1248
+ }
1249
+ async function handleFixCommand(options) {
1250
+ const monorepoRoot = await detectMonorepoRoot();
1251
+ if (!monorepoRoot) {
1252
+ console.log(color.red("\u2717") + " Not a monorepo workspace");
1253
+ console.log(color.dim(" Run this command from within a monorepo"));
1254
+ process.exit(1);
1255
+ }
1256
+ const { valid, errors } = await validateWorkspace(monorepoRoot);
1257
+ if (valid) {
1258
+ console.log(color.green("\u2713") + " Workspace is already valid");
1259
+ console.log(color.dim(` ${monorepoRoot}`));
1260
+ process.exit(0);
1261
+ }
1262
+ console.log(color.yellow("!") + " Invalid monorepo workspace");
1263
+ for (const error of errors) {
1264
+ console.log(color.dim(` \u2022 ${error}`));
1265
+ }
1266
+ console.log();
1267
+ const tooling = await detectWorkspaceTooling(monorepoRoot);
1268
+ const existingConfigs = await detectExistingConfigs(monorepoRoot);
1269
+ const detectedLinter = tooling.linter ?? existingConfigs.linter ?? "oxlint";
1270
+ const detectedFormatter = tooling.formatter ?? existingConfigs.formatter ?? "oxfmt";
1271
+ const isNonInteractive = options.linter && options.formatter;
1272
+ let linter;
1273
+ let formatter;
1274
+ if (isNonInteractive) {
1275
+ linter = options.linter;
1276
+ formatter = options.formatter;
1277
+ } else {
1278
+ const linterChoice = await p.select({
1279
+ message: "Linter",
1280
+ options: [
1281
+ {
1282
+ value: "oxlint",
1283
+ label: "oxlint" + (tooling.linter === "oxlint" ? color.dim(" (installed)") : "")
1284
+ },
1285
+ {
1286
+ value: "eslint",
1287
+ label: "eslint" + (tooling.linter === "eslint" || existingConfigs.linter === "eslint" ? color.dim(" (installed)") : "")
1288
+ },
1289
+ {
1290
+ value: "biome",
1291
+ label: "biome" + (tooling.linter === "biome" ? color.dim(" (installed)") : "")
1292
+ }
1293
+ ],
1294
+ initialValue: detectedLinter
1295
+ });
1296
+ if (p.isCancel(linterChoice)) {
1297
+ p.cancel("Operation cancelled.");
1298
+ process.exit(0);
934
1299
  }
935
- if (!isLibrary) {
936
- versionPromises.push(
937
- getLatestNpmVersion("vite", "6.3.4").then((v) => {
938
- versions.vite = v;
939
- })
940
- );
1300
+ const formatterChoice = await p.select({
1301
+ message: "Formatter",
1302
+ options: [
1303
+ {
1304
+ value: "oxfmt",
1305
+ label: "oxfmt" + (tooling.formatter === "oxfmt" ? color.dim(" (installed)") : "")
1306
+ },
1307
+ {
1308
+ value: "prettier",
1309
+ label: "prettier" + (tooling.formatter === "prettier" || existingConfigs.formatter === "prettier" ? color.dim(" (installed)") : "")
1310
+ },
1311
+ {
1312
+ value: "biome",
1313
+ label: "biome" + (tooling.formatter === "biome" ? color.dim(" (installed)") : "")
1314
+ }
1315
+ ],
1316
+ initialValue: detectedFormatter
1317
+ });
1318
+ if (p.isCancel(formatterChoice)) {
1319
+ p.cancel("Operation cancelled.");
1320
+ process.exit(0);
941
1321
  }
942
- const linter = generateOptions.linter ?? "oxlint";
943
- if (linter === "eslint") {
944
- versionPromises.push(
945
- getLatestNpmVersion("eslint", "9.17.0").then((v) => {
946
- versions.eslint = v;
947
- })
948
- );
949
- } else if (linter === "oxlint") {
950
- versionPromises.push(
951
- getLatestNpmVersion("oxlint", "0.16.0").then((v) => {
952
- versions.oxlint = v;
953
- })
1322
+ linter = linterChoice;
1323
+ formatter = formatterChoice;
1324
+ }
1325
+ console.log();
1326
+ const spinner = p.spinner();
1327
+ spinner.start("Fixing workspace...");
1328
+ try {
1329
+ const files = {};
1330
+ const tsConfigExists = await fileExists(
1331
+ join(monorepoRoot, ".config/typescript/package.json")
1332
+ );
1333
+ if (!tsConfigExists) {
1334
+ generateTypescriptConfigPackage(files);
1335
+ }
1336
+ if (linter === "oxlint") {
1337
+ const oxlintExists = await fileExists(
1338
+ join(monorepoRoot, ".config/oxlint/package.json")
954
1339
  );
955
- } else if (linter === "biome") {
956
- versionPromises.push(
957
- getLatestNpmVersion("@biomejs/biome", "1.9.4").then((v) => {
958
- versions.biome = v;
959
- })
1340
+ if (!oxlintExists) generateOxlintConfigPackage(files);
1341
+ } else if (linter === "eslint") {
1342
+ const eslintPkgExists = await fileExists(
1343
+ join(monorepoRoot, ".config/eslint/package.json")
960
1344
  );
1345
+ if (!eslintPkgExists) {
1346
+ if (existingConfigs.eslintConfigPath) {
1347
+ await migrateEslintConfig(monorepoRoot, files);
1348
+ } else {
1349
+ generateEslintConfigPackage(files);
1350
+ }
1351
+ }
961
1352
  }
962
- const formatter = generateOptions.formatter ?? "oxfmt";
963
- if (formatter === "prettier") {
964
- versionPromises.push(
965
- getLatestNpmVersion("prettier", "3.4.2").then((v) => {
966
- versions.prettier = v;
967
- })
968
- );
969
- } else if (formatter === "oxfmt") {
970
- versionPromises.push(
971
- getLatestNpmVersion("oxfmt", "0.1.0").then((v) => {
972
- versions.oxfmt = v;
973
- })
1353
+ if (formatter === "oxfmt") {
1354
+ const oxfmtExists = await fileExists(
1355
+ join(monorepoRoot, ".config/oxfmt/package.json")
974
1356
  );
975
- } else if (formatter === "biome" && linter !== "biome") {
976
- versionPromises.push(
977
- getLatestNpmVersion("@biomejs/biome", "1.9.4").then((v) => {
978
- versions.biome = v;
979
- })
1357
+ if (!oxfmtExists) generateOxfmtConfigPackage(files);
1358
+ } else if (formatter === "prettier") {
1359
+ const prettierPkgExists = await fileExists(
1360
+ join(monorepoRoot, ".config/prettier/package.json")
980
1361
  );
981
- }
982
- await Promise.all(versionPromises);
983
- generateOptions.versions = versions;
984
- const basePath = join(cwd(), generateOptions.name);
985
- const s = p.spinner();
986
- s.start("Creating project...");
987
- try {
988
- const files = generate(generateOptions);
989
- const filePaths = Object.keys(files).sort();
990
- for (const filePath of filePaths) {
991
- const fullFilePath = join(basePath, filePath);
992
- await mkdir(dirname(fullFilePath), { recursive: true });
993
- const file = files[filePath];
994
- if (file.type === "text") {
995
- await writeFile(fullFilePath, file.content);
1362
+ if (!prettierPkgExists) {
1363
+ if (existingConfigs.prettierConfigPath) {
1364
+ await migratePrettierConfig(monorepoRoot, files);
996
1365
  } else {
997
- const response = await fetch(file.url);
998
- await writeFile(fullFilePath, response.body);
1366
+ generatePrettierConfigPackage(files);
1367
+ }
1368
+ }
1369
+ }
1370
+ if ((linter === "biome" || formatter === "biome") && !existingConfigs.biomeConfigPath) {
1371
+ const biomeConfig = {
1372
+ $schema: "https://biomejs.dev/schemas/1.9.4/schema.json",
1373
+ vcs: {
1374
+ enabled: true,
1375
+ clientKind: "git",
1376
+ useIgnoreFile: true
1377
+ },
1378
+ linter: {
1379
+ enabled: linter === "biome",
1380
+ rules: {
1381
+ recommended: true
1382
+ }
1383
+ },
1384
+ formatter: {
1385
+ enabled: formatter === "biome"
1386
+ }
1387
+ };
1388
+ files["biome.json"] = {
1389
+ type: "text",
1390
+ content: JSON.stringify(biomeConfig, null, 2)
1391
+ };
1392
+ }
1393
+ for (const [filePath, file] of Object.entries(files)) {
1394
+ const fullPath = join(monorepoRoot, filePath);
1395
+ await mkdir(dirname(fullPath), { recursive: true });
1396
+ await writeFile(fullPath, file.content);
1397
+ }
1398
+ await ensureConfigInWorkspace(monorepoRoot);
1399
+ if (existingConfigs.eslintConfigPath && linter === "eslint") {
1400
+ try {
1401
+ await unlink(existingConfigs.eslintConfigPath);
1402
+ } catch {
1403
+ }
1404
+ }
1405
+ if (existingConfigs.prettierConfigPath && formatter === "prettier") {
1406
+ try {
1407
+ await unlink(existingConfigs.prettierConfigPath);
1408
+ } catch {
1409
+ }
1410
+ }
1411
+ spinner.stop(color.green("\u2713") + " Workspace fixed!");
1412
+ const generated = Object.keys(files).filter(
1413
+ (f) => f.endsWith("package.json")
1414
+ );
1415
+ for (const pkgFile of generated) {
1416
+ const pkgName = pkgFile.replace("/package.json", "");
1417
+ console.log(color.dim(` Generated ${pkgName}`));
1418
+ }
1419
+ const vscodeSettingsExists = await fileExists(
1420
+ join(monorepoRoot, ".vscode/settings.json")
1421
+ );
1422
+ const vscodeExtensionsExists = await fileExists(
1423
+ join(monorepoRoot, ".vscode/extensions.json")
1424
+ );
1425
+ const vscodeExists = vscodeSettingsExists && vscodeExtensionsExists;
1426
+ if (!vscodeExists) {
1427
+ let addVscode = false;
1428
+ if (isNonInteractive) {
1429
+ addVscode = true;
1430
+ } else {
1431
+ const vscodeChoice = await p.confirm({
1432
+ message: "Generate VS Code settings?",
1433
+ initialValue: true
1434
+ });
1435
+ addVscode = !p.isCancel(vscodeChoice) && vscodeChoice;
1436
+ }
1437
+ if (addVscode) {
1438
+ const vscodeFiles = {};
1439
+ generateVscodeFiles(vscodeFiles, linter, formatter);
1440
+ for (const [filePath, file] of Object.entries(vscodeFiles)) {
1441
+ const fullPath = join(monorepoRoot, filePath);
1442
+ await mkdir(dirname(fullPath), { recursive: true });
1443
+ await writeFile(fullPath, file.content);
999
1444
  }
1445
+ console.log(color.dim(" Generated .vscode/settings.json"));
1446
+ console.log(color.dim(" Generated .vscode/extensions.json"));
1000
1447
  }
1001
- s.stop("Project created!");
1002
- const isLibrary2 = generateOptions.projectType === "library";
1003
- const nextSteps = isLibrary2 ? [
1004
- `cd ${generateOptions.name}`,
1005
- `${packageManager} install`,
1006
- `${packageManager} run build`
1007
- ].join("\n") : [
1008
- `cd ${generateOptions.name}`,
1009
- `${packageManager} install`,
1010
- `${packageManager} run dev`
1011
- ].join("\n");
1012
- p.note(nextSteps, "Next steps");
1013
- const savedEditor = getPreferredEditor();
1014
- let selectedEditor;
1015
- if (savedEditor && savedEditor !== "skip") {
1448
+ }
1449
+ const aiFilePaths = {
1450
+ "cursor-rules": ".cursor/rules",
1451
+ "agents-md": "AGENTS.md",
1452
+ "claude-md": "CLAUDE.md",
1453
+ "copilot-md": ".github/copilot-instructions.md"
1454
+ };
1455
+ const existingAiFiles = [];
1456
+ for (const [choice, path] of Object.entries(aiFilePaths)) {
1457
+ if (await fileExists(join(monorepoRoot, path))) {
1458
+ existingAiFiles.push(choice);
1459
+ }
1460
+ }
1461
+ let selectedAiFiles = [];
1462
+ const savedAiFiles = getAiFiles();
1463
+ const availableChoices = ["cursor-rules", "agents-md", "claude-md", "copilot-md"].filter((c) => !existingAiFiles.includes(c));
1464
+ if (availableChoices.length === 0) {
1465
+ } else if (isNonInteractive) {
1466
+ const preferred = savedAiFiles ?? ["cursor-rules"];
1467
+ selectedAiFiles = preferred.filter((f) => availableChoices.includes(f));
1468
+ } else if (savedAiFiles && savedAiFiles.length > 0) {
1469
+ const availableSaved = savedAiFiles.filter(
1470
+ (f) => availableChoices.includes(f)
1471
+ );
1472
+ if (availableSaved.length > 0) {
1473
+ const savedLabels = availableSaved.map((f) => aiFilePaths[f]).join(", ");
1016
1474
  const useDefault = await p.confirm({
1017
- message: `Open in editor? ${color.dim(`(${editorNames[savedEditor]})`)}`,
1475
+ message: `Generate AI instruction files? ${color.dim(
1476
+ `(${savedLabels})`
1477
+ )}`,
1018
1478
  initialValue: true
1019
1479
  });
1020
- if (p.isCancel(useDefault)) {
1021
- selectedEditor = void 0;
1022
- } else if (useDefault) {
1023
- selectedEditor = savedEditor;
1024
- } else {
1025
- selectedEditor = "skip";
1480
+ if (!p.isCancel(useDefault) && useDefault) {
1481
+ selectedAiFiles = availableSaved;
1026
1482
  }
1027
- } else {
1028
- const openEditor = await p.select({
1029
- message: "Open project in editor?",
1030
- options: [
1031
- { value: "skip", label: "Skip" },
1032
- { value: "cursor", label: "Cursor" },
1033
- { value: "code", label: "VS Code" },
1034
- { value: "webstorm", label: "WebStorm" }
1035
- ],
1036
- initialValue: "skip"
1483
+ }
1484
+ } else {
1485
+ const aiFilesChoice = await p.multiselect({
1486
+ message: "Generate AI instruction files?",
1487
+ options: availableChoices.map((c) => ({
1488
+ value: c,
1489
+ label: aiFilePaths[c],
1490
+ hint: c === "cursor-rules" ? "Cursor AI" : c === "agents-md" ? "GitHub Copilot, general" : c === "claude-md" ? "Claude" : "GitHub Copilot"
1491
+ })),
1492
+ required: false
1493
+ });
1494
+ if (!p.isCancel(aiFilesChoice) && aiFilesChoice.length > 0) {
1495
+ selectedAiFiles = aiFilesChoice;
1496
+ const saveChoice = await p.confirm({
1497
+ message: "Save as default for future?",
1498
+ initialValue: true
1037
1499
  });
1038
- if (!p.isCancel(openEditor)) {
1039
- selectedEditor = openEditor;
1040
- const saveChoice = await p.confirm({
1041
- message: `Save ${editorNames[selectedEditor] ?? "Skip"} as default editor?`,
1042
- initialValue: true
1043
- });
1044
- if (!p.isCancel(saveChoice) && saveChoice) {
1045
- setPreferredEditor(selectedEditor);
1046
- if (selectedEditor === "cursor" || selectedEditor === "code") {
1047
- const reuseChoice = await p.confirm({
1048
- message: "Reuse current window when opening projects?",
1049
- initialValue: false
1050
- });
1051
- if (!p.isCancel(reuseChoice)) {
1052
- setReuseWindow(reuseChoice);
1053
- }
1054
- }
1055
- }
1500
+ if (!p.isCancel(saveChoice) && saveChoice) {
1501
+ setAiFiles(selectedAiFiles);
1056
1502
  }
1057
1503
  }
1058
- if (selectedEditor && selectedEditor !== "skip") {
1059
- try {
1060
- await openInEditor(
1061
- selectedEditor,
1062
- basePath,
1063
- getReuseWindow()
1064
- );
1065
- p.log.success(`Opening in ${editorNames[selectedEditor]}...`);
1066
- } catch {
1067
- p.log.warn(
1068
- `Could not open ${editorNames[selectedEditor]}. Make sure the CLI command is in your PATH.`
1069
- );
1504
+ }
1505
+ if (selectedAiFiles.length > 0) {
1506
+ const scope = await getMonorepoScope(monorepoRoot);
1507
+ const aiFilesOutput = {};
1508
+ generateAiFiles(aiFilesOutput, {
1509
+ name: scope,
1510
+ packageManager: "pnpm",
1511
+ linter,
1512
+ formatter,
1513
+ aiFiles: selectedAiFiles
1514
+ });
1515
+ for (const [filePath, file] of Object.entries(aiFilesOutput)) {
1516
+ const fullPath = join(monorepoRoot, filePath);
1517
+ await mkdir(dirname(fullPath), { recursive: true });
1518
+ await writeFile(fullPath, file.content);
1519
+ console.log(color.dim(` Generated ${filePath}`));
1520
+ }
1521
+ }
1522
+ process.exit(0);
1523
+ } catch (error) {
1524
+ spinner.stop(color.red("\u2717") + " Failed to fix workspace");
1525
+ console.error(error);
1526
+ process.exit(1);
1527
+ }
1528
+ }
1529
+ async function handleWorkspaceCommand(name, options) {
1530
+ const monorepoRoot = await detectMonorepoRoot();
1531
+ if (!monorepoRoot) {
1532
+ console.error(
1533
+ color.red("Error:") + " --workspace flag requires being inside a monorepo"
1534
+ );
1535
+ process.exit(1);
1536
+ }
1537
+ if (!name) {
1538
+ console.error(
1539
+ color.red("Error:") + " Package name is required with --workspace flag"
1540
+ );
1541
+ console.log(
1542
+ color.dim(
1543
+ " Example: pnpm create krispya my-lib --workspace --type library"
1544
+ )
1545
+ );
1546
+ process.exit(1);
1547
+ }
1548
+ const scope = await getMonorepoScope(monorepoRoot);
1549
+ const inheritedTooling = await detectWorkspaceTooling(monorepoRoot);
1550
+ const projectType = options.type ?? "app";
1551
+ const defaultDir = projectType === "library" ? "packages" : "apps";
1552
+ const targetDir = options.dir ?? defaultDir;
1553
+ const template = options.template ?? "vanilla";
1554
+ const baseTemplate = getBaseTemplate(template);
1555
+ const scopedName = name.startsWith("@") ? name : `@${scope}/${name}`;
1556
+ const fullPackagePath = join(monorepoRoot, targetDir, name);
1557
+ try {
1558
+ await access(fullPackagePath, constants$1.F_OK);
1559
+ console.error(
1560
+ color.red("Error:") + ` Directory ${targetDir}/${name} already exists`
1561
+ );
1562
+ process.exit(1);
1563
+ } catch {
1564
+ }
1565
+ const versions = {};
1566
+ const versionPromises = [];
1567
+ const isLibrary = projectType === "library";
1568
+ if (!isLibrary) {
1569
+ versionPromises.push(
1570
+ getLatestNpmVersion("vite", "6.3.4").then((v) => {
1571
+ versions.vite = v;
1572
+ })
1573
+ );
1574
+ }
1575
+ const linter = inheritedTooling.linter ?? options.linter ?? "oxlint";
1576
+ const formatter = inheritedTooling.formatter ?? options.formatter ?? "oxfmt";
1577
+ await Promise.all(versionPromises);
1578
+ const relativePkgPath = join(targetDir, name);
1579
+ const workspaceRoot = calculateWorkspaceRoot(relativePkgPath);
1580
+ const generateOptions = {
1581
+ name: scopedName,
1582
+ projectType,
1583
+ libraryBundler: isLibrary ? options.bundler ?? "unbuild" : void 0,
1584
+ template,
1585
+ linter,
1586
+ formatter,
1587
+ workspaceRoot,
1588
+ versions,
1589
+ ...baseTemplate === "r3f" && {
1590
+ drei: options.drei ? {} : void 0,
1591
+ handle: options.handle ? {} : void 0,
1592
+ leva: options.leva ? {} : void 0,
1593
+ postprocessing: options.postprocessing ? {} : void 0,
1594
+ rapier: options.rapier ? {} : void 0,
1595
+ xr: options.xr ? {} : void 0,
1596
+ uikit: options.uikit ? {} : void 0,
1597
+ offscreen: options.offscreen ? {} : void 0,
1598
+ zustand: options.zustand ? {} : void 0,
1599
+ koota: options.koota ? {} : void 0,
1600
+ viverse: options.viverse ? {} : void 0,
1601
+ triplex: options.triplex ? {} : void 0
1602
+ }
1603
+ };
1604
+ console.log(
1605
+ color.cyan("Creating") + ` ${scopedName} in ${targetDir}/${name}...`
1606
+ );
1607
+ try {
1608
+ const files = generate(generateOptions);
1609
+ await writeGeneratedFiles(fullPackagePath, files);
1610
+ console.log(
1611
+ color.green("\u2713") + ` Created ${scopedName} at ${targetDir}/${name}`
1612
+ );
1613
+ process.exit(0);
1614
+ } catch (error) {
1615
+ console.error(color.red("Error:") + " Failed to create package");
1616
+ console.error(String(error));
1617
+ process.exit(1);
1618
+ }
1619
+ }
1620
+ async function handleMonorepoCreation(generateOptions) {
1621
+ const { generateMonorepo } = await import('./chunks/index.mjs').then(function (n) { return n.s; });
1622
+ const packageManager = generateOptions.packageManager || "pnpm";
1623
+ if (packageManager === "pnpm") {
1624
+ generateOptions.pnpmVersion = await getLatestPnpmVersion();
1625
+ } else if (packageManager === "yarn") {
1626
+ generateOptions.yarnVersion = await getLatestYarnVersion();
1627
+ } else if (packageManager === "npm") {
1628
+ generateOptions.npmVersion = await getLatestNpmCliVersion();
1629
+ }
1630
+ const nodeVersion = generateOptions.nodeVersion ?? "latest";
1631
+ if (nodeVersion === "latest") {
1632
+ generateOptions.nodeVersion = await getLatestNodeVersion();
1633
+ }
1634
+ const savedAiFiles = getAiFiles();
1635
+ let selectedAiFiles = [];
1636
+ if (savedAiFiles && savedAiFiles.length > 0) {
1637
+ const aiFileLabels = {
1638
+ "cursor-rules": ".cursor/rules",
1639
+ "agents-md": "AGENTS.md",
1640
+ "claude-md": "CLAUDE.md",
1641
+ "copilot-md": ".github/copilot-instructions.md"
1642
+ };
1643
+ const savedLabels = savedAiFiles.map((f) => aiFileLabels[f]).join(", ");
1644
+ const useDefault = await p.confirm({
1645
+ message: `Generate AI instruction files? ${color.dim(
1646
+ `(${savedLabels})`
1647
+ )}`,
1648
+ initialValue: true
1649
+ });
1650
+ if (!p.isCancel(useDefault) && useDefault) {
1651
+ selectedAiFiles = savedAiFiles;
1652
+ }
1653
+ } else {
1654
+ const aiFilesChoice = await p.multiselect({
1655
+ message: "Generate AI instruction files?",
1656
+ options: [
1657
+ { value: "cursor-rules", label: ".cursor/rules", hint: "Cursor AI" },
1658
+ {
1659
+ value: "agents-md",
1660
+ label: "AGENTS.md",
1661
+ hint: "GitHub Copilot, general"
1662
+ },
1663
+ { value: "claude-md", label: "CLAUDE.md", hint: "Claude" },
1664
+ {
1665
+ value: "copilot-md",
1666
+ label: ".github/copilot-instructions.md",
1667
+ hint: "GitHub Copilot"
1070
1668
  }
1669
+ ],
1670
+ required: false
1671
+ });
1672
+ if (!p.isCancel(aiFilesChoice) && aiFilesChoice.length > 0) {
1673
+ selectedAiFiles = aiFilesChoice;
1674
+ const saveChoice = await p.confirm({
1675
+ message: "Save as default for future monorepos?",
1676
+ initialValue: true
1677
+ });
1678
+ if (!p.isCancel(saveChoice) && saveChoice) {
1679
+ setAiFiles(selectedAiFiles);
1680
+ }
1681
+ }
1682
+ }
1683
+ const projectPath = join(cwd(), generateOptions.name);
1684
+ const spinner = p.spinner();
1685
+ spinner.start("Creating monorepo workspace...");
1686
+ try {
1687
+ const { files } = generateMonorepo({
1688
+ name: generateOptions.name,
1689
+ linter: generateOptions.linter ?? "oxlint",
1690
+ formatter: generateOptions.formatter ?? "oxfmt",
1691
+ packageManager,
1692
+ pnpmVersion: generateOptions.pnpmVersion,
1693
+ pnpmManageVersions: generateOptions.pnpmManageVersions,
1694
+ nodeVersion: generateOptions.nodeVersion,
1695
+ aiFiles: selectedAiFiles.length > 0 ? selectedAiFiles : void 0
1696
+ });
1697
+ const filePaths = Object.keys(files).sort();
1698
+ for (const filePath of filePaths) {
1699
+ const fullFilePath = join(projectPath, filePath);
1700
+ await mkdir(dirname(fullFilePath), { recursive: true });
1701
+ const file = files[filePath];
1702
+ if (file.type === "text") {
1703
+ await writeFile(fullFilePath, file.content);
1704
+ }
1705
+ }
1706
+ spinner.stop(color.green.inverse(" \u2713 Monorepo workspace created! "));
1707
+ const newMonorepoTooling = {
1708
+ linter: generateOptions.linter,
1709
+ formatter: generateOptions.formatter
1710
+ };
1711
+ const scope = generateOptions.name;
1712
+ let addMore = true;
1713
+ while (addMore) {
1714
+ addMore = await createPackageInWorkspace(
1715
+ projectPath,
1716
+ packageManager,
1717
+ newMonorepoTooling,
1718
+ scope
1719
+ );
1720
+ }
1721
+ const nextSteps = [
1722
+ `cd ${generateOptions.name}`,
1723
+ `${packageManager} install`,
1724
+ `${packageManager} run dev`
1725
+ ].join("\n");
1726
+ p.note(nextSteps, "Next steps");
1727
+ await promptAndOpenEditor(projectPath);
1728
+ p.outro(color.green("Happy coding! \u2728"));
1729
+ process.exit(0);
1730
+ } catch (error) {
1731
+ spinner.stop("Failed to create monorepo workspace");
1732
+ p.log.error(String(error));
1733
+ process.exit(1);
1734
+ }
1735
+ }
1736
+ async function handleStandaloneProjectCreation(generateOptions) {
1737
+ const base = generateOptions.template ? getBaseTemplate(generateOptions.template) : "vanilla";
1738
+ const defaultFallbackName = base === "vanilla" ? "vanilla-app" : base === "react" ? "react-app" : "react-three-app";
1739
+ generateOptions.name ??= defaultFallbackName;
1740
+ const packageManager = generateOptions.packageManager || "pnpm";
1741
+ if (packageManager === "pnpm") {
1742
+ generateOptions.pnpmVersion = await getLatestPnpmVersion();
1743
+ } else if (packageManager === "yarn") {
1744
+ generateOptions.yarnVersion = await getLatestYarnVersion();
1745
+ } else if (packageManager === "npm") {
1746
+ generateOptions.npmVersion = await getLatestNpmCliVersion();
1747
+ }
1748
+ const nodeVersion = generateOptions.nodeVersion ?? "latest";
1749
+ if (nodeVersion === "latest") {
1750
+ generateOptions.nodeVersion = await getLatestNodeVersion();
1751
+ }
1752
+ const versions = {};
1753
+ const versionPromises = [];
1754
+ const isLibrary = generateOptions.projectType === "library";
1755
+ const testing = generateOptions.testing ?? (isLibrary ? "vitest" : "none");
1756
+ if (testing === "vitest") {
1757
+ versionPromises.push(
1758
+ getLatestNpmVersion("vitest", "4.0.0").then((v) => {
1759
+ versions.vitest = v;
1760
+ })
1761
+ );
1762
+ }
1763
+ if (!isLibrary) {
1764
+ versionPromises.push(
1765
+ getLatestNpmVersion("vite", "6.3.4").then((v) => {
1766
+ versions.vite = v;
1767
+ })
1768
+ );
1769
+ }
1770
+ const linter = generateOptions.linter ?? "oxlint";
1771
+ if (linter === "eslint") {
1772
+ versionPromises.push(
1773
+ getLatestNpmVersion("eslint", "9.17.0").then((v) => {
1774
+ versions.eslint = v;
1775
+ })
1776
+ );
1777
+ } else if (linter === "oxlint") {
1778
+ versionPromises.push(
1779
+ getLatestNpmVersion("oxlint", "0.16.0").then((v) => {
1780
+ versions.oxlint = v;
1781
+ })
1782
+ );
1783
+ } else if (linter === "biome") {
1784
+ versionPromises.push(
1785
+ getLatestNpmVersion("@biomejs/biome", "1.9.4").then((v) => {
1786
+ versions.biome = v;
1787
+ })
1788
+ );
1789
+ }
1790
+ const formatter = generateOptions.formatter ?? "oxfmt";
1791
+ if (formatter === "prettier") {
1792
+ versionPromises.push(
1793
+ getLatestNpmVersion("prettier", "3.4.2").then((v) => {
1794
+ versions.prettier = v;
1795
+ })
1796
+ );
1797
+ } else if (formatter === "oxfmt") {
1798
+ versionPromises.push(
1799
+ getLatestNpmVersion("oxfmt", "0.1.0").then((v) => {
1800
+ versions.oxfmt = v;
1801
+ })
1802
+ );
1803
+ } else if (formatter === "biome" && linter !== "biome") {
1804
+ versionPromises.push(
1805
+ getLatestNpmVersion("@biomejs/biome", "1.9.4").then((v) => {
1806
+ versions.biome = v;
1807
+ })
1808
+ );
1809
+ }
1810
+ await Promise.all(versionPromises);
1811
+ generateOptions.versions = versions;
1812
+ const projectPath = join(cwd(), generateOptions.name);
1813
+ const spinner = p.spinner();
1814
+ spinner.start("Creating project...");
1815
+ try {
1816
+ const files = generate(generateOptions);
1817
+ await writeGeneratedFiles(projectPath, files);
1818
+ spinner.stop(color.green.inverse(" \u2713 Project created! "));
1819
+ const nextSteps = isLibrary ? [
1820
+ `cd ${generateOptions.name}`,
1821
+ `${packageManager} install`,
1822
+ `${packageManager} run build`
1823
+ ].join("\n") : [
1824
+ `cd ${generateOptions.name}`,
1825
+ `${packageManager} install`,
1826
+ `${packageManager} run dev`
1827
+ ].join("\n");
1828
+ p.note(nextSteps, "Next steps");
1829
+ await promptAndOpenEditor(projectPath);
1830
+ p.outro(color.green("Happy coding! \u2728"));
1831
+ } catch (error) {
1832
+ spinner.stop("Failed to create project");
1833
+ p.log.error(String(error));
1834
+ process.exit(1);
1835
+ }
1836
+ }
1837
+ async function handleInteractiveMonorepoMode(monorepoRoot) {
1838
+ const choice = await p.select({
1839
+ message: "Detected monorepo workspace",
1840
+ options: [
1841
+ { value: "add", label: "Add new package to this workspace" },
1842
+ { value: "standalone", label: "Create standalone project" }
1843
+ ],
1844
+ initialValue: "add"
1845
+ });
1846
+ if (p.isCancel(choice)) {
1847
+ p.cancel("Operation cancelled.");
1848
+ process.exit(0);
1849
+ }
1850
+ if (choice === "add") {
1851
+ const inheritedTooling = await detectWorkspaceTooling(monorepoRoot);
1852
+ if (inheritedTooling.linter || inheritedTooling.formatter) {
1853
+ const toolingInfo = [
1854
+ inheritedTooling.linter && `linter: ${inheritedTooling.linter}`,
1855
+ inheritedTooling.formatter && `formatter: ${inheritedTooling.formatter}`
1856
+ ].filter(Boolean).join(", ");
1857
+ p.log.info(`Using workspace tooling (${toolingInfo})`);
1858
+ }
1859
+ const scope = await getMonorepoScope(monorepoRoot);
1860
+ let addMore = true;
1861
+ while (addMore) {
1862
+ addMore = await createPackageInWorkspace(
1863
+ monorepoRoot,
1864
+ "pnpm",
1865
+ inheritedTooling,
1866
+ scope
1867
+ );
1868
+ }
1869
+ p.note(
1870
+ [`cd ${monorepoRoot}`, "pnpm install", "pnpm run dev"].join("\n"),
1871
+ "Next steps"
1872
+ );
1873
+ await promptAndOpenEditor(monorepoRoot);
1874
+ p.outro(color.green("Happy coding! \u2728"));
1875
+ process.exit(0);
1876
+ }
1877
+ }
1878
+ async function main() {
1879
+ const program = new Command().name("create-krispya").description(
1880
+ "CLI for creating Vanilla, React, and React Three Fiber projects"
1881
+ ).argument("[name]", "name for the project").option("--type <type>", "project type: app or library (default: app)").option(
1882
+ "--bundler <bundler>",
1883
+ "library bundler: unbuild or tsdown (default: unbuild, only for libraries)"
1884
+ ).option(
1885
+ "--template <type>",
1886
+ "project template: vanilla, vanilla-js, react, react-js, r3f, r3f-js (default: vanilla)"
1887
+ ).option(
1888
+ "--linter <type>",
1889
+ "linter: eslint, oxlint, or biome (default: oxlint)"
1890
+ ).option(
1891
+ "--formatter <type>",
1892
+ "formatter: prettier, oxfmt, or biome (default: oxfmt)"
1893
+ ).option("--drei", "add @react-three/drei (r3f only)").option("--handle", "add @react-three/handle (r3f only)").option("--leva", "add leva (r3f only)").option("--postprocessing", "add @react-three/postprocessing (r3f only)").option("--rapier", "add @react-three/rapier (r3f only)").option("--xr", "add @react-three/xr (r3f only)").option("--uikit", "add @react-three/uikit (r3f only)").option("--offscreen", "add @react-three/offscreen (r3f only)").option("--zustand", "add zustand (r3f only)").option("--koota", "add koota (r3f only)").option("--triplex", "set up triplex development environment (r3f only)").option("--viverse", "set up viverse deployment (r3f only)").option(
1894
+ "--package-manager <manager>",
1895
+ "specify package manager (e.g. npm, yarn, pnpm)"
1896
+ ).option(
1897
+ "--pnpm-manage-versions",
1898
+ "enable manage-package-manager-versions in pnpm-workspace.yaml (default: true)"
1899
+ ).option(
1900
+ "--no-pnpm-manage-versions",
1901
+ "disable manage-package-manager-versions in pnpm-workspace.yaml"
1902
+ ).option(
1903
+ "--node-version <version>",
1904
+ 'set Node.js version for engines.node field (default: "latest")'
1905
+ ).option(
1906
+ "--workspace",
1907
+ "Add package to current monorepo workspace (non-interactive)"
1908
+ ).option(
1909
+ "--dir <directory>",
1910
+ "Target directory for --workspace (default: apps/ or packages/)"
1911
+ ).option("--clear-config", "Clear saved preferences (e.g. editor choice)").option("--config-path", "Print the path to the config file").option(
1912
+ "--check",
1913
+ "Check if current directory is in a valid monorepo workspace"
1914
+ ).option("--fix", "Fix monorepo by generating missing .config packages").option(
1915
+ "--path <directory>",
1916
+ "Run in specified directory instead of current working directory"
1917
+ ).action(async (name, options) => {
1918
+ if (options.path) {
1919
+ process.chdir(options.path);
1920
+ }
1921
+ if (options.clearConfig) {
1922
+ clearConfig();
1923
+ console.log("Configuration cleared.");
1924
+ process.exit(0);
1925
+ }
1926
+ if (options.configPath) {
1927
+ console.log(getConfigPath());
1928
+ process.exit(0);
1929
+ }
1930
+ if (name?.startsWith("-")) {
1931
+ switch (name) {
1932
+ case "--version":
1933
+ case "-V":
1934
+ console.log(pkg.version);
1935
+ process.exit(0);
1936
+ case "--help":
1937
+ case "-h":
1938
+ program.help();
1939
+ break;
1940
+ case "--clear-config":
1941
+ clearConfig();
1942
+ console.log("Configuration cleared.");
1943
+ process.exit(0);
1944
+ case "--config-path":
1945
+ console.log(getConfigPath());
1946
+ process.exit(0);
1947
+ case "--check":
1948
+ await handleCheckCommand();
1949
+ break;
1950
+ case "--fix":
1951
+ options.fix = true;
1952
+ break;
1953
+ default:
1954
+ console.error(color.red(`Unknown option: ${name}`));
1955
+ process.exit(1);
1071
1956
  }
1072
- p.outro(color.green("Happy coding! \u2728"));
1073
- } catch (error) {
1074
- s.stop("Failed to create project");
1075
- p.log.error(String(error));
1957
+ }
1958
+ if (options.check) {
1959
+ await handleCheckCommand();
1960
+ }
1961
+ if (options.fix) {
1962
+ await handleFixCommand(options);
1963
+ }
1964
+ if (options.dir && !options.workspace) {
1965
+ console.error(color.red("Error:") + " --dir requires --workspace flag");
1966
+ console.log(
1967
+ color.dim(
1968
+ " Example: pnpm create krispya my-lib --workspace --dir examples"
1969
+ )
1970
+ );
1076
1971
  process.exit(1);
1077
1972
  }
1973
+ if (options.workspace) {
1974
+ await handleWorkspaceCommand(name, options);
1975
+ }
1976
+ console.clear();
1977
+ p.intro(color.bgCyan(color.black(` create-krispya v${pkg.version} `)));
1978
+ const monorepoRoot = await detectMonorepoRoot();
1979
+ if (monorepoRoot && Object.keys(options).length === 0) {
1980
+ await handleInteractiveMonorepoMode(monorepoRoot);
1981
+ }
1982
+ let generateOptions;
1983
+ if (Object.keys(options).length > 0) {
1984
+ const template = options.template ?? "vanilla";
1985
+ const baseTemplate = getBaseTemplate(template);
1986
+ const defaultName = getDefaultProjectName(template);
1987
+ const projectType = options.type ?? "app";
1988
+ generateOptions = {
1989
+ name: name || defaultName,
1990
+ projectType,
1991
+ libraryBundler: projectType === "library" ? options.bundler ?? "unbuild" : void 0,
1992
+ template,
1993
+ linter: options.linter ?? "oxlint",
1994
+ formatter: options.formatter ?? "oxfmt",
1995
+ ...baseTemplate === "r3f" && {
1996
+ drei: options.drei ? {} : void 0,
1997
+ handle: options.handle ? {} : void 0,
1998
+ leva: options.leva ? {} : void 0,
1999
+ postprocessing: options.postprocessing ? {} : void 0,
2000
+ rapier: options.rapier ? {} : void 0,
2001
+ xr: options.xr ? {} : void 0,
2002
+ uikit: options.uikit ? {} : void 0,
2003
+ offscreen: options.offscreen ? {} : void 0,
2004
+ zustand: options.zustand ? {} : void 0,
2005
+ koota: options.koota ? {} : void 0,
2006
+ viverse: options.viverse ? {} : void 0,
2007
+ triplex: options.triplex ? {} : void 0
2008
+ },
2009
+ packageManager: options.packageManager,
2010
+ pnpmManageVersions: options.pnpmManageVersions,
2011
+ nodeVersion: options.nodeVersion ?? "latest"
2012
+ };
2013
+ } else {
2014
+ generateOptions = await promptForOptions(name);
2015
+ }
2016
+ if (generateOptions.projectType === "monorepo") {
2017
+ await handleMonorepoCreation(generateOptions);
2018
+ } else {
2019
+ await handleStandaloneProjectCreation(generateOptions);
2020
+ }
1078
2021
  });
1079
2022
  await program.parseAsync();
1080
2023
  }