@sanity/cli 6.3.2 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +16 -10
  2. package/dist/actions/init/bootstrapLocalTemplate.js +16 -1
  3. package/dist/actions/init/bootstrapLocalTemplate.js.map +1 -1
  4. package/dist/actions/init/initApp.js +72 -0
  5. package/dist/actions/init/initApp.js.map +1 -0
  6. package/dist/actions/init/initHelpers.js +37 -0
  7. package/dist/actions/init/initHelpers.js.map +1 -0
  8. package/dist/actions/init/initNextJs.js +246 -0
  9. package/dist/actions/init/initNextJs.js.map +1 -0
  10. package/dist/actions/init/initStudio.js +127 -0
  11. package/dist/actions/init/initStudio.js.map +1 -0
  12. package/dist/actions/init/scaffoldTemplate.js +114 -0
  13. package/dist/actions/init/scaffoldTemplate.js.map +1 -0
  14. package/dist/actions/init/templates/appQuickstart.js +2 -1
  15. package/dist/actions/init/templates/appQuickstart.js.map +1 -1
  16. package/dist/actions/init/templates/appSanityUi.js +2 -1
  17. package/dist/actions/init/templates/appSanityUi.js.map +1 -1
  18. package/dist/actions/init/templates/shopify.js +6 -6
  19. package/dist/actions/init/templates/shopify.js.map +1 -1
  20. package/dist/actions/init/templates/shopifyOnline.js +2 -2
  21. package/dist/actions/init/templates/shopifyOnline.js.map +1 -1
  22. package/dist/actions/mcp/detectAvailableEditors.js +16 -3
  23. package/dist/actions/mcp/detectAvailableEditors.js.map +1 -1
  24. package/dist/actions/mcp/editorConfigs.js +192 -132
  25. package/dist/actions/mcp/editorConfigs.js.map +1 -1
  26. package/dist/actions/mcp/setupMCP.js +4 -1
  27. package/dist/actions/mcp/setupMCP.js.map +1 -1
  28. package/dist/actions/mcp/writeMCPConfig.js +2 -2
  29. package/dist/actions/mcp/writeMCPConfig.js.map +1 -1
  30. package/dist/actions/schema/extractSchema.js +5 -7
  31. package/dist/actions/schema/extractSchema.js.map +1 -1
  32. package/dist/commands/datasets/copy.js +14 -0
  33. package/dist/commands/datasets/copy.js.map +1 -1
  34. package/dist/commands/init.js +149 -482
  35. package/dist/commands/init.js.map +1 -1
  36. package/dist/commands/mcp/configure.js +1 -1
  37. package/dist/commands/mcp/configure.js.map +1 -1
  38. package/dist/hooks/prerun/injectEnvVariables.js +3 -5
  39. package/dist/hooks/prerun/injectEnvVariables.js.map +1 -1
  40. package/dist/services/datasets.js +2 -1
  41. package/dist/services/datasets.js.map +1 -1
  42. package/dist/telemetry/init.telemetry.js.map +1 -1
  43. package/dist/util/packageManager/installationInfo/detectPackages.js +13 -7
  44. package/dist/util/packageManager/installationInfo/detectPackages.js.map +1 -1
  45. package/dist/util/update/fetchUpdateInfo.js +40 -0
  46. package/dist/util/update/fetchUpdateInfo.js.map +1 -0
  47. package/dist/util/update/fetchUpdateInfo.worker.js +19 -0
  48. package/dist/util/update/fetchUpdateInfo.worker.js.map +1 -0
  49. package/dist/util/update/getRunnerUpdateCommand.js +33 -0
  50. package/dist/util/update/getRunnerUpdateCommand.js.map +1 -0
  51. package/dist/util/update/getUpdateCommand.js +6 -7
  52. package/dist/util/update/getUpdateCommand.js.map +1 -1
  53. package/dist/util/update/packageRunner.js +10 -0
  54. package/dist/util/update/packageRunner.js.map +1 -0
  55. package/dist/util/update/resolveRunnerPackage.js +45 -0
  56. package/dist/util/update/resolveRunnerPackage.js.map +1 -0
  57. package/dist/util/update/resolveUpdateTarget.js +31 -0
  58. package/dist/util/update/resolveUpdateTarget.js.map +1 -0
  59. package/dist/util/update/showNotificationUpdate.js +8 -6
  60. package/dist/util/update/showNotificationUpdate.js.map +1 -1
  61. package/dist/util/update/updateChecker.js +73 -38
  62. package/dist/util/update/updateChecker.js.map +1 -1
  63. package/oclif.manifest.json +540 -525
  64. package/package.json +6 -6
  65. package/templates/app-quickstart/src/App.tsx +2 -2
  66. package/templates/app-sanity-ui/src/App.tsx +2 -2
  67. package/templates/shopify/schemaTypes/objects/hotspot/imageWithProductHotspotsType.ts +1 -1
  68. package/dist/util/update/fetchLatestVersion.js +0 -21
  69. package/dist/util/update/fetchLatestVersion.js.map +0 -1
@@ -1,56 +1,44 @@
1
- import { existsSync } from 'node:fs';
2
- import { mkdir, writeFile } from 'node:fs/promises';
3
1
  import path from 'node:path';
4
2
  import { styleText } from 'node:util';
5
3
  import { Args, Flags } from '@oclif/core';
6
4
  import { CLIError } from '@oclif/core/errors';
7
- import { getCliToken, SanityCommand, subdebug } from '@sanity/cli-core';
5
+ import { SanityCommand, subdebug } from '@sanity/cli-core';
8
6
  import { confirm, input, logSymbols, select, Separator, spinner } from '@sanity/cli-core/ux';
9
7
  import { isHttpError } from '@sanity/client';
10
8
  import { frameworks } from '@vercel/frameworks';
11
- import { execa } from 'execa';
12
9
  import deburr from 'lodash-es/deburr.js';
13
10
  import { validateSession } from '../actions/auth/ensureAuthenticated.js';
14
11
  import { getProviderName } from '../actions/auth/getProviderName.js';
15
12
  import { login } from '../actions/auth/login/login.js';
16
13
  import { createDataset } from '../actions/dataset/create.js';
17
- import { bootstrapTemplate } from '../actions/init/bootstrapTemplate.js';
18
14
  import { checkNextJsReactCompatibility } from '../actions/init/checkNextJsReactCompatibility.js';
19
- import { countNestedFolders } from '../actions/init/countNestedFolders.js';
20
15
  import { determineAppTemplate } from '../actions/init/determineAppTemplate.js';
21
16
  import { createOrAppendEnvVars } from '../actions/init/env/createOrAppendEnvVars.js';
22
- import { fetchPostInitPrompt } from '../actions/init/fetchPostInitPrompt.js';
23
- import { tryGitInit } from '../actions/init/git.js';
17
+ import { initApp } from '../actions/init/initApp.js';
18
+ import { flagOrDefault, shouldPrompt, writeStagingEnvIfNeeded } from '../actions/init/initHelpers.js';
19
+ import { initNextJs } from '../actions/init/initNextJs.js';
20
+ import { initStudio } from '../actions/init/initStudio.js';
24
21
  import { checkIsRemoteTemplate, getGitHubRepoInfo } from '../actions/init/remoteTemplate.js';
25
- import { resolvePackageManager } from '../actions/init/resolvePackageManager.js';
26
- import templates from '../actions/init/templates/index.js';
27
- import { sanityCliTemplate, sanityConfigTemplate, sanityFolder, sanityStudioTemplate } from '../actions/init/templates/nextjs/index.js';
28
22
  import { setupMCP } from '../actions/mcp/setupMCP.js';
29
23
  import { findOrganizationByUserName } from '../actions/organizations/findOrganizationByUserName.js';
30
24
  import { getOrganizationChoices } from '../actions/organizations/getOrganizationChoices.js';
31
25
  import { getOrganizationsWithAttachGrantInfo } from '../actions/organizations/getOrganizationsWithAttachGrantInfo.js';
32
26
  import { hasProjectAttachGrant } from '../actions/organizations/hasProjectAttachGrant.js';
33
- import { promptForAppendEnv, promptForConfigFiles, promptForEmbeddedStudio, promptForNextTemplate, promptForStudioPath } from '../prompts/init/nextjs.js';
34
- import { promptForTypeScript } from '../prompts/init/promptForTypescript.js';
27
+ import { promptForConfigFiles } from '../prompts/init/nextjs.js';
35
28
  import { promptForDatasetName } from '../prompts/promptForDatasetName.js';
36
29
  import { promptForDefaultConfig } from '../prompts/promptForDefaultConfig.js';
37
30
  import { promptForOrganizationName } from '../prompts/promptForOrganizationName.js';
38
- import { createCorsOrigin, listCorsOrigins } from '../services/cors.js';
39
31
  import { createDataset as createDatasetService, listDatasets } from '../services/datasets.js';
40
32
  import { getProjectFeatures } from '../services/getProjectFeatures.js';
41
33
  import { createOrganization, listOrganizations } from '../services/organizations.js';
42
34
  import { getPlanId, getPlanIdFromCoupon } from '../services/plans.js';
43
- import { createProject, listProjects, updateProjectInitializedAt } from '../services/projects.js';
35
+ import { createProject, listProjects } from '../services/projects.js';
44
36
  import { getCliUser } from '../services/user.js';
45
37
  import { CLIInitStepCompleted } from '../telemetry/init.telemetry.js';
46
38
  import { detectFrameworkRecord } from '../util/detectFramework.js';
47
39
  import { absolutify, validateEmptyPath } from '../util/fsUtils.js';
48
40
  import { getProjectDefaults } from '../util/getProjectDefaults.js';
49
41
  import { getSanityEnv } from '../util/getSanityEnv.js';
50
- import { getPeerDependencies } from '../util/packageManager/getPeerDependencies.js';
51
- import { installDeclaredPackages, installNewPackages } from '../util/packageManager/installPackages.js';
52
- import { getPartialEnvWithNpmPath } from '../util/packageManager/packageManagerChoice.js';
53
- import { ImportDatasetCommand } from './datasets/import.js';
54
42
  const debug = subdebug('init');
55
43
  export class InitCommand extends SanityCommand {
56
44
  static args = {
@@ -395,8 +383,8 @@ export class InitCommand extends SanityCommand {
395
383
  this.log(`\nYou can find your project on Sanity Manage — https://www.sanity.io/manage/project/${projectId}\n`);
396
384
  return;
397
385
  }
398
- let initNext = this.flagOrDefault('nextjs-add-config-files', false);
399
- if (isNextJs && this.promptForUndefinedFlag(this.flags['nextjs-add-config-files'])) {
386
+ let initNext = flagOrDefault(this.flags['nextjs-add-config-files'], false);
387
+ if (isNextJs && shouldPrompt(this.isUnattended(), this.flags['nextjs-add-config-files'])) {
400
388
  initNext = await promptForConfigFiles();
401
389
  }
402
390
  this._trace.log({
@@ -453,14 +441,25 @@ export class InitCommand extends SanityCommand {
453
441
  });
454
442
  }
455
443
  if (initNext) {
456
- await this.initNextJs({
444
+ await initNextJs({
457
445
  datasetName,
458
446
  detectedFramework,
459
447
  envFilename,
460
448
  mcpConfigured,
449
+ nextjsAppendEnv: this.flags['nextjs-append-env'],
450
+ nextjsEmbedStudio: this.flags['nextjs-embed-studio'],
451
+ output: this.output,
452
+ overwriteFiles: this.flags['overwrite-files'],
453
+ packageManager: this.flags['package-manager'],
461
454
  projectId,
455
+ template: this.flags.template,
456
+ trace: this._trace,
457
+ typescript: this.flags.typescript,
458
+ unattended: this.isUnattended(),
462
459
  workDir
463
460
  });
461
+ this._trace.complete();
462
+ return;
464
463
  }
465
464
  // user wants to write environment variables to file
466
465
  if (this.flags.env) {
@@ -475,179 +474,42 @@ export class InitCommand extends SanityCommand {
475
474
  output: this.output,
476
475
  outputPath
477
476
  });
478
- await this.writeStagingEnvIfNeeded(outputPath);
477
+ await writeStagingEnvIfNeeded(this.output, outputPath);
479
478
  this.exit(0);
480
479
  }
481
- // Prompt for template to use
482
- const templateName = await this.promptForTemplate();
483
- this._trace.log({
484
- selectedOption: templateName,
485
- step: 'selectProjectTemplate'
486
- });
487
- const template = templates[templateName];
488
- if (!remoteTemplateInfo && !template) {
489
- this.error(`Template "${templateName}" not found`, {
490
- exit: 1
491
- });
492
- }
493
- let useTypeScript = this.flags.typescript;
494
- if (!remoteTemplateInfo && template && template.typescriptOnly === true) {
495
- useTypeScript = true;
496
- } else if (this.promptForUndefinedFlag(this.flags.typescript)) {
497
- useTypeScript = await promptForTypeScript();
498
- this._trace.log({
499
- selectedOption: useTypeScript ? 'yes' : 'no',
500
- step: 'useTypeScript'
501
- });
502
- }
503
- // If the template has a sample dataset, prompt the user whether or not we should import it
504
- const importDatasetFlag = this.flags['import-dataset'];
505
- const shouldImport = template?.datasetUrl && (importDatasetFlag ?? (!this.isUnattended() && await this.promptForDatasetImport(template.importPrompt)));
506
- this._trace.log({
507
- selectedOption: shouldImport ? 'yes' : 'no',
508
- step: 'importTemplateDataset'
509
- });
510
- try {
511
- await updateProjectInitializedAt(projectId);
512
- } catch (err) {
513
- // Non-critical update
514
- debug('Failed to update cliInitializedAt metadata', err);
515
- }
516
- try {
517
- await bootstrapTemplate({
518
- autoUpdates: this.flags['auto-updates'],
519
- bearerToken: this.flags['template-token'],
520
- dataset: datasetName,
521
- organizationId,
522
- output: this.output,
523
- outputPath,
524
- overwriteFiles: this.flags['overwrite-files'],
525
- packageName: sluggedName,
526
- projectId,
527
- projectName: displayName || defaults.projectName,
528
- remoteTemplateInfo,
529
- templateName,
530
- useTypeScript
531
- });
532
- } catch (error) {
533
- if (error instanceof Error) {
534
- throw error;
535
- }
536
- throw new Error(String(error), {
537
- cause: error
538
- });
539
- }
540
- const pkgManager = await resolvePackageManager({
541
- interactive: !this.isUnattended(),
480
+ const sharedParams = {
481
+ autoUpdates: this.flags['auto-updates'],
482
+ defaults,
483
+ error: this.error.bind(this),
484
+ git: this.flags.git,
485
+ mcpConfigured,
486
+ noGit: this.flags['no-git'],
487
+ organizationId,
542
488
  output: this.output,
489
+ outputPath,
490
+ overwriteFiles: this.flags['overwrite-files'],
543
491
  packageManager: this.flags['package-manager'],
544
- targetDir: outputPath
545
- });
546
- this._trace.log({
547
- selectedOption: pkgManager,
548
- step: 'selectPackageManager'
549
- });
550
- // Now for the slow part... installing dependencies
551
- await installDeclaredPackages(outputPath, pkgManager, {
552
- output: this.output,
492
+ remoteTemplateInfo,
493
+ sluggedName,
494
+ template: this.flags.template,
495
+ templateToken: this.flags['template-token'],
496
+ trace: this._trace,
497
+ typescript: this.flags.typescript,
498
+ unattended: this.isUnattended(),
553
499
  workDir
554
- });
555
- const useGit = !this.flags['no-git'] && (this.flags.git === undefined || Boolean(this.flags.git));
556
- const commitMessage = this.flags.git;
557
- await this.writeStagingEnvIfNeeded(outputPath);
558
- // Try initializing a git repository
559
- if (useGit) {
560
- tryGitInit(outputPath, typeof commitMessage === 'string' ? commitMessage : undefined);
561
- }
562
- // Prompt for dataset import (if a dataset is defined)
563
- if (shouldImport && template?.datasetUrl) {
564
- const token = await getCliToken();
565
- if (!token) {
566
- this.error('Authentication required to import dataset', {
567
- exit: 1
568
- });
569
- }
570
- await ImportDatasetCommand.run([
571
- template.datasetUrl,
572
- '--project-id',
573
- projectId,
574
- '--dataset',
575
- datasetName,
576
- '--token',
577
- token,
578
- '--missing'
579
- ], {
580
- root: outputPath
581
- });
582
- this.log('');
583
- this.log('If you want to delete the imported data, use');
584
- this.log(` ${styleText('cyan', `npx sanity dataset delete ${datasetName}`)}`);
585
- this.log('and create a new clean dataset with');
586
- this.log(` ${styleText('cyan', `npx sanity dataset create <name>`)}\n`);
587
- }
588
- const devCommandMap = {
589
- bun: 'bun dev',
590
- manual: 'npm run dev',
591
- npm: 'npm run dev',
592
- pnpm: 'pnpm dev',
593
- yarn: 'yarn dev'
594
500
  };
595
- const devCommand = devCommandMap[pkgManager];
596
- const isCurrentDir = outputPath === process.cwd();
597
- const goToProjectDir = `\n(${styleText('cyan', `cd ${outputPath}`)} to navigate to your new project directory)`;
598
- if (isAppTemplate) {
599
- //output for custom apps here
600
- this.log(`${logSymbols.success} ${styleText([
601
- 'green',
602
- 'bold'
603
- ], 'Success!')} Your custom app has been scaffolded.`);
604
- if (!isCurrentDir) this.log(goToProjectDir);
605
- this.log(`\n${styleText('bold', 'Next')}, configure the project(s) and dataset(s) your app should work with.`);
606
- this.log('\nGet started in `src/App.tsx`, or refer to our documentation for a walkthrough:');
607
- this.log(styleText([
608
- 'blue',
609
- 'underline'
610
- ], 'https://www.sanity.io/docs/app-sdk/sdk-configuration'));
611
- if (mcpConfigured && mcpConfigured.length > 0) {
612
- const message = await this.getPostInitMCPPrompt(mcpConfigured);
613
- this.log(`\n${message}`);
614
- this.log(`\nLearn more: ${styleText('cyan', 'https://mcp.sanity.io')}`);
615
- this.log(`\nHave feedback? Tell us in the community: ${styleText('cyan', 'https://www.sanity.io/community/join')}`);
616
- }
617
- this.log('\n');
618
- this.log(`Other helpful commands:`);
619
- this.log(`npx sanity docs browse to open the documentation in a browser`);
620
- this.log(`npx sanity dev to start the development server for your app`);
621
- this.log(`npx sanity deploy to deploy your app`);
622
- } else {
623
- //output for Studios here
624
- this.log(`✅ ${styleText([
625
- 'green',
626
- 'bold'
627
- ], 'Success!')} Your Studio has been created.`);
628
- if (!isCurrentDir) this.log(goToProjectDir);
629
- this.log(`\nGet started by running ${styleText('cyan', devCommand)} to launch your Studio's development server`);
630
- if (mcpConfigured && mcpConfigured.length > 0) {
631
- const message = await this.getPostInitMCPPrompt(mcpConfigured);
632
- this.log(`\n${message}`);
633
- this.log(`\nLearn more: ${styleText('cyan', 'https://mcp.sanity.io')}`);
634
- this.log(`\nHave feedback? Tell us in the community: ${styleText('cyan', 'https://www.sanity.io/community/join')}`);
635
- }
636
- this.log('\n');
637
- this.log(`Other helpful commands:`);
638
- this.log(`npx sanity docs browse to open the documentation in a browser`);
639
- this.log(`npx sanity manage to open the project settings in a browser`);
640
- this.log(`npx sanity help to explore the CLI manual`);
641
- }
642
- if (isFirstProject) {
643
- this._trace.log({
644
- selectedOption: 'yes',
645
- step: 'sendCommunityInvite'
646
- });
647
- const DISCORD_INVITE_LINK = 'https://www.sanity.io/community/join';
648
- this.log(`\nJoin the Sanity community: ${styleText('cyan', DISCORD_INVITE_LINK)}`);
649
- this.log('We look forward to seeing you there!\n');
650
- }
501
+ await (isAppTemplate ? initApp({
502
+ ...sharedParams,
503
+ datasetName,
504
+ projectId
505
+ }) : initStudio({
506
+ ...sharedParams,
507
+ datasetName,
508
+ displayName,
509
+ importDataset: this.flags['import-dataset'],
510
+ isFirstProject,
511
+ projectId
512
+ }));
651
513
  this._trace.complete();
652
514
  }
653
515
  checkFlagsInUnattendedMode({ createProjectName, isAppTemplate, isNextJs }) {
@@ -756,9 +618,6 @@ export class InitCommand extends SanityCommand {
756
618
  user: loggedInUser
757
619
  };
758
620
  }
759
- flagOrDefault(flag, defaultValue) {
760
- return typeof this.flags[flag] === 'boolean' ? this.flags[flag] : defaultValue;
761
- }
762
621
  async getOrCreateDataset(opts) {
763
622
  const visibility = this.flags.visibility;
764
623
  const dataset = this.flags.dataset;
@@ -1003,38 +862,36 @@ export class InitCommand extends SanityCommand {
1003
862
  return undefined;
1004
863
  }
1005
864
  }
1006
- async getPostInitMCPPrompt(editorsNames) {
1007
- return fetchPostInitPrompt(new Intl.ListFormat('en').format(editorsNames));
1008
- }
1009
865
  async getProjectDetails({ isAppTemplate, newProject, planId, showDefaultConfigPrompt, user }) {
1010
866
  if (isAppTemplate) {
1011
- // If organization flag is provided, use it directly (skip prompt and API call)
1012
- if (this.flags.organization) {
1013
- return {
1014
- datasetName: '',
1015
- displayName: '',
1016
- isFirstProject: false,
1017
- organizationId: this.flags.organization,
1018
- projectId: ''
1019
- };
867
+ let organizationId = this.flags.organization;
868
+ if (!organizationId) {
869
+ let organizations;
870
+ try {
871
+ organizations = await listOrganizations();
872
+ } catch (err) {
873
+ this.error(`Failed to communicate with the Sanity API:\n${err.message}`, {
874
+ exit: 1
875
+ });
876
+ }
877
+ organizationId = await this.promptUserForOrganization({
878
+ isAppTemplate: true,
879
+ organizations,
880
+ user
881
+ });
1020
882
  }
1021
- // Interactive mode: fetch orgs and prompt
1022
- // Note: unattended mode without --organization is rejected by checkFlagsInUnattendedMode
1023
- const organizations = await listOrganizations({
1024
- includeImplicitMemberships: 'true',
1025
- includeMembers: 'true'
1026
- });
1027
- const appOrganizationId = await this.promptUserForOrganization({
1028
- isAppTemplate: true,
1029
- organizations,
883
+ const { datasetName, displayName, projectId } = await this.promptForAppTemplateSetup({
884
+ newProject,
885
+ organizationId,
886
+ planId,
1030
887
  user
1031
888
  });
1032
889
  return {
1033
- datasetName: '',
1034
- displayName: '',
890
+ datasetName,
891
+ displayName,
1035
892
  isFirstProject: false,
1036
- organizationId: appOrganizationId,
1037
- projectId: ''
893
+ organizationId,
894
+ projectId
1038
895
  };
1039
896
  }
1040
897
  debug('Prompting user to select or create a project');
@@ -1078,183 +935,90 @@ export class InitCommand extends SanityCommand {
1078
935
  });
1079
936
  return absolutify(inputPath);
1080
937
  }
1081
- async initNextJs({ datasetName, detectedFramework, envFilename, mcpConfigured, projectId, workDir }) {
1082
- let useTypeScript = this.flagOrDefault('typescript', true);
1083
- if (this.promptForUndefinedFlag(this.flags.typescript)) {
1084
- useTypeScript = await promptForTypeScript();
1085
- }
1086
- this._trace.log({
1087
- selectedOption: useTypeScript ? 'yes' : 'no',
1088
- step: 'useTypeScript'
1089
- });
1090
- const fileExtension = useTypeScript ? 'ts' : 'js';
1091
- let embeddedStudio = this.flagOrDefault('nextjs-embed-studio', true);
1092
- if (this.promptForUndefinedFlag(this.flags['nextjs-embed-studio'])) {
1093
- embeddedStudio = await promptForEmbeddedStudio();
1094
- }
1095
- let hasSrcFolder = false;
1096
- if (embeddedStudio) {
1097
- // find source path (app or src/app)
1098
- const appDir = 'app';
1099
- let srcPath = path.join(workDir, appDir);
1100
- if (!existsSync(srcPath)) {
1101
- srcPath = path.join(workDir, 'src', appDir);
1102
- hasSrcFolder = true;
1103
- if (!existsSync(srcPath)) {
1104
- try {
1105
- await mkdir(srcPath, {
1106
- recursive: true
1107
- });
1108
- } catch {
1109
- debug('Error creating folder %s', srcPath);
1110
- }
1111
- }
938
+ async promptForAppTemplateSetup({ newProject, organizationId, planId, user }) {
939
+ if (this.isUnattended()) {
940
+ if (!this.flags.project && !newProject) {
941
+ return {
942
+ datasetName: '',
943
+ displayName: '',
944
+ projectId: ''
945
+ };
1112
946
  }
1113
- const studioPath = this.isUnattended() ? '/studio' : await promptForStudioPath();
1114
- const embeddedStudioRouteFilePath = path.join(srcPath, `${studioPath}/`, `[[...tool]]/page.${fileExtension}x`);
1115
- // this selects the correct template string based on whether the user is using the app or pages directory and
1116
- // replaces the ":configPath:" placeholder in the template with the correct path to the sanity.config.ts file.
1117
- // we account for the user-defined embeddedStudioPath (default /studio) is accounted for by creating enough "../"
1118
- // relative paths to reach the root level of the project
1119
- await this.writeOrOverwrite(embeddedStudioRouteFilePath, sanityStudioTemplate.replace(':configPath:', `${'../'.repeat(countNestedFolders(path.dirname(embeddedStudioRouteFilePath.slice(workDir.length))))}sanity.config`), workDir);
1120
- const sanityConfigPath = path.join(workDir, `sanity.config.${fileExtension}`);
1121
- await this.writeOrOverwrite(sanityConfigPath, sanityConfigTemplate(hasSrcFolder).replace(':route:', embeddedStudioRouteFilePath.slice(workDir.length).replace('src/', '')).replace(':basePath:', studioPath), workDir);
1122
- }
1123
- const sanityCliPath = path.join(workDir, `sanity.cli.${fileExtension}`);
1124
- await this.writeOrOverwrite(sanityCliPath, sanityCliTemplate, workDir);
1125
- let templateToUse = this.flags.template ?? 'clean';
1126
- if (this.promptForUndefinedFlag(this.flags.template)) {
1127
- templateToUse = await promptForNextTemplate();
1128
- }
1129
- await this.writeSourceFiles({
1130
- fileExtension,
1131
- files: sanityFolder(useTypeScript, templateToUse),
1132
- folderPath: undefined,
1133
- srcFolderPrefix: hasSrcFolder,
1134
- workDir
1135
- });
1136
- let appendEnv = this.flagOrDefault('nextjs-append-env', true);
1137
- if (this.promptForUndefinedFlag(this.flags['nextjs-append-env'])) {
1138
- appendEnv = await promptForAppendEnv(envFilename);
947
+ const project = await this.getOrCreateProject({
948
+ newProject,
949
+ planId,
950
+ user
951
+ });
952
+ const dataset = await this.getOrCreateDataset({
953
+ displayName: project.displayName,
954
+ projectId: project.projectId,
955
+ showDefaultConfigPrompt: false
956
+ });
957
+ return {
958
+ datasetName: dataset.datasetName,
959
+ displayName: project.displayName,
960
+ projectId: project.projectId
961
+ };
1139
962
  }
1140
- if (appendEnv) {
1141
- await createOrAppendEnvVars({
1142
- envVars: {
1143
- DATASET: datasetName,
1144
- PROJECT_ID: projectId
963
+ const projects = (await listProjects()).toSorted((a, b)=>b.createdAt.localeCompare(a.createdAt));
964
+ const projectChoices = projects.map((project)=>({
965
+ name: `${project.displayName} (${project.id})`,
966
+ value: project.id
967
+ }));
968
+ const SKIP_PROJECT = '__skip__';
969
+ const NEW_PROJECT = '__new__';
970
+ const selected = await select({
971
+ choices: [
972
+ {
973
+ name: "Skip — I'll configure later",
974
+ value: SKIP_PROJECT
1145
975
  },
1146
- filename: envFilename,
1147
- framework: detectedFramework,
1148
- log: true,
1149
- output: this.output,
1150
- outputPath: workDir
976
+ {
977
+ name: 'Create new project',
978
+ value: NEW_PROJECT
979
+ },
980
+ ...projectChoices.length > 0 ? [
981
+ new Separator(),
982
+ ...projectChoices
983
+ ] : []
984
+ ],
985
+ message: 'Configure a project for this app?'
986
+ });
987
+ if (selected === SKIP_PROJECT) {
988
+ this._trace.log({
989
+ selectedOption: 'skip',
990
+ step: 'configureAppProject'
1151
991
  });
992
+ return {
993
+ datasetName: '',
994
+ displayName: '',
995
+ projectId: ''
996
+ };
1152
997
  }
1153
- if (embeddedStudio) {
1154
- const nextjsLocalDevOrigin = 'http://localhost:3000';
1155
- const existingCorsOrigins = await listCorsOrigins(projectId);
1156
- const hasExistingCorsOrigin = existingCorsOrigins.some((item)=>item.origin === nextjsLocalDevOrigin);
1157
- if (!hasExistingCorsOrigin) {
1158
- try {
1159
- const createCorsRes = await createCorsOrigin({
1160
- allowCredentials: true,
1161
- origin: nextjsLocalDevOrigin,
1162
- projectId
1163
- });
1164
- this.log(createCorsRes.id ? `Added ${nextjsLocalDevOrigin} to CORS origins` : `Failed to add ${nextjsLocalDevOrigin} to CORS origins`);
1165
- } catch (error) {
1166
- debug(`Error creating new CORS Origin ${nextjsLocalDevOrigin}: ${error}`);
1167
- this.error(`Failed to add ${nextjsLocalDevOrigin} to CORS origins: ${error}`, {
1168
- exit: 1
1169
- });
1170
- }
1171
- }
1172
- }
1173
- const chosen = await resolvePackageManager({
1174
- interactive: !this.isUnattended(),
1175
- output: this.output,
1176
- packageManager: this.flags['package-manager'],
1177
- targetDir: workDir
1178
- });
1179
998
  this._trace.log({
1180
- selectedOption: chosen,
1181
- step: 'selectPackageManager'
999
+ selectedOption: selected === NEW_PROJECT ? 'create' : 'existing',
1000
+ step: 'configureAppProject'
1182
1001
  });
1183
- const packages = [
1184
- '@sanity/vision@5',
1185
- 'sanity@5',
1186
- '@sanity/image-url@2',
1187
- 'styled-components@6'
1188
- ];
1189
- if (templateToUse === 'blog') {
1190
- packages.push('@sanity/icons');
1191
- }
1192
- await installNewPackages({
1193
- packageManager: chosen,
1194
- packages
1195
- }, {
1196
- output: this.output,
1197
- workDir
1198
- });
1199
- // will refactor this later
1200
- const execOptions = {
1201
- cwd: workDir,
1202
- encoding: 'utf8',
1203
- env: getPartialEnvWithNpmPath(workDir),
1204
- stdio: 'inherit'
1002
+ const project = selected === NEW_PROJECT ? await this.promptForProjectCreation({
1003
+ isUsersFirstProject: projects.length === 0,
1004
+ organizationId,
1005
+ organizations: [],
1006
+ planId,
1007
+ user
1008
+ }) : {
1009
+ displayName: projects.find((p)=>p.id === selected)?.displayName ?? '',
1010
+ projectId: selected
1205
1011
  };
1206
- switch(chosen){
1207
- case 'npm':
1208
- {
1209
- await execa('npm', [
1210
- 'install',
1211
- 'next-sanity@12'
1212
- ], execOptions);
1213
- break;
1214
- }
1215
- case 'pnpm':
1216
- {
1217
- await execa('pnpm', [
1218
- 'install',
1219
- 'next-sanity@12'
1220
- ], execOptions);
1221
- break;
1222
- }
1223
- case 'yarn':
1224
- {
1225
- const peerDeps = await getPeerDependencies('next-sanity@12', workDir);
1226
- await installNewPackages({
1227
- packageManager: 'yarn',
1228
- packages: [
1229
- 'next-sanity@12',
1230
- ...peerDeps
1231
- ]
1232
- }, {
1233
- output: this.output,
1234
- workDir
1235
- });
1236
- break;
1237
- }
1238
- default:
1239
- {
1240
- break;
1241
- }
1242
- }
1243
- this.log(`\n${styleText('green', 'Success!')} Your Sanity configuration files has been added to this project`);
1244
- if (mcpConfigured && mcpConfigured.length > 0) {
1245
- const message = await this.getPostInitMCPPrompt(mcpConfigured);
1246
- this.log(`\n${message}`);
1247
- this.log(`\nLearn more: ${styleText('cyan', 'https://mcp.sanity.io')}`);
1248
- this.log(`\nHave feedback? Tell us in the community: ${styleText('cyan', 'https://www.sanity.io/community/join')}`);
1249
- }
1250
- await this.writeStagingEnvIfNeeded(workDir);
1251
- this.exit(0);
1252
- }
1253
- async promptForDatasetImport(message) {
1254
- return confirm({
1255
- default: true,
1256
- message: message || 'This template includes a sample dataset, would you like to use it?'
1012
+ const dataset = await this.getOrCreateDataset({
1013
+ displayName: project.displayName,
1014
+ projectId: project.projectId,
1015
+ showDefaultConfigPrompt: false
1257
1016
  });
1017
+ return {
1018
+ datasetName: dataset.datasetName,
1019
+ displayName: project.displayName,
1020
+ projectId: project.projectId
1021
+ };
1258
1022
  }
1259
1023
  async promptForProjectCreation({ isUsersFirstProject, organizationId, organizations, planId, user }) {
1260
1024
  const projectName = await input({
@@ -1290,37 +1054,6 @@ export class InitCommand extends SanityCommand {
1290
1054
  userAction: 'create'
1291
1055
  };
1292
1056
  }
1293
- async promptForTemplate() {
1294
- const template = this.flags.template;
1295
- const defaultTemplate = this.isUnattended() || template ? template || 'clean' : null;
1296
- if (defaultTemplate) {
1297
- return defaultTemplate;
1298
- }
1299
- return select({
1300
- choices: [
1301
- {
1302
- name: 'Clean project with no predefined schema types',
1303
- value: 'clean'
1304
- },
1305
- {
1306
- name: 'Blog (schema)',
1307
- value: 'blog'
1308
- },
1309
- {
1310
- name: 'E-commerce (Shopify)',
1311
- value: 'shopify'
1312
- },
1313
- {
1314
- name: 'Movie project (schema + sample data)',
1315
- value: 'moviedb'
1316
- }
1317
- ],
1318
- message: 'Select project template'
1319
- });
1320
- }
1321
- promptForUndefinedFlag(flag) {
1322
- return !this.isUnattended() && flag === undefined;
1323
- }
1324
1057
  async promptUserForNewOrganization(user) {
1325
1058
  const name = await promptForOrganizationName(user);
1326
1059
  const spin = spinner('Creating organization').start();
@@ -1426,72 +1159,6 @@ export class InitCommand extends SanityCommand {
1426
1159
  }
1427
1160
  }
1428
1161
  }
1429
- async writeOrOverwrite(filePath, content, workDir) {
1430
- if (existsSync(filePath)) {
1431
- let overwrite = this.flagOrDefault('overwrite-files', false);
1432
- if (this.promptForUndefinedFlag(this.flags['overwrite-files'])) {
1433
- overwrite = await confirm({
1434
- default: false,
1435
- message: `File ${styleText('yellow', filePath.replace(workDir, ''))} already exists. Do you want to overwrite it?`
1436
- });
1437
- }
1438
- if (!overwrite) {
1439
- return;
1440
- }
1441
- }
1442
- // make folder if not exists
1443
- const folderPath = path.dirname(filePath);
1444
- try {
1445
- await mkdir(folderPath, {
1446
- recursive: true
1447
- });
1448
- } catch {
1449
- debug('Error creating folder %s', folderPath);
1450
- }
1451
- await writeFile(filePath, content, {
1452
- encoding: 'utf8'
1453
- });
1454
- }
1455
- // write sanity folder files
1456
- async writeSourceFiles({ fileExtension, files, folderPath, srcFolderPrefix, workDir }) {
1457
- for (const [filePath, content] of Object.entries(files)){
1458
- // check if file ends with full stop to indicate it's file and not directory (this only works with our template tree structure)
1459
- if (filePath.includes('.') && typeof content === 'string') {
1460
- await this.writeOrOverwrite(path.join(workDir, srcFolderPrefix ? 'src' : '', 'sanity', folderPath || '', `${filePath}${fileExtension}`), content, workDir);
1461
- } else {
1462
- await mkdir(path.join(workDir, srcFolderPrefix ? 'src' : '', 'sanity', filePath), {
1463
- recursive: true
1464
- });
1465
- if (typeof content === 'object') {
1466
- await this.writeSourceFiles({
1467
- fileExtension,
1468
- files: content,
1469
- folderPath: filePath,
1470
- srcFolderPrefix,
1471
- workDir
1472
- });
1473
- }
1474
- }
1475
- }
1476
- }
1477
- /**
1478
- * When running in a non-production Sanity environment (e.g. staging), write the
1479
- * `SANITY_INTERNAL_ENV` variable to a `.env` file in the output directory so that
1480
- * the bootstrapped project continues to target the same environment.
1481
- */ async writeStagingEnvIfNeeded(outputPath) {
1482
- const sanityEnv = getSanityEnv();
1483
- if (sanityEnv === 'production') return;
1484
- await createOrAppendEnvVars({
1485
- envVars: {
1486
- INTERNAL_ENV: sanityEnv
1487
- },
1488
- filename: '.env',
1489
- framework: null,
1490
- log: false,
1491
- output: this.output,
1492
- outputPath
1493
- });
1494
- }
1495
1162
  }
1496
1163
 
1497
1164
  //# sourceMappingURL=init.js.map