@sanity/cli 6.3.2 → 6.5.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 (150) hide show
  1. package/README.md +30 -18
  2. package/dist/actions/build/buildApp.js +12 -4
  3. package/dist/actions/build/buildApp.js.map +1 -1
  4. package/dist/actions/build/buildStaticFiles.js +3 -1
  5. package/dist/actions/build/buildStaticFiles.js.map +1 -1
  6. package/dist/actions/build/buildStudio.js +29 -7
  7. package/dist/actions/build/buildStudio.js.map +1 -1
  8. package/dist/actions/build/buildVendorDependencies.js +7 -7
  9. package/dist/actions/build/buildVendorDependencies.js.map +1 -1
  10. package/dist/actions/build/checkRequiredDependencies.js +4 -4
  11. package/dist/actions/build/checkRequiredDependencies.js.map +1 -1
  12. package/dist/actions/build/checkStudioDependencyVersions.js +9 -9
  13. package/dist/actions/build/checkStudioDependencyVersions.js.map +1 -1
  14. package/dist/actions/build/getAutoUpdatesImportMap.js +9 -0
  15. package/dist/actions/build/getAutoUpdatesImportMap.js.map +1 -1
  16. package/dist/actions/build/getViteConfig.js +2 -1
  17. package/dist/actions/build/getViteConfig.js.map +1 -1
  18. package/dist/actions/build/renderDocument.js.map +1 -1
  19. package/dist/actions/build/renderDocumentWorker/addTimestampImportMapScriptToHtml.js +34 -14
  20. package/dist/actions/build/renderDocumentWorker/addTimestampImportMapScriptToHtml.js.map +1 -1
  21. package/dist/actions/build/renderDocumentWorker/getDocumentHtml.js +2 -2
  22. package/dist/actions/build/renderDocumentWorker/getDocumentHtml.js.map +1 -1
  23. package/dist/actions/build/renderDocumentWorker/renderDocumentWorker.js +2 -2
  24. package/dist/actions/build/renderDocumentWorker/renderDocumentWorker.js.map +1 -1
  25. package/dist/actions/codemods/reactIconsV3.js +2 -2
  26. package/dist/actions/codemods/reactIconsV3.js.map +1 -1
  27. package/dist/actions/dev/startStudioDevServer.js +2 -2
  28. package/dist/actions/dev/startStudioDevServer.js.map +1 -1
  29. package/dist/actions/doctor/types.js.map +1 -1
  30. package/dist/actions/graphql/resolveGraphQLApisFromWorkspaces.js.map +1 -1
  31. package/dist/actions/init/bootstrapLocalTemplate.js +16 -1
  32. package/dist/actions/init/bootstrapLocalTemplate.js.map +1 -1
  33. package/dist/actions/init/bootstrapTemplate.js.map +1 -1
  34. package/dist/actions/init/checkNextJsReactCompatibility.js +3 -3
  35. package/dist/actions/init/checkNextJsReactCompatibility.js.map +1 -1
  36. package/dist/actions/init/initAction.js +287 -0
  37. package/dist/actions/init/initAction.js.map +1 -0
  38. package/dist/actions/init/initApp.js +63 -0
  39. package/dist/actions/init/initApp.js.map +1 -0
  40. package/dist/actions/init/initError.js +10 -0
  41. package/dist/actions/init/initError.js.map +1 -0
  42. package/dist/actions/init/initHelpers.js +28 -0
  43. package/dist/actions/init/initHelpers.js.map +1 -0
  44. package/dist/actions/init/initNextJs.js +243 -0
  45. package/dist/actions/init/initNextJs.js.map +1 -0
  46. package/dist/actions/init/initStudio.js +118 -0
  47. package/dist/actions/init/initStudio.js.map +1 -0
  48. package/dist/actions/init/plan/getPlan.js +15 -0
  49. package/dist/actions/init/plan/getPlan.js.map +1 -0
  50. package/dist/actions/init/plan/verifyCoupon.js +35 -0
  51. package/dist/actions/init/plan/verifyCoupon.js.map +1 -0
  52. package/dist/actions/init/plan/verifyPlan.js +34 -0
  53. package/dist/actions/init/plan/verifyPlan.js.map +1 -0
  54. package/dist/actions/init/project/createProjectFromName.js +44 -0
  55. package/dist/actions/init/project/createProjectFromName.js.map +1 -0
  56. package/dist/actions/init/project/getOrCreateDataset.js +126 -0
  57. package/dist/actions/init/project/getOrCreateDataset.js.map +1 -0
  58. package/dist/actions/init/project/getOrCreateProject.js +128 -0
  59. package/dist/actions/init/project/getOrCreateProject.js.map +1 -0
  60. package/dist/actions/init/project/getProjectDetails.js +87 -0
  61. package/dist/actions/init/project/getProjectDetails.js.map +1 -0
  62. package/dist/actions/init/project/getProjectOutputPath.js +17 -0
  63. package/dist/actions/init/project/getProjectOutputPath.js.map +1 -0
  64. package/dist/actions/init/project/promptForAppTemplateSetup.js +112 -0
  65. package/dist/actions/init/project/promptForAppTemplateSetup.js.map +1 -0
  66. package/dist/actions/init/project/promptForProjectCreation.js +40 -0
  67. package/dist/actions/init/project/promptForProjectCreation.js.map +1 -0
  68. package/dist/actions/init/project/promptUserForNewOrganization.js +12 -0
  69. package/dist/actions/init/project/promptUserForNewOrganization.js.map +1 -0
  70. package/dist/actions/init/project/promptUserForOrganization.js +38 -0
  71. package/dist/actions/init/project/promptUserForOrganization.js.map +1 -0
  72. package/dist/actions/init/scaffoldTemplate.js +108 -0
  73. package/dist/actions/init/scaffoldTemplate.js.map +1 -0
  74. package/dist/actions/init/templates/appQuickstart.js +2 -1
  75. package/dist/actions/init/templates/appQuickstart.js.map +1 -1
  76. package/dist/actions/init/templates/appSanityUi.js +2 -1
  77. package/dist/actions/init/templates/appSanityUi.js.map +1 -1
  78. package/dist/actions/init/templates/shopify.js +6 -6
  79. package/dist/actions/init/templates/shopify.js.map +1 -1
  80. package/dist/actions/init/templates/shopifyOnline.js +2 -2
  81. package/dist/actions/init/templates/shopifyOnline.js.map +1 -1
  82. package/dist/actions/init/types.js +47 -1
  83. package/dist/actions/init/types.js.map +1 -1
  84. package/dist/actions/manifest/types.js +0 -1
  85. package/dist/actions/manifest/types.js.map +1 -1
  86. package/dist/actions/mcp/detectAvailableEditors.js +16 -3
  87. package/dist/actions/mcp/detectAvailableEditors.js.map +1 -1
  88. package/dist/actions/mcp/editorConfigs.js +192 -132
  89. package/dist/actions/mcp/editorConfigs.js.map +1 -1
  90. package/dist/actions/mcp/setupMCP.js +4 -1
  91. package/dist/actions/mcp/setupMCP.js.map +1 -1
  92. package/dist/actions/mcp/writeMCPConfig.js +2 -2
  93. package/dist/actions/mcp/writeMCPConfig.js.map +1 -1
  94. package/dist/actions/schema/extractSchema.js +5 -7
  95. package/dist/actions/schema/extractSchema.js.map +1 -1
  96. package/dist/actions/versions/buildPackageArray.js +2 -2
  97. package/dist/actions/versions/buildPackageArray.js.map +1 -1
  98. package/dist/actions/versions/findSanityModulesVersions.js +3 -3
  99. package/dist/actions/versions/findSanityModulesVersions.js.map +1 -1
  100. package/dist/commands/datasets/copy.js +14 -0
  101. package/dist/commands/datasets/copy.js.map +1 -1
  102. package/dist/commands/init.js +11 -1244
  103. package/dist/commands/init.js.map +1 -1
  104. package/dist/commands/mcp/configure.js +1 -1
  105. package/dist/commands/mcp/configure.js.map +1 -1
  106. package/dist/hooks/prerun/injectEnvVariables.js +3 -5
  107. package/dist/hooks/prerun/injectEnvVariables.js.map +1 -1
  108. package/dist/server/vite/plugin-sanity-build-entries.js +2 -1
  109. package/dist/server/vite/plugin-sanity-build-entries.js.map +1 -1
  110. package/dist/services/datasets.js +2 -1
  111. package/dist/services/datasets.js.map +1 -1
  112. package/dist/telemetry/init.telemetry.js.map +1 -1
  113. package/dist/util/compareDependencyVersions.js +4 -4
  114. package/dist/util/compareDependencyVersions.js.map +1 -1
  115. package/dist/util/createExpiringConfig.js +1 -1
  116. package/dist/util/createExpiringConfig.js.map +1 -1
  117. package/dist/util/packageManager/installationInfo/analyzeIssues.js +7 -7
  118. package/dist/util/packageManager/installationInfo/analyzeIssues.js.map +1 -1
  119. package/dist/util/packageManager/installationInfo/detectPackages.js +13 -7
  120. package/dist/util/packageManager/installationInfo/detectPackages.js.map +1 -1
  121. package/dist/util/packageManager/installationInfo/types.js.map +1 -1
  122. package/dist/util/packageManager/packageManagerChoice.js +2 -2
  123. package/dist/util/packageManager/packageManagerChoice.js.map +1 -1
  124. package/dist/util/packageManager/preferredPm.js +106 -0
  125. package/dist/util/packageManager/preferredPm.js.map +1 -0
  126. package/dist/util/update/fetchUpdateInfo.js +40 -0
  127. package/dist/util/update/fetchUpdateInfo.js.map +1 -0
  128. package/dist/util/update/fetchUpdateInfo.worker.js +19 -0
  129. package/dist/util/update/fetchUpdateInfo.worker.js.map +1 -0
  130. package/dist/util/update/getRunnerUpdateCommand.js +33 -0
  131. package/dist/util/update/getRunnerUpdateCommand.js.map +1 -0
  132. package/dist/util/update/getUpdateCommand.js +6 -7
  133. package/dist/util/update/getUpdateCommand.js.map +1 -1
  134. package/dist/util/update/packageRunner.js +10 -0
  135. package/dist/util/update/packageRunner.js.map +1 -0
  136. package/dist/util/update/resolveRunnerPackage.js +45 -0
  137. package/dist/util/update/resolveRunnerPackage.js.map +1 -0
  138. package/dist/util/update/resolveUpdateTarget.js +31 -0
  139. package/dist/util/update/resolveUpdateTarget.js.map +1 -0
  140. package/dist/util/update/showNotificationUpdate.js +8 -6
  141. package/dist/util/update/showNotificationUpdate.js.map +1 -1
  142. package/dist/util/update/updateChecker.js +73 -38
  143. package/dist/util/update/updateChecker.js.map +1 -1
  144. package/oclif.manifest.json +17 -2
  145. package/package.json +23 -22
  146. package/templates/app-quickstart/src/App.tsx +2 -2
  147. package/templates/app-sanity-ui/src/App.tsx +2 -2
  148. package/templates/shopify/schemaTypes/objects/hotspot/imageWithProductHotspotsType.ts +1 -1
  149. package/dist/util/update/fetchLatestVersion.js +0 -21
  150. package/dist/util/update/fetchLatestVersion.js.map +0 -1
@@ -1,57 +1,10 @@
1
- import { existsSync } from 'node:fs';
2
- import { mkdir, writeFile } from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { styleText } from 'node:util';
5
1
  import { Args, Flags } from '@oclif/core';
6
2
  import { CLIError } from '@oclif/core/errors';
7
- import { getCliToken, SanityCommand, subdebug } from '@sanity/cli-core';
8
- import { confirm, input, logSymbols, select, Separator, spinner } from '@sanity/cli-core/ux';
9
- import { isHttpError } from '@sanity/client';
10
- import { frameworks } from '@vercel/frameworks';
11
- import { execa } from 'execa';
12
- import deburr from 'lodash-es/deburr.js';
13
- import { validateSession } from '../actions/auth/ensureAuthenticated.js';
14
- import { getProviderName } from '../actions/auth/getProviderName.js';
15
- import { login } from '../actions/auth/login/login.js';
16
- import { createDataset } from '../actions/dataset/create.js';
17
- import { bootstrapTemplate } from '../actions/init/bootstrapTemplate.js';
18
- import { checkNextJsReactCompatibility } from '../actions/init/checkNextJsReactCompatibility.js';
19
- import { countNestedFolders } from '../actions/init/countNestedFolders.js';
20
- import { determineAppTemplate } from '../actions/init/determineAppTemplate.js';
21
- import { createOrAppendEnvVars } from '../actions/init/env/createOrAppendEnvVars.js';
22
- import { fetchPostInitPrompt } from '../actions/init/fetchPostInitPrompt.js';
23
- import { tryGitInit } from '../actions/init/git.js';
24
- 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
- import { setupMCP } from '../actions/mcp/setupMCP.js';
29
- import { findOrganizationByUserName } from '../actions/organizations/findOrganizationByUserName.js';
30
- import { getOrganizationChoices } from '../actions/organizations/getOrganizationChoices.js';
31
- import { getOrganizationsWithAttachGrantInfo } from '../actions/organizations/getOrganizationsWithAttachGrantInfo.js';
32
- 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';
35
- import { promptForDatasetName } from '../prompts/promptForDatasetName.js';
36
- import { promptForDefaultConfig } from '../prompts/promptForDefaultConfig.js';
37
- import { promptForOrganizationName } from '../prompts/promptForOrganizationName.js';
38
- import { createCorsOrigin, listCorsOrigins } from '../services/cors.js';
39
- import { createDataset as createDatasetService, listDatasets } from '../services/datasets.js';
40
- import { getProjectFeatures } from '../services/getProjectFeatures.js';
41
- import { createOrganization, listOrganizations } from '../services/organizations.js';
42
- import { getPlanId, getPlanIdFromCoupon } from '../services/plans.js';
43
- import { createProject, listProjects, updateProjectInitializedAt } from '../services/projects.js';
44
- import { getCliUser } from '../services/user.js';
45
- import { CLIInitStepCompleted } from '../telemetry/init.telemetry.js';
46
- import { detectFrameworkRecord } from '../util/detectFramework.js';
47
- import { absolutify, validateEmptyPath } from '../util/fsUtils.js';
48
- import { getProjectDefaults } from '../util/getProjectDefaults.js';
3
+ import { SanityCommand } from '@sanity/cli-core';
4
+ import { initAction } from '../actions/init/initAction.js';
5
+ import { InitError } from '../actions/init/initError.js';
6
+ import { flagsToInitOptions } from '../actions/init/types.js';
49
7
  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
- const debug = subdebug('init');
55
8
  export class InitCommand extends SanityCommand {
56
9
  static args = {
57
10
  type: Args.string({
@@ -284,1214 +237,28 @@ export class InitCommand extends SanityCommand {
284
237
  description: 'Unattended mode, answers "yes" to any "yes/no" prompt and otherwise uses defaults'
285
238
  })
286
239
  };
287
- _trace;
288
240
  async run() {
289
- const workDir = process.cwd();
290
- const createProjectName = this.flags['project-name'] ?? this.flags['create-project'];
291
- // For backwards "compatibility" - we used to allow `sanity init plugin`,
292
- // and no longer do - but instead of printing an error about an unknown
293
- // _command_, we want to acknowledge that the user is trying to do something
294
- // that no longer exists but might have at some point in the past.
295
- if (this.args.type) {
296
- this.error(this.args.type === 'plugin' ? 'Initializing plugins through the CLI is no longer supported' : `Unknown init type "${this.args.type}"`, {
297
- exit: 1
298
- });
299
- }
300
- this._trace = this.telemetry.trace(CLIInitStepCompleted);
301
- // Slightly more helpful message for removed flags rather than just saying the flag
302
- // does not exist.
303
- if (this.flags.reconfigure) {
304
- this.error('--reconfigure is deprecated - manual configuration is now required', {
305
- exit: 1
306
- });
307
- }
308
- // Oclif doesn't support custom exclusive error messaging
309
- if (this.flags.project && this.flags.organization) {
310
- this.error('You have specified both a project and an organization. To move a project to an organization please visit https://www.sanity.io/manage', {
311
- exit: 1
312
- });
313
- }
314
- const defaultConfig = this.flags['dataset-default'];
315
- let showDefaultConfigPrompt = !defaultConfig;
316
- if (this.flags.dataset || this.flags.visibility || this.flags['dataset-default'] || this.isUnattended()) {
317
- showDefaultConfigPrompt = false;
318
- }
319
- const detectedFramework = await detectFrameworkRecord({
320
- frameworkList: frameworks,
321
- rootPath: process.cwd()
322
- });
323
- const isNextJs = detectedFramework?.slug === 'nextjs';
324
- let remoteTemplateInfo;
325
- if (this.flags.template && checkIsRemoteTemplate(this.flags.template)) {
326
- remoteTemplateInfo = await getGitHubRepoInfo(this.flags.template, this.flags['template-token']);
327
- }
328
- if (detectedFramework && detectedFramework.slug !== 'sanity' && remoteTemplateInfo) {
329
- this.error(`A remote template cannot be used with a detected framework. Detected: ${detectedFramework.name}`, {
330
- exit: 1
331
- });
332
- }
333
- const isAppTemplate = this.flags.template ? determineAppTemplate(this.flags.template) : false // Default to false
334
- ;
335
- // Checks flags are present when in unattended mode
336
- if (this.isUnattended()) {
337
- this.checkFlagsInUnattendedMode({
338
- createProjectName,
339
- isAppTemplate,
340
- isNextJs
341
- });
342
- }
343
- this._trace.start();
344
- this._trace.log({
345
- flags: {
346
- bare: this.flags.bare,
347
- coupon: this.flags.coupon,
348
- defaultConfig,
349
- env: this.flags.env,
350
- git: this.flags.git,
351
- plan: this.flags['project-plan'],
352
- reconfigure: this.flags.reconfigure,
353
- unattended: this.isUnattended()
354
- },
355
- step: 'start'
356
- });
357
- // Plan can be set through `--project-plan`, or implied through `--coupon`.
358
- // As coupons can expire and project plans might change/be removed, we need to
359
- // verify that the passed flags are valid. The complexity of this is hidden in the
360
- // below plan methods, eventually returning a plan ID or undefined if we are told to
361
- // use the default plan.
362
- const planId = await this.getPlan();
363
- let envFilenameDefault = '.env';
364
- if (detectedFramework && detectedFramework.slug === 'nextjs') {
365
- envFilenameDefault = '.env.local';
366
- }
367
- const envFilename = typeof this.flags.env === 'string' ? this.flags.env : envFilenameDefault;
368
- // If the user isn't already autenticated, make it so
369
- const { user } = await this.ensureAuthenticated();
370
- if (!isAppTemplate) {
371
- this.log(`${logSymbols.success} Fetching existing projects`);
372
- this.log('');
373
- }
374
- let newProject;
375
- if (createProjectName) {
376
- newProject = await this.createProjectFromName({
377
- createProjectName,
378
- planId,
379
- user
380
- });
381
- }
382
- const { datasetName, displayName, isFirstProject, organizationId, projectId } = await this.getProjectDetails({
383
- isAppTemplate,
384
- newProject,
385
- planId,
386
- showDefaultConfigPrompt,
387
- user
388
- });
389
- // If user doesn't want to output any template code
390
- if (this.flags.bare) {
391
- this.log(`${logSymbols.success} Below are your project details`);
392
- this.log('');
393
- this.log(`Project ID: ${styleText('cyan', projectId)}`);
394
- this.log(`Dataset: ${styleText('cyan', datasetName)}`);
395
- this.log(`\nYou can find your project on Sanity Manage — https://www.sanity.io/manage/project/${projectId}\n`);
396
- return;
397
- }
398
- let initNext = this.flagOrDefault('nextjs-add-config-files', false);
399
- if (isNextJs && this.promptForUndefinedFlag(this.flags['nextjs-add-config-files'])) {
400
- initNext = await promptForConfigFiles();
401
- }
402
- this._trace.log({
403
- detectedFramework: detectedFramework?.name,
404
- selectedOption: initNext ? 'yes' : 'no',
405
- step: 'useDetectedFramework'
406
- });
407
- const sluggedName = deburr(displayName.toLowerCase()).replaceAll(/\s+/g, '-').replaceAll(/[^a-z0-9-]/g, '');
408
- // add more frameworks to this as we add support for them
409
- // this is used to skip the getProjectInfo prompt
410
- const initFramework = initNext;
411
- // Gather project defaults based on environment
412
- const defaults = await getProjectDefaults({
413
- isPlugin: false,
414
- workDir
415
- });
416
- // Prompt the user for required information
417
- const outputPath = await this.getProjectOutputPath({
418
- initFramework,
419
- sluggedName,
420
- workDir
421
- });
422
- // Set up MCP integration (skip in non-production environments)
423
241
  let mcpMode = 'prompt';
424
242
  if (!this.flags.mcp || !this.resolveIsInteractive() || getSanityEnv() !== 'production') {
425
243
  mcpMode = 'skip';
426
244
  } else if (this.flags.yes) {
427
245
  mcpMode = 'auto';
428
246
  }
429
- const mcpResult = await setupMCP({
430
- mode: mcpMode
431
- });
432
- this._trace.log({
433
- configuredEditors: mcpResult.configuredEditors,
434
- detectedEditors: mcpResult.detectedEditors,
435
- skipped: mcpResult.skipped,
436
- step: 'mcpSetup'
437
- });
438
- if (mcpResult.error) {
439
- this._trace.error(mcpResult.error);
440
- }
441
- const mcpConfigured = mcpResult.configuredEditors;
442
- // Show checkmark for editors that were already configured
443
- const { alreadyConfiguredEditors } = mcpResult;
444
- if (alreadyConfiguredEditors.length > 0) {
445
- const label = alreadyConfiguredEditors.length === 1 ? `${alreadyConfiguredEditors[0]} already configured for Sanity MCP` : `${alreadyConfiguredEditors.length} editors already configured for Sanity MCP`;
446
- spinner(label).start().succeed();
447
- }
448
- if (isNextJs) {
449
- await checkNextJsReactCompatibility({
450
- detectedFramework,
451
- output: this.output,
452
- outputPath
453
- });
454
- }
455
- if (initNext) {
456
- await this.initNextJs({
457
- datasetName,
458
- detectedFramework,
459
- envFilename,
460
- mcpConfigured,
461
- projectId,
462
- workDir
463
- });
464
- }
465
- // user wants to write environment variables to file
466
- if (this.flags.env) {
467
- await createOrAppendEnvVars({
468
- envVars: {
469
- DATASET: datasetName,
470
- PROJECT_ID: projectId
471
- },
472
- filename: envFilename,
473
- framework: detectedFramework,
474
- log: false,
475
- output: this.output,
476
- outputPath
477
- });
478
- await this.writeStagingEnvIfNeeded(outputPath);
479
- this.exit(0);
480
- }
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(),
542
- output: this.output,
543
- 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,
553
- 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
- };
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
- }
651
- this._trace.complete();
652
- }
653
- checkFlagsInUnattendedMode({ createProjectName, isAppTemplate, isNextJs }) {
654
- debug('Unattended mode, validating required options');
655
- // App templates only require --organization and --output-path
656
- if (isAppTemplate) {
657
- if (!this.flags['output-path']) {
658
- this.error('`--output-path` must be specified in unattended mode', {
659
- exit: 1
660
- });
661
- }
662
- if (!this.flags.organization) {
663
- this.error('The --organization flag is required for app templates in unattended mode. ' + 'Use --organization <id> to specify which organization to use.', {
664
- exit: 1
665
- });
666
- }
667
- return;
668
- }
669
- // output-path is required in unattended mode when not using nextjs or bare
670
- if (!isNextJs && !this.flags.bare && !this.flags['output-path']) {
671
- this.error(`\`--output-path\` must be specified in unattended mode`, {
672
- exit: 1
673
- });
674
- }
675
- if (!this.flags.project && !createProjectName) {
676
- this.error('`--project <id>` or `--project-name <name>` must be specified in unattended mode', {
677
- exit: 1
678
- });
679
- }
680
- if (createProjectName && !this.flags.organization) {
681
- this.error('`--project-name` requires `--organization <id>` in unattended mode', {
682
- exit: 1
683
- });
684
- }
685
- }
686
- async createProjectFromName({ createProjectName, planId, user }) {
687
- debug('--project-name specified, creating a new project');
688
- let orgForCreateProjectFlag = this.flags.organization;
689
- if (!orgForCreateProjectFlag) {
690
- debug('no organization specified, selecting one');
691
- const organizations = await listOrganizations();
692
- orgForCreateProjectFlag = await this.promptUserForOrganization({
693
- organizations,
694
- user
695
- });
696
- }
697
- debug('creating a new project');
698
- const createdProject = await createProject({
699
- displayName: createProjectName.trim(),
700
- metadata: {
701
- coupon: this.flags.coupon
702
- },
703
- organizationId: orgForCreateProjectFlag,
704
- subscription: planId ? {
705
- planId
706
- } : undefined
707
- });
708
- debug('Project with ID %s created', createdProject.projectId);
709
- if (this.flags.dataset) {
710
- debug('--dataset specified, creating dataset (%s)', this.flags.dataset);
711
- const spin = spinner('Creating dataset').start();
712
- await createDatasetService({
713
- aclMode: this.flags.visibility,
714
- datasetName: this.flags.dataset,
715
- projectId: createdProject.projectId
716
- });
717
- spin.succeed();
718
- }
719
- return createdProject.projectId;
720
- }
721
- // @todo do we actually need to be authenticated for init? check flags and determine.
722
- async ensureAuthenticated() {
723
- const user = await validateSession();
724
- if (user) {
725
- this._trace.log({
726
- alreadyLoggedIn: true,
727
- step: 'login'
728
- });
729
- this.log(`${logSymbols.success} You are logged in as ${user.email} using ${getProviderName(user.provider)}`);
730
- return {
731
- user
732
- };
733
- }
734
- if (this.isUnattended()) {
735
- this.error('Must be logged in to run this command in unattended mode, run `sanity login`', {
736
- exit: 1
737
- });
738
- }
739
- this._trace.log({
740
- step: 'login'
741
- });
742
247
  try {
743
- await login({
248
+ await initAction(flagsToInitOptions(this.flags, this.isUnattended(), this.args, mcpMode), {
744
249
  output: this.output,
745
- telemetry: this._trace.newContext('login')
250
+ telemetry: this.telemetry,
251
+ workDir: process.cwd()
746
252
  });
747
253
  } catch (error) {
748
- const message = error instanceof Error ? error.message : String(error);
749
- this.error(`Login failed: ${message}`, {
750
- exit: 1
751
- });
752
- }
753
- const loggedInUser = await getCliUser();
754
- this.log(`${logSymbols.success} You are logged in as ${loggedInUser.email} using ${getProviderName(loggedInUser.provider)}`);
755
- return {
756
- user: loggedInUser
757
- };
758
- }
759
- flagOrDefault(flag, defaultValue) {
760
- return typeof this.flags[flag] === 'boolean' ? this.flags[flag] : defaultValue;
761
- }
762
- async getOrCreateDataset(opts) {
763
- const visibility = this.flags.visibility;
764
- const dataset = this.flags.dataset;
765
- let defaultConfig = this.flags['dataset-default'];
766
- if (dataset && this.isUnattended()) {
767
- return {
768
- datasetName: dataset,
769
- userAction: 'none'
770
- };
771
- }
772
- const [datasets, projectFeatures] = await Promise.all([
773
- listDatasets(opts.projectId),
774
- getProjectFeatures(opts.projectId)
775
- ]);
776
- if (dataset) {
777
- debug('User has specified dataset through a flag (%s)', dataset);
778
- const existing = datasets.find((ds)=>ds.name === dataset);
779
- if (!existing) {
780
- debug('Specified dataset not found, creating it');
781
- await createDataset({
782
- datasetName: dataset,
783
- forcePublic: defaultConfig,
784
- output: this.output,
785
- projectFeatures,
786
- projectId: opts.projectId,
787
- visibility
788
- });
789
- }
790
- return {
791
- datasetName: dataset,
792
- userAction: 'none'
793
- };
794
- }
795
- // In unattended mode without --dataset, default to "production" with public visibility
796
- // (same behavior as --dataset-default)
797
- if (this.isUnattended()) {
798
- debug('Unattended mode without --dataset, defaulting to "production" dataset');
799
- const datasetName = 'production';
800
- const existing = datasets.find((ds)=>ds.name === datasetName);
801
- if (!existing) {
802
- await createDataset({
803
- datasetName,
804
- forcePublic: visibility === undefined,
805
- isUnattended: true,
806
- output: this.output,
807
- projectFeatures,
808
- projectId: opts.projectId,
809
- visibility
810
- });
811
- }
812
- return {
813
- datasetName,
814
- userAction: existing ? 'none' : 'create'
815
- };
816
- }
817
- if (datasets.length === 0) {
818
- debug('No datasets found for project, prompting for name');
819
- if (opts.showDefaultConfigPrompt) {
820
- defaultConfig = await promptForDefaultConfig();
821
- }
822
- const name = defaultConfig ? 'production' : await promptForDatasetName({
823
- message: 'Name of your first dataset:'
824
- });
825
- await createDataset({
826
- datasetName: name,
827
- forcePublic: defaultConfig,
828
- output: this.output,
829
- projectFeatures,
830
- projectId: opts.projectId,
831
- visibility
832
- });
833
- return {
834
- datasetName: name,
835
- userAction: 'create'
836
- };
837
- }
838
- debug(`User has ${datasets.length} dataset(s) already, showing list of choices`);
839
- const datasetChoices = datasets.map((dataset)=>({
840
- value: dataset.name
841
- }));
842
- const selected = await select({
843
- choices: [
844
- {
845
- name: 'Create new dataset',
846
- value: 'new'
847
- },
848
- new Separator(),
849
- ...datasetChoices
850
- ],
851
- message: 'Select dataset to use'
852
- });
853
- if (selected === 'new') {
854
- const existingDatasetNames = datasets.map((ds)=>ds.name);
855
- debug('User wants to create a new dataset, prompting for name');
856
- if (opts.showDefaultConfigPrompt && !existingDatasetNames.includes('production')) {
857
- defaultConfig = await promptForDefaultConfig();
858
- }
859
- const newDatasetName = defaultConfig ? 'production' : await promptForDatasetName({
860
- message: 'Dataset name:'
861
- }, existingDatasetNames);
862
- await createDataset({
863
- datasetName: newDatasetName,
864
- forcePublic: defaultConfig,
865
- output: this.output,
866
- projectFeatures,
867
- projectId: opts.projectId,
868
- visibility
869
- });
870
- return {
871
- datasetName: newDatasetName,
872
- userAction: 'create'
873
- };
874
- }
875
- debug(`Returning selected dataset (${selected})`);
876
- return {
877
- datasetName: selected,
878
- userAction: 'select'
879
- };
880
- }
881
- async getOrCreateProject({ newProject, planId, user }) {
882
- const projectId = this.flags.project || newProject;
883
- const organizationId = this.flags.organization;
884
- let projects;
885
- let organizations;
886
- try {
887
- const [allProjects, allOrgs] = await Promise.all([
888
- listProjects(),
889
- listOrganizations()
890
- ]);
891
- projects = allProjects.toSorted((a, b)=>b.createdAt.localeCompare(a.createdAt));
892
- organizations = allOrgs;
893
- } catch (err) {
894
- if (this.isUnattended() && projectId) {
895
- return {
896
- displayName: 'Unknown project',
897
- isFirstProject: false,
898
- projectId,
899
- userAction: 'select'
900
- };
901
- }
902
- this.error(`Failed to communicate with the Sanity API:\n${err.message}`, {
903
- exit: 1
904
- });
905
- }
906
- if (projects.length === 0 && this.isUnattended()) {
907
- this.error('No projects found for current user', {
908
- exit: 1
909
- });
910
- }
911
- if (projectId) {
912
- const project = projects.find((proj)=>proj.id === projectId);
913
- if (!project && !this.isUnattended()) {
914
- this.error(`Given project ID (${projectId}) not found, or you do not have access to it`, {
915
- exit: 1
916
- });
917
- }
918
- return {
919
- displayName: project ? project.displayName : 'Unknown project',
920
- isFirstProject: false,
921
- projectId,
922
- userAction: 'select'
923
- };
924
- }
925
- if (organizationId) {
926
- const organization = organizations.find((org)=>org.id === organizationId) || organizations.find((org)=>org.slug === organizationId);
927
- if (!organization) {
928
- this.error(`Given organization ID (${organizationId}) not found, or you do not have access to it`, {
929
- exit: 1
930
- });
931
- }
932
- if (!await hasProjectAttachGrant(organizationId)) {
933
- this.error('You lack the necessary permissions to attach a project to this organization', {
934
- exit: 1
935
- });
936
- }
937
- }
938
- // If the user has no projects or is using a coupon (which can only be applied to new projects)
939
- // just ask for project details instead of showing a list of projects
940
- const isUsersFirstProject = projects.length === 0;
941
- if (isUsersFirstProject || this.flags.coupon) {
942
- debug(isUsersFirstProject ? 'No projects found for user, prompting for name' : 'Using a coupon - skipping project selection');
943
- const newProject = await this.promptForProjectCreation({
944
- isUsersFirstProject,
945
- organizationId,
946
- organizations,
947
- planId,
948
- user
949
- });
950
- return {
951
- ...newProject,
952
- isFirstProject: isUsersFirstProject,
953
- userAction: 'create'
954
- };
955
- }
956
- debug(`User has ${projects.length} project(s) already, showing list of choices`);
957
- const projectChoices = projects.map((project)=>({
958
- name: `${project.displayName} (${project.id})`,
959
- value: project.id
960
- }));
961
- const selected = await select({
962
- choices: [
963
- {
964
- name: 'Create new project',
965
- value: 'new'
966
- },
967
- new Separator(),
968
- ...projectChoices
969
- ],
970
- message: 'Create a new project or select an existing one'
971
- });
972
- if (selected === 'new') {
973
- debug('User wants to create a new project, prompting for name');
974
- const newProject = await this.promptForProjectCreation({
975
- isUsersFirstProject,
976
- organizationId,
977
- organizations,
978
- planId,
979
- user
980
- });
981
- return {
982
- ...newProject,
983
- isFirstProject: isUsersFirstProject,
984
- userAction: 'create'
985
- };
986
- }
987
- debug(`Returning selected project (${selected})`);
988
- return {
989
- displayName: projects.find((proj)=>proj.id === selected)?.displayName || '',
990
- isFirstProject: isUsersFirstProject,
991
- projectId: selected,
992
- userAction: 'select'
993
- };
994
- }
995
- async getPlan() {
996
- const intendedPlan = this.flags['project-plan'];
997
- const intendedCoupon = this.flags.coupon;
998
- if (intendedCoupon) {
999
- return this.verifyCoupon(intendedCoupon);
1000
- } else if (intendedPlan) {
1001
- return this.verifyPlan(intendedPlan);
1002
- } else {
1003
- return undefined;
1004
- }
1005
- }
1006
- async getPostInitMCPPrompt(editorsNames) {
1007
- return fetchPostInitPrompt(new Intl.ListFormat('en').format(editorsNames));
1008
- }
1009
- async getProjectDetails({ isAppTemplate, newProject, planId, showDefaultConfigPrompt, user }) {
1010
- 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
- };
1020
- }
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,
1030
- user
1031
- });
1032
- return {
1033
- datasetName: '',
1034
- displayName: '',
1035
- isFirstProject: false,
1036
- organizationId: appOrganizationId,
1037
- projectId: ''
1038
- };
1039
- }
1040
- debug('Prompting user to select or create a project');
1041
- const project = await this.getOrCreateProject({
1042
- newProject,
1043
- planId,
1044
- user
1045
- });
1046
- debug(`Project with name ${project.displayName} selected`);
1047
- // Now let's pick or create a dataset
1048
- debug('Prompting user to select or create a dataset');
1049
- const dataset = await this.getOrCreateDataset({
1050
- displayName: project.displayName,
1051
- projectId: project.projectId,
1052
- showDefaultConfigPrompt
1053
- });
1054
- debug(`Dataset with name ${dataset.datasetName} selected`);
1055
- this._trace.log({
1056
- datasetName: dataset.datasetName,
1057
- selectedOption: dataset.userAction,
1058
- step: 'createOrSelectDataset',
1059
- visibility: this.flags.visibility
1060
- });
1061
- return {
1062
- datasetName: dataset.datasetName,
1063
- displayName: project.displayName,
1064
- isFirstProject: project.isFirstProject,
1065
- projectId: project.projectId
1066
- };
1067
- }
1068
- async getProjectOutputPath({ initFramework, sluggedName, workDir }) {
1069
- const outputPath = this.flags['output-path'];
1070
- const specifiedPath = outputPath && path.resolve(outputPath);
1071
- if (this.isUnattended() || specifiedPath || this.flags.env || initFramework) {
1072
- return specifiedPath || workDir;
1073
- }
1074
- const inputPath = await input({
1075
- default: path.join(workDir, sluggedName),
1076
- message: 'Project output path:',
1077
- validate: validateEmptyPath
1078
- });
1079
- return absolutify(inputPath);
1080
- }
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
- }
1112
- }
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);
1139
- }
1140
- if (appendEnv) {
1141
- await createOrAppendEnvVars({
1142
- envVars: {
1143
- DATASET: datasetName,
1144
- PROJECT_ID: projectId
1145
- },
1146
- filename: envFilename,
1147
- framework: detectedFramework,
1148
- log: true,
1149
- output: this.output,
1150
- outputPath: workDir
1151
- });
1152
- }
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
- this._trace.log({
1180
- selectedOption: chosen,
1181
- step: 'selectPackageManager'
1182
- });
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'
1205
- };
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?'
1257
- });
1258
- }
1259
- async promptForProjectCreation({ isUsersFirstProject, organizationId, organizations, planId, user }) {
1260
- const projectName = await input({
1261
- default: 'My Sanity Project',
1262
- message: 'Project name:',
1263
- validate (input) {
1264
- if (!input || input.trim() === '') {
1265
- return 'Project name cannot be empty';
1266
- }
1267
- if (input.length > 80) {
1268
- return 'Project name cannot be longer than 80 characters';
1269
- }
1270
- return true;
1271
- }
1272
- });
1273
- const organization = organizationId || await this.promptUserForOrganization({
1274
- organizations,
1275
- user
1276
- });
1277
- const newProject = await createProject({
1278
- displayName: projectName,
1279
- metadata: {
1280
- coupon: this.flags.coupon
1281
- },
1282
- organizationId: organization,
1283
- subscription: planId ? {
1284
- planId
1285
- } : undefined
1286
- });
1287
- return {
1288
- ...newProject,
1289
- isFirstProject: isUsersFirstProject,
1290
- userAction: 'create'
1291
- };
1292
- }
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
- async promptUserForNewOrganization(user) {
1325
- const name = await promptForOrganizationName(user);
1326
- const spin = spinner('Creating organization').start();
1327
- const organization = await createOrganization(name);
1328
- spin.succeed();
1329
- return organization;
1330
- }
1331
- async promptUserForOrganization({ isAppTemplate = false, organizations, user }) {
1332
- // If the user has no organizations, prompt them to create one with the same name as
1333
- // their user, but allow them to customize it if they want
1334
- if (organizations.length === 0) {
1335
- const newOrganization = await this.promptUserForNewOrganization(user);
1336
- return newOrganization.id;
1337
- }
1338
- let organizationChoices;
1339
- let defaultOrganizationId;
1340
- if (isAppTemplate) {
1341
- // For app templates, all organizations are valid — no attach grant check needed
1342
- organizationChoices = getOrganizationChoices(organizations);
1343
- defaultOrganizationId = organizations.length === 1 ? organizations[0].id : findOrganizationByUserName(organizations, user);
1344
- } else {
1345
- // For studio projects, check which organizations the user can attach projects to
1346
- debug(`User has ${organizations.length} organization(s), checking attach access`);
1347
- const withGrantInfo = await getOrganizationsWithAttachGrantInfo(organizations);
1348
- const withAttach = withGrantInfo.filter(({ hasAttachGrant })=>hasAttachGrant);
1349
- debug('User has attach access to %d organizations.', withAttach.length);
1350
- organizationChoices = getOrganizationChoices(withGrantInfo);
1351
- defaultOrganizationId = withAttach.length === 1 ? withAttach[0].organization.id : findOrganizationByUserName(organizations, user);
1352
- }
1353
- const chosenOrg = await select({
1354
- choices: organizationChoices,
1355
- default: defaultOrganizationId || undefined,
1356
- message: 'Select organization:'
1357
- });
1358
- if (chosenOrg === '-new-') {
1359
- const newOrganization = await this.promptUserForNewOrganization(user);
1360
- return newOrganization.id;
1361
- }
1362
- return chosenOrg || undefined;
1363
- }
1364
- async verifyCoupon(intendedCoupon) {
1365
- try {
1366
- const planId = await getPlanIdFromCoupon(intendedCoupon);
1367
- this.log(`Coupon "${intendedCoupon}" validated!\n`);
1368
- return planId;
1369
- } catch (err) {
1370
- if (!isHttpError(err) || err.statusCode !== 404) {
1371
- const message = err instanceof Error ? err.message : `${err}`;
1372
- this.error(`Unable to validate coupon, please try again later:\n\n${message}`, {
1373
- exit: 1
1374
- });
1375
- }
1376
- const useDefaultPlan = this.isUnattended() || await confirm({
1377
- default: true,
1378
- message: `Coupon "${intendedCoupon}" is not available, use default plan instead?`
1379
- });
1380
- if (this.isUnattended()) {
1381
- this.warn(`Coupon "${intendedCoupon}" is not available - using default plan`);
1382
- }
1383
- this._trace.log({
1384
- coupon: intendedCoupon,
1385
- selectedOption: useDefaultPlan ? 'yes' : 'no',
1386
- step: 'useDefaultPlanCoupon'
1387
- });
1388
- if (useDefaultPlan) {
1389
- this.log('Using default plan.');
1390
- } else {
1391
- this.error(`Coupon "${intendedCoupon}" does not exist`, {
1392
- exit: 1
254
+ if (error instanceof InitError) {
255
+ this.error(error.message, {
256
+ exit: error.exitCode
1393
257
  });
1394
258
  }
259
+ throw error;
1395
260
  }
1396
261
  }
1397
- async verifyPlan(intendedPlan) {
1398
- try {
1399
- const planId = await getPlanId(intendedPlan);
1400
- return planId;
1401
- } catch (err) {
1402
- if (!isHttpError(err) || err.statusCode !== 404) {
1403
- const message = err instanceof Error ? err.message : `${err}`;
1404
- this.error(`Unable to validate plan, please try again later:\n\n${message}`, {
1405
- exit: 1
1406
- });
1407
- }
1408
- const useDefaultPlan = this.isUnattended() || await confirm({
1409
- default: true,
1410
- message: `Project plan "${intendedPlan}" does not exist, use default plan instead?`
1411
- });
1412
- if (this.isUnattended()) {
1413
- this.warn(`Project plan "${intendedPlan}" does not exist - using default plan`);
1414
- }
1415
- this._trace.log({
1416
- planId: intendedPlan,
1417
- selectedOption: useDefaultPlan ? 'yes' : 'no',
1418
- step: 'useDefaultPlanId'
1419
- });
1420
- if (useDefaultPlan) {
1421
- this.log('Using default plan.');
1422
- } else {
1423
- this.error(`Plan id "${intendedPlan}" does not exist`, {
1424
- exit: 1
1425
- });
1426
- }
1427
- }
1428
- }
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
262
  }
1496
263
 
1497
264
  //# sourceMappingURL=init.js.map