create-krispya 0.5.0 → 0.5.1

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
@@ -123,7 +123,32 @@ 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 clearConfig() {
142
+ config.clear();
143
+ }
144
+ function getConfigPath() {
145
+ return config.path;
146
+ }
147
+ function getCustomTemplates() {
148
+ return config.get("customTemplates") ?? {};
149
+ }
150
+
151
+ function getDefaultOptions(template, name, projectType = "app", libraryBundler, integrations, inheritedTooling) {
127
152
  const baseTemplate = getBaseTemplate(template);
128
153
  const base = {
129
154
  name,
@@ -133,26 +158,26 @@ function getDefaultOptions(template, name, projectType = "app", libraryBundler)
133
158
  packageManager: "pnpm",
134
159
  pnpmManageVersions: true,
135
160
  nodeVersion: "latest",
136
- linter: "oxlint",
137
- formatter: "oxfmt",
161
+ linter: inheritedTooling?.linter ?? "oxlint",
162
+ formatter: inheritedTooling?.formatter ?? "oxfmt",
138
163
  // Libraries get vitest by default, apps don't
139
164
  testing: projectType === "library" ? "vitest" : "none"
140
165
  };
141
- if (baseTemplate === "r3f") {
166
+ if (baseTemplate === "r3f" && integrations) {
142
167
  return {
143
168
  ...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: {}
169
+ drei: integrations.includes("drei") ? {} : void 0,
170
+ handle: integrations.includes("handle") ? {} : void 0,
171
+ leva: integrations.includes("leva") ? {} : void 0,
172
+ postprocessing: integrations.includes("postprocessing") ? {} : void 0,
173
+ rapier: integrations.includes("rapier") ? {} : void 0,
174
+ xr: integrations.includes("xr") ? {} : void 0,
175
+ uikit: integrations.includes("uikit") ? {} : void 0,
176
+ offscreen: integrations.includes("offscreen") ? {} : void 0,
177
+ zustand: integrations.includes("zustand") ? {} : void 0,
178
+ koota: integrations.includes("koota") ? {} : void 0,
179
+ triplex: integrations.includes("triplex") ? {} : void 0,
180
+ viverse: integrations.includes("viverse") ? {} : void 0
156
181
  };
157
182
  }
158
183
  return base;
@@ -168,7 +193,33 @@ function getDefaultProjectName(template) {
168
193
  return `react-three-${generateRandomName()}`;
169
194
  }
170
195
  }
171
- async function promptForCustomization(template, name, projectType) {
196
+ async function promptForR3fIntegrations() {
197
+ const selected = await p.multiselect({
198
+ message: "R3F integrations",
199
+ options: [
200
+ { value: "drei", label: "Drei" },
201
+ { value: "handle", label: "Handle" },
202
+ { value: "leva", label: "Leva" },
203
+ { value: "postprocessing", label: "Postprocessing" },
204
+ { value: "rapier", label: "Rapier" },
205
+ { value: "xr", label: "XR" },
206
+ { value: "uikit", label: "UIKit" },
207
+ { value: "offscreen", label: "Offscreen" },
208
+ { value: "zustand", label: "Zustand" },
209
+ { value: "koota", label: "Koota" },
210
+ { value: "triplex", label: "Triplex" },
211
+ { value: "viverse", label: "Viverse" }
212
+ ],
213
+ initialValues: ["drei"],
214
+ required: false
215
+ });
216
+ if (p.isCancel(selected)) {
217
+ p.cancel("Operation cancelled.");
218
+ process.exit(0);
219
+ }
220
+ return selected;
221
+ }
222
+ async function promptForCustomization(template, name, projectType, integrations, inheritedTooling) {
172
223
  let libraryBundler;
173
224
  if (projectType === "library") {
174
225
  const bundler = await p.select({
@@ -240,31 +291,39 @@ async function promptForCustomization(template, name, projectType) {
240
291
  }
241
292
  pnpmManageVersions = managePnpm;
242
293
  }
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);
294
+ let linter = inheritedTooling?.linter ?? "oxlint";
295
+ let formatter = inheritedTooling?.formatter ?? "oxfmt";
296
+ if (!inheritedTooling?.linter) {
297
+ const linterChoice = await p.select({
298
+ message: "Linter",
299
+ options: [
300
+ { value: "oxlint", label: "Oxlint", hint: "fast, from OXC" },
301
+ { value: "eslint", label: "ESLint", hint: "classic" },
302
+ { value: "biome", label: "Biome", hint: "all-in-one" }
303
+ ],
304
+ initialValue: "oxlint"
305
+ });
306
+ if (p.isCancel(linterChoice)) {
307
+ p.cancel("Operation cancelled.");
308
+ process.exit(0);
309
+ }
310
+ linter = linterChoice;
255
311
  }
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);
312
+ if (!inheritedTooling?.formatter) {
313
+ const formatterChoice = await p.select({
314
+ message: "Formatter",
315
+ options: [
316
+ { value: "oxfmt", label: "Oxfmt", hint: "fast, Prettier-compatible" },
317
+ { value: "prettier", label: "Prettier", hint: "classic" },
318
+ { value: "biome", label: "Biome", hint: "all-in-one" }
319
+ ],
320
+ initialValue: "oxfmt"
321
+ });
322
+ if (p.isCancel(formatterChoice)) {
323
+ p.cancel("Operation cancelled.");
324
+ process.exit(0);
325
+ }
326
+ formatter = formatterChoice;
268
327
  }
269
328
  const testing = await p.select({
270
329
  message: "Testing",
@@ -292,47 +351,7 @@ async function promptForCustomization(template, name, projectType) {
292
351
  }
293
352
  const baseTemplate = getBaseTemplate(template);
294
353
  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 {
354
+ const base = {
336
355
  name,
337
356
  template: finalTemplate,
338
357
  projectType,
@@ -342,8 +361,11 @@ async function promptForCustomization(template, name, projectType) {
342
361
  pnpmManageVersions,
343
362
  linter,
344
363
  formatter,
345
- testing,
346
- ...baseTemplate === "r3f" && {
364
+ testing
365
+ };
366
+ if (baseTemplate === "r3f" && integrations) {
367
+ return {
368
+ ...base,
347
369
  drei: integrations.includes("drei") ? {} : void 0,
348
370
  handle: integrations.includes("handle") ? {} : void 0,
349
371
  leva: integrations.includes("leva") ? {} : void 0,
@@ -356,8 +378,9 @@ async function promptForCustomization(template, name, projectType) {
356
378
  koota: integrations.includes("koota") ? {} : void 0,
357
379
  triplex: integrations.includes("triplex") ? {} : void 0,
358
380
  viverse: integrations.includes("viverse") ? {} : void 0
359
- }
360
- };
381
+ };
382
+ }
383
+ return base;
361
384
  }
362
385
  async function promptForInitialPackage() {
363
386
  const choice = await p.select({
@@ -476,19 +499,15 @@ async function promptForMonorepo(workspaceName) {
476
499
  }),
477
500
  "Workspace Configuration"
478
501
  );
479
- const action = await p.select({
502
+ const proceed = await p.confirm({
480
503
  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"
504
+ initialValue: true
486
505
  });
487
- if (p.isCancel(action)) {
506
+ if (p.isCancel(proceed)) {
488
507
  p.cancel("Operation cancelled.");
489
508
  process.exit(0);
490
509
  }
491
- if (action === "confirm") {
510
+ if (proceed) {
492
511
  return defaultOptions;
493
512
  }
494
513
  return promptForMonorepoCustomization(workspaceName);
@@ -528,65 +547,122 @@ async function promptForOptions(name) {
528
547
  }
529
548
  return promptForPackageOptions(projectName, projectType);
530
549
  }
531
- async function promptForPackageOptions(projectName, projectType) {
532
- const template = await p.select({
550
+ function customTemplateToOptions(customTemplate, name, projectType) {
551
+ const baseTemplate = customTemplate.baseTemplate;
552
+ const template = baseTemplate;
553
+ const base = {
554
+ name,
555
+ template,
556
+ projectType,
557
+ packageManager: "pnpm",
558
+ pnpmManageVersions: true,
559
+ nodeVersion: "latest",
560
+ linter: customTemplate.linter,
561
+ formatter: customTemplate.formatter,
562
+ testing: customTemplate.testing
563
+ };
564
+ if (baseTemplate === "r3f" && customTemplate.integrations) {
565
+ const integrations = customTemplate.integrations;
566
+ return {
567
+ ...base,
568
+ drei: integrations.includes("drei") ? {} : void 0,
569
+ handle: integrations.includes("handle") ? {} : void 0,
570
+ leva: integrations.includes("leva") ? {} : void 0,
571
+ postprocessing: integrations.includes("postprocessing") ? {} : void 0,
572
+ rapier: integrations.includes("rapier") ? {} : void 0,
573
+ xr: integrations.includes("xr") ? {} : void 0,
574
+ uikit: integrations.includes("uikit") ? {} : void 0,
575
+ offscreen: integrations.includes("offscreen") ? {} : void 0,
576
+ zustand: integrations.includes("zustand") ? {} : void 0,
577
+ koota: integrations.includes("koota") ? {} : void 0,
578
+ triplex: integrations.includes("triplex") ? {} : void 0,
579
+ viverse: integrations.includes("viverse") ? {} : void 0
580
+ };
581
+ }
582
+ return base;
583
+ }
584
+ async function promptForPackageOptions(projectName, projectType, inheritedTooling) {
585
+ const builtInOptions = [
586
+ { value: "vanilla", label: "Vanilla" },
587
+ { value: "react", label: "React" },
588
+ { value: "r3f", label: "React Three Fiber" }
589
+ ];
590
+ const customTemplates = getCustomTemplates();
591
+ const customOptions = Object.keys(customTemplates).map((name) => ({
592
+ value: `custom:${name}`,
593
+ label: name,
594
+ hint: "saved template"
595
+ }));
596
+ const allOptions = [...builtInOptions, ...customOptions];
597
+ const templateSelection = await p.select({
533
598
  message: "Select a template",
534
- options: [
535
- { value: "vanilla", label: "Vanilla" },
536
- { value: "react", label: "React" },
537
- { value: "r3f", label: "React Three Fiber" }
538
- ],
599
+ options: allOptions,
539
600
  initialValue: "vanilla"
540
601
  });
541
- if (p.isCancel(template)) {
602
+ if (p.isCancel(templateSelection)) {
542
603
  p.cancel("Operation cancelled.");
543
604
  process.exit(0);
544
605
  }
606
+ const selection = templateSelection;
607
+ if (selection.startsWith("custom:")) {
608
+ const customName = selection.slice(7);
609
+ const customTemplate = customTemplates[customName];
610
+ const defaultOptions2 = customTemplateToOptions(customTemplate, projectName, projectType);
611
+ if (inheritedTooling?.linter) {
612
+ defaultOptions2.linter = inheritedTooling.linter;
613
+ }
614
+ if (inheritedTooling?.formatter) {
615
+ defaultOptions2.formatter = inheritedTooling.formatter;
616
+ }
617
+ const configTitle2 = inheritedTooling ? `Template: ${customName} (using workspace tooling)` : `Template: ${customName}`;
618
+ p.note(formatConfigSummary(defaultOptions2), configTitle2);
619
+ const proceed2 = await p.confirm({
620
+ message: "Proceed with these settings?",
621
+ initialValue: true
622
+ });
623
+ if (p.isCancel(proceed2)) {
624
+ p.cancel("Operation cancelled.");
625
+ process.exit(0);
626
+ }
627
+ if (proceed2) {
628
+ return defaultOptions2;
629
+ }
630
+ return promptForCustomization(
631
+ customTemplate.baseTemplate,
632
+ projectName,
633
+ projectType,
634
+ customTemplate.integrations,
635
+ inheritedTooling
636
+ );
637
+ }
638
+ const template = selection;
639
+ const baseTemplate = getBaseTemplate(template);
640
+ let integrations;
641
+ if (baseTemplate === "r3f") {
642
+ integrations = await promptForR3fIntegrations();
643
+ }
545
644
  const defaultOptions = getDefaultOptions(
546
645
  template,
547
646
  projectName,
548
- projectType
647
+ projectType,
648
+ void 0,
649
+ integrations,
650
+ inheritedTooling
549
651
  );
550
- p.note(formatConfigSummary(defaultOptions), "Template Configuration");
551
- const action = await p.select({
652
+ const configTitle = inheritedTooling ? "Template Configuration (using workspace tooling)" : "Template Configuration";
653
+ p.note(formatConfigSummary(defaultOptions), configTitle);
654
+ const proceed = await p.confirm({
552
655
  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"
656
+ initialValue: true
558
657
  });
559
- if (p.isCancel(action)) {
658
+ if (p.isCancel(proceed)) {
560
659
  p.cancel("Operation cancelled.");
561
660
  process.exit(0);
562
661
  }
563
- if (action === "confirm") {
662
+ if (proceed) {
564
663
  return defaultOptions;
565
664
  }
566
- return promptForCustomization(
567
- template,
568
- projectName,
569
- projectType
570
- );
571
- }
572
-
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);
587
- }
588
- function clearConfig() {
589
- config.clear();
665
+ return promptForCustomization(template, projectName, projectType, integrations, inheritedTooling);
590
666
  }
591
667
 
592
668
  const require$1 = createRequire(import.meta.url);
@@ -608,6 +684,258 @@ async function detectMonorepoRoot() {
608
684
  }
609
685
  return null;
610
686
  }
687
+ async function detectWorkspaceTooling(monorepoRoot) {
688
+ try {
689
+ const pkgPath = join(monorepoRoot, "package.json");
690
+ const content = await readFile(pkgPath, "utf-8");
691
+ const pkg2 = JSON.parse(content);
692
+ const devDeps = pkg2.devDependencies ?? {};
693
+ const linter = devDeps.oxlint ? "oxlint" : devDeps.eslint ? "eslint" : devDeps["@biomejs/biome"] ? "biome" : void 0;
694
+ const formatter = devDeps.oxfmt ? "oxfmt" : devDeps.prettier ? "prettier" : devDeps["@biomejs/biome"] ? "biome" : void 0;
695
+ return { linter, formatter };
696
+ } catch {
697
+ return {};
698
+ }
699
+ }
700
+ async function getMonorepoScope(monorepoRoot) {
701
+ try {
702
+ const pkgPath = join(monorepoRoot, "package.json");
703
+ const content = await readFile(pkgPath, "utf-8");
704
+ const pkg2 = JSON.parse(content);
705
+ if (pkg2.name) {
706
+ return pkg2.name.replace(/^@/, "").replace(/\/.*$/, "");
707
+ }
708
+ } catch {
709
+ }
710
+ return monorepoRoot.split(/[/\\]/).pop() ?? "workspace";
711
+ }
712
+ async function getWorkspacePackages(monorepoRoot) {
713
+ const packagesDir = join(monorepoRoot, "packages");
714
+ const packages = [];
715
+ try {
716
+ const { readdir } = await import('fs/promises');
717
+ const entries = await readdir(packagesDir, { withFileTypes: true });
718
+ for (const entry of entries) {
719
+ if (entry.isDirectory()) {
720
+ try {
721
+ const pkgJsonPath = join(packagesDir, entry.name, "package.json");
722
+ const content = await readFile(pkgJsonPath, "utf-8");
723
+ const pkg2 = JSON.parse(content);
724
+ if (pkg2.name) {
725
+ packages.push({ name: pkg2.name, path: `packages/${entry.name}` });
726
+ }
727
+ } catch {
728
+ }
729
+ }
730
+ }
731
+ } catch {
732
+ }
733
+ return packages;
734
+ }
735
+ async function createPackageInWorkspace(monorepoRoot, packageManager, inheritedTooling, scope) {
736
+ const packageType = await promptForInitialPackage();
737
+ if (packageType === "skip") {
738
+ return false;
739
+ }
740
+ const packageNameInput = await p.text({
741
+ message: "Package name?",
742
+ placeholder: `Scoped to @${scope}/`,
743
+ validate: (value) => {
744
+ if (!value.length) return "Package name is required";
745
+ }
746
+ });
747
+ if (p.isCancel(packageNameInput)) {
748
+ return false;
749
+ }
750
+ const shortName = packageNameInput;
751
+ const scopedName = `@${scope}/${shortName}`;
752
+ const targetDir = packageType === "app" ? "apps" : "packages";
753
+ const packagePath = join(targetDir, shortName);
754
+ const workspaceRoot = "../..";
755
+ const packageOptions = await promptForPackageOptions(
756
+ scopedName,
757
+ packageType,
758
+ inheritedTooling
759
+ );
760
+ packageOptions.workspaceRoot = workspaceRoot;
761
+ packageOptions.name = scopedName;
762
+ if (packageManager === "pnpm") {
763
+ packageOptions.pnpmVersion = await getLatestPnpmVersion();
764
+ }
765
+ const nodeVersion = packageOptions.nodeVersion ?? "latest";
766
+ if (nodeVersion === "latest") {
767
+ packageOptions.nodeVersion = await getLatestNodeVersion();
768
+ }
769
+ const versions = {};
770
+ const versionPromises = [];
771
+ const pkgIsLibrary = packageOptions.projectType === "library";
772
+ const pkgTesting = packageOptions.testing ?? (pkgIsLibrary ? "vitest" : "none");
773
+ if (pkgTesting === "vitest") {
774
+ versionPromises.push(
775
+ getLatestNpmVersion("vitest", "4.0.0").then((v) => {
776
+ versions.vitest = v;
777
+ })
778
+ );
779
+ }
780
+ if (!pkgIsLibrary) {
781
+ versionPromises.push(
782
+ getLatestNpmVersion("vite", "6.3.4").then((v) => {
783
+ versions.vite = v;
784
+ })
785
+ );
786
+ }
787
+ const linter = packageOptions.linter ?? "oxlint";
788
+ if (linter === "eslint") {
789
+ versionPromises.push(
790
+ getLatestNpmVersion("eslint", "9.17.0").then((v) => {
791
+ versions.eslint = v;
792
+ })
793
+ );
794
+ } else if (linter === "oxlint") {
795
+ versionPromises.push(
796
+ getLatestNpmVersion("oxlint", "0.16.0").then((v) => {
797
+ versions.oxlint = v;
798
+ })
799
+ );
800
+ } else if (linter === "biome") {
801
+ versionPromises.push(
802
+ getLatestNpmVersion("@biomejs/biome", "1.9.4").then((v) => {
803
+ versions.biome = v;
804
+ })
805
+ );
806
+ }
807
+ const formatter = packageOptions.formatter ?? "oxfmt";
808
+ if (formatter === "prettier") {
809
+ versionPromises.push(
810
+ getLatestNpmVersion("prettier", "3.4.2").then((v) => {
811
+ versions.prettier = v;
812
+ })
813
+ );
814
+ } else if (formatter === "oxfmt") {
815
+ versionPromises.push(
816
+ getLatestNpmVersion("oxfmt", "0.1.0").then((v) => {
817
+ versions.oxfmt = v;
818
+ })
819
+ );
820
+ } else if (formatter === "biome" && linter !== "biome") {
821
+ versionPromises.push(
822
+ getLatestNpmVersion("@biomejs/biome", "1.9.4").then((v) => {
823
+ versions.biome = v;
824
+ })
825
+ );
826
+ }
827
+ await Promise.all(versionPromises);
828
+ packageOptions.versions = versions;
829
+ if (packageType === "app") {
830
+ const workspacePackages = await getWorkspacePackages(monorepoRoot);
831
+ if (workspacePackages.length > 0) {
832
+ const selectedDeps = await p.multiselect({
833
+ message: "Add workspace dependencies?",
834
+ options: workspacePackages.map((pkg2) => ({
835
+ value: pkg2.name,
836
+ label: pkg2.name.replace(/^@[^/]+\//, "")
837
+ })),
838
+ required: false
839
+ });
840
+ if (!p.isCancel(selectedDeps) && selectedDeps.length > 0) {
841
+ packageOptions.workspaceDependencies = selectedDeps;
842
+ }
843
+ }
844
+ }
845
+ const basePath = join(monorepoRoot, packagePath);
846
+ const s = p.spinner();
847
+ s.start("Creating package...");
848
+ try {
849
+ const files = generate(packageOptions);
850
+ const filePaths = Object.keys(files).sort();
851
+ for (const filePath of filePaths) {
852
+ const fullFilePath = join(basePath, filePath);
853
+ await mkdir(dirname(fullFilePath), { recursive: true });
854
+ const file = files[filePath];
855
+ if (file.type === "text") {
856
+ await writeFile(fullFilePath, file.content);
857
+ } else {
858
+ const response = await fetch(file.url);
859
+ await writeFile(fullFilePath, response.body);
860
+ }
861
+ }
862
+ s.stop(color.green.inverse(` \u2713 Package created at ${packagePath}! `));
863
+ const addAnother = await p.select({
864
+ message: "Add another package?",
865
+ options: [
866
+ { value: "no", label: "No, I'm done" },
867
+ { value: "yes", label: "Yes, add another" }
868
+ ],
869
+ initialValue: "no"
870
+ });
871
+ return !p.isCancel(addAnother) && addAnother === "yes";
872
+ } catch (error) {
873
+ s.stop("Failed to create package");
874
+ p.log.error(String(error));
875
+ return false;
876
+ }
877
+ }
878
+ async function promptAndOpenEditor(basePath) {
879
+ const savedEditor = getPreferredEditor();
880
+ let selectedEditor;
881
+ if (savedEditor && savedEditor !== "skip") {
882
+ const useDefault = await p.confirm({
883
+ message: `Open in editor? ${color.dim(`(${editorNames[savedEditor]})`)}`,
884
+ initialValue: true
885
+ });
886
+ if (p.isCancel(useDefault)) {
887
+ selectedEditor = void 0;
888
+ } else if (useDefault) {
889
+ selectedEditor = savedEditor;
890
+ } else {
891
+ selectedEditor = "skip";
892
+ }
893
+ } else {
894
+ const openEditor = await p.select({
895
+ message: "Open project in editor?",
896
+ options: [
897
+ { value: "skip", label: "Skip" },
898
+ { value: "cursor", label: "Cursor" },
899
+ { value: "code", label: "VS Code" },
900
+ { value: "webstorm", label: "WebStorm" }
901
+ ],
902
+ initialValue: "skip"
903
+ });
904
+ if (!p.isCancel(openEditor)) {
905
+ selectedEditor = openEditor;
906
+ const saveChoice = await p.confirm({
907
+ message: `Save ${editorNames[selectedEditor] ?? "Skip"} as default editor?`,
908
+ initialValue: true
909
+ });
910
+ if (!p.isCancel(saveChoice) && saveChoice) {
911
+ setPreferredEditor(selectedEditor);
912
+ if (selectedEditor === "cursor" || selectedEditor === "code") {
913
+ const reuseChoice = await p.confirm({
914
+ message: "Reuse current window when opening projects?",
915
+ initialValue: false
916
+ });
917
+ if (!p.isCancel(reuseChoice)) {
918
+ setReuseWindow(reuseChoice);
919
+ }
920
+ }
921
+ }
922
+ }
923
+ }
924
+ if (selectedEditor && selectedEditor !== "skip") {
925
+ try {
926
+ await openInEditor(
927
+ selectedEditor,
928
+ basePath,
929
+ getReuseWindow()
930
+ );
931
+ p.log.success(`Opening in ${editorNames[selectedEditor]}...`);
932
+ } catch {
933
+ p.log.warn(
934
+ `Could not open ${editorNames[selectedEditor]}. Make sure the CLI command is in your PATH.`
935
+ );
936
+ }
937
+ }
938
+ }
611
939
  async function main() {
612
940
  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
941
  "--bundler <bundler>",
@@ -624,12 +952,16 @@ async function main() {
624
952
  ).option(
625
953
  "--node-version <version>",
626
954
  '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) => {
955
+ ).option("-y, --yes", "Skip prompts and use default values").option("--clear-config", "Clear saved preferences (e.g. editor choice)").option("--config-path", "Print the path to the config file").action(async (name, options) => {
628
956
  if (options.clearConfig) {
629
957
  clearConfig();
630
958
  console.log("Configuration cleared.");
631
959
  process.exit(0);
632
960
  }
961
+ if (options.configPath) {
962
+ console.log(getConfigPath());
963
+ process.exit(0);
964
+ }
633
965
  console.clear();
634
966
  p.intro(color.bgCyan(color.black(` create-krispya v${pkg.version} `)));
635
967
  const monorepoRoot = await detectMonorepoRoot();
@@ -647,132 +979,26 @@ async function main() {
647
979
  process.exit(0);
648
980
  }
649
981
  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
- );
982
+ const inheritedTooling = await detectWorkspaceTooling(monorepoRoot);
983
+ if (inheritedTooling.linter || inheritedTooling.formatter) {
984
+ const toolingInfo = [
985
+ inheritedTooling.linter && `linter: ${inheritedTooling.linter}`,
986
+ inheritedTooling.formatter && `formatter: ${inheritedTooling.formatter}`
987
+ ].filter(Boolean).join(", ");
988
+ p.log.info(`Using workspace tooling (${toolingInfo})`);
697
989
  }
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...");
743
- 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
- }
756
- }
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);
990
+ const scope = await getMonorepoScope(monorepoRoot);
991
+ let addMore = true;
992
+ while (addMore) {
993
+ addMore = await createPackageInWorkspace(monorepoRoot, "pnpm", inheritedTooling, scope);
775
994
  }
995
+ p.note(
996
+ [`cd ${monorepoRoot}`, "pnpm install", "pnpm run dev"].join("\n"),
997
+ "Next steps"
998
+ );
999
+ await promptAndOpenEditor(monorepoRoot);
1000
+ p.outro(color.green("Happy coding! \u2728"));
1001
+ process.exit(0);
776
1002
  }
777
1003
  }
778
1004
  let generateOptions;
@@ -841,60 +1067,15 @@ async function main() {
841
1067
  await writeFile(fullFilePath, file.content);
842
1068
  }
843
1069
  }
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
- }
853
- });
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!");
897
- }
1070
+ s2.stop(color.green.inverse(" \u2713 Monorepo workspace created! "));
1071
+ const newMonorepoTooling = {
1072
+ linter: generateOptions.linter,
1073
+ formatter: generateOptions.formatter
1074
+ };
1075
+ const scope = generateOptions.name;
1076
+ let addMore = true;
1077
+ while (addMore) {
1078
+ addMore = await createPackageInWorkspace(basePath2, packageManager2, newMonorepoTooling, scope);
898
1079
  }
899
1080
  const nextSteps = [
900
1081
  `cd ${generateOptions.name}`,
@@ -902,6 +1083,7 @@ async function main() {
902
1083
  `${packageManager2} run dev`
903
1084
  ].join("\n");
904
1085
  p.note(nextSteps, "Next steps");
1086
+ await promptAndOpenEditor(basePath2);
905
1087
  p.outro(color.green("Happy coding! \u2728"));
906
1088
  process.exit(0);
907
1089
  } catch (error) {
@@ -998,7 +1180,7 @@ async function main() {
998
1180
  await writeFile(fullFilePath, response.body);
999
1181
  }
1000
1182
  }
1001
- s.stop("Project created!");
1183
+ s.stop(color.green.inverse(" \u2713 Project created! "));
1002
1184
  const isLibrary2 = generateOptions.projectType === "library";
1003
1185
  const nextSteps = isLibrary2 ? [
1004
1186
  `cd ${generateOptions.name}`,