@sanity/cli 6.4.0 → 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 (99) hide show
  1. package/README.md +14 -8
  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/bootstrapTemplate.js.map +1 -1
  32. package/dist/actions/init/checkNextJsReactCompatibility.js +3 -3
  33. package/dist/actions/init/checkNextJsReactCompatibility.js.map +1 -1
  34. package/dist/actions/init/initAction.js +287 -0
  35. package/dist/actions/init/initAction.js.map +1 -0
  36. package/dist/actions/init/initApp.js +7 -16
  37. package/dist/actions/init/initApp.js.map +1 -1
  38. package/dist/actions/init/initError.js +10 -0
  39. package/dist/actions/init/initError.js.map +1 -0
  40. package/dist/actions/init/initHelpers.js +3 -12
  41. package/dist/actions/init/initHelpers.js.map +1 -1
  42. package/dist/actions/init/initNextJs.js +17 -20
  43. package/dist/actions/init/initNextJs.js.map +1 -1
  44. package/dist/actions/init/initStudio.js +11 -20
  45. package/dist/actions/init/initStudio.js.map +1 -1
  46. package/dist/actions/init/plan/getPlan.js +15 -0
  47. package/dist/actions/init/plan/getPlan.js.map +1 -0
  48. package/dist/actions/init/plan/verifyCoupon.js +35 -0
  49. package/dist/actions/init/plan/verifyCoupon.js.map +1 -0
  50. package/dist/actions/init/plan/verifyPlan.js +34 -0
  51. package/dist/actions/init/plan/verifyPlan.js.map +1 -0
  52. package/dist/actions/init/project/createProjectFromName.js +44 -0
  53. package/dist/actions/init/project/createProjectFromName.js.map +1 -0
  54. package/dist/actions/init/project/getOrCreateDataset.js +126 -0
  55. package/dist/actions/init/project/getOrCreateDataset.js.map +1 -0
  56. package/dist/actions/init/project/getOrCreateProject.js +128 -0
  57. package/dist/actions/init/project/getOrCreateProject.js.map +1 -0
  58. package/dist/actions/init/project/getProjectDetails.js +87 -0
  59. package/dist/actions/init/project/getProjectDetails.js.map +1 -0
  60. package/dist/actions/init/project/getProjectOutputPath.js +17 -0
  61. package/dist/actions/init/project/getProjectOutputPath.js.map +1 -0
  62. package/dist/actions/init/project/promptForAppTemplateSetup.js +112 -0
  63. package/dist/actions/init/project/promptForAppTemplateSetup.js.map +1 -0
  64. package/dist/actions/init/project/promptForProjectCreation.js +40 -0
  65. package/dist/actions/init/project/promptForProjectCreation.js.map +1 -0
  66. package/dist/actions/init/project/promptUserForNewOrganization.js +12 -0
  67. package/dist/actions/init/project/promptUserForNewOrganization.js.map +1 -0
  68. package/dist/actions/init/project/promptUserForOrganization.js +38 -0
  69. package/dist/actions/init/project/promptUserForOrganization.js.map +1 -0
  70. package/dist/actions/init/scaffoldTemplate.js +23 -29
  71. package/dist/actions/init/scaffoldTemplate.js.map +1 -1
  72. package/dist/actions/init/types.js +47 -1
  73. package/dist/actions/init/types.js.map +1 -1
  74. package/dist/actions/manifest/types.js +0 -1
  75. package/dist/actions/manifest/types.js.map +1 -1
  76. package/dist/actions/versions/buildPackageArray.js +2 -2
  77. package/dist/actions/versions/buildPackageArray.js.map +1 -1
  78. package/dist/actions/versions/findSanityModulesVersions.js +3 -3
  79. package/dist/actions/versions/findSanityModulesVersions.js.map +1 -1
  80. package/dist/commands/datasets/copy.js.map +1 -1
  81. package/dist/commands/init.js +11 -911
  82. package/dist/commands/init.js.map +1 -1
  83. package/dist/server/vite/plugin-sanity-build-entries.js +2 -1
  84. package/dist/server/vite/plugin-sanity-build-entries.js.map +1 -1
  85. package/dist/services/datasets.js.map +1 -1
  86. package/dist/telemetry/init.telemetry.js.map +1 -1
  87. package/dist/util/compareDependencyVersions.js +4 -4
  88. package/dist/util/compareDependencyVersions.js.map +1 -1
  89. package/dist/util/createExpiringConfig.js +1 -1
  90. package/dist/util/createExpiringConfig.js.map +1 -1
  91. package/dist/util/packageManager/installationInfo/analyzeIssues.js +7 -7
  92. package/dist/util/packageManager/installationInfo/analyzeIssues.js.map +1 -1
  93. package/dist/util/packageManager/installationInfo/types.js.map +1 -1
  94. package/dist/util/packageManager/packageManagerChoice.js +2 -2
  95. package/dist/util/packageManager/packageManagerChoice.js.map +1 -1
  96. package/dist/util/packageManager/preferredPm.js +106 -0
  97. package/dist/util/packageManager/preferredPm.js.map +1 -0
  98. package/oclif.manifest.json +526 -526
  99. package/package.json +23 -22
@@ -1,45 +1,10 @@
1
- import path from 'node:path';
2
- import { styleText } from 'node:util';
3
1
  import { Args, Flags } from '@oclif/core';
4
2
  import { CLIError } from '@oclif/core/errors';
5
- import { SanityCommand, subdebug } from '@sanity/cli-core';
6
- import { confirm, input, logSymbols, select, Separator, spinner } from '@sanity/cli-core/ux';
7
- import { isHttpError } from '@sanity/client';
8
- import { frameworks } from '@vercel/frameworks';
9
- import deburr from 'lodash-es/deburr.js';
10
- import { validateSession } from '../actions/auth/ensureAuthenticated.js';
11
- import { getProviderName } from '../actions/auth/getProviderName.js';
12
- import { login } from '../actions/auth/login/login.js';
13
- import { createDataset } from '../actions/dataset/create.js';
14
- import { checkNextJsReactCompatibility } from '../actions/init/checkNextJsReactCompatibility.js';
15
- import { determineAppTemplate } from '../actions/init/determineAppTemplate.js';
16
- import { createOrAppendEnvVars } from '../actions/init/env/createOrAppendEnvVars.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';
21
- import { checkIsRemoteTemplate, getGitHubRepoInfo } from '../actions/init/remoteTemplate.js';
22
- import { setupMCP } from '../actions/mcp/setupMCP.js';
23
- import { findOrganizationByUserName } from '../actions/organizations/findOrganizationByUserName.js';
24
- import { getOrganizationChoices } from '../actions/organizations/getOrganizationChoices.js';
25
- import { getOrganizationsWithAttachGrantInfo } from '../actions/organizations/getOrganizationsWithAttachGrantInfo.js';
26
- import { hasProjectAttachGrant } from '../actions/organizations/hasProjectAttachGrant.js';
27
- import { promptForConfigFiles } from '../prompts/init/nextjs.js';
28
- import { promptForDatasetName } from '../prompts/promptForDatasetName.js';
29
- import { promptForDefaultConfig } from '../prompts/promptForDefaultConfig.js';
30
- import { promptForOrganizationName } from '../prompts/promptForOrganizationName.js';
31
- import { createDataset as createDatasetService, listDatasets } from '../services/datasets.js';
32
- import { getProjectFeatures } from '../services/getProjectFeatures.js';
33
- import { createOrganization, listOrganizations } from '../services/organizations.js';
34
- import { getPlanId, getPlanIdFromCoupon } from '../services/plans.js';
35
- import { createProject, listProjects } from '../services/projects.js';
36
- import { getCliUser } from '../services/user.js';
37
- import { CLIInitStepCompleted } from '../telemetry/init.telemetry.js';
38
- import { detectFrameworkRecord } from '../util/detectFramework.js';
39
- import { absolutify, validateEmptyPath } from '../util/fsUtils.js';
40
- 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';
41
7
  import { getSanityEnv } from '../util/getSanityEnv.js';
42
- const debug = subdebug('init');
43
8
  export class InitCommand extends SanityCommand {
44
9
  static args = {
45
10
  type: Args.string({
@@ -272,891 +237,26 @@ export class InitCommand extends SanityCommand {
272
237
  description: 'Unattended mode, answers "yes" to any "yes/no" prompt and otherwise uses defaults'
273
238
  })
274
239
  };
275
- _trace;
276
240
  async run() {
277
- const workDir = process.cwd();
278
- const createProjectName = this.flags['project-name'] ?? this.flags['create-project'];
279
- // For backwards "compatibility" - we used to allow `sanity init plugin`,
280
- // and no longer do - but instead of printing an error about an unknown
281
- // _command_, we want to acknowledge that the user is trying to do something
282
- // that no longer exists but might have at some point in the past.
283
- if (this.args.type) {
284
- this.error(this.args.type === 'plugin' ? 'Initializing plugins through the CLI is no longer supported' : `Unknown init type "${this.args.type}"`, {
285
- exit: 1
286
- });
287
- }
288
- this._trace = this.telemetry.trace(CLIInitStepCompleted);
289
- // Slightly more helpful message for removed flags rather than just saying the flag
290
- // does not exist.
291
- if (this.flags.reconfigure) {
292
- this.error('--reconfigure is deprecated - manual configuration is now required', {
293
- exit: 1
294
- });
295
- }
296
- // Oclif doesn't support custom exclusive error messaging
297
- if (this.flags.project && this.flags.organization) {
298
- 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', {
299
- exit: 1
300
- });
301
- }
302
- const defaultConfig = this.flags['dataset-default'];
303
- let showDefaultConfigPrompt = !defaultConfig;
304
- if (this.flags.dataset || this.flags.visibility || this.flags['dataset-default'] || this.isUnattended()) {
305
- showDefaultConfigPrompt = false;
306
- }
307
- const detectedFramework = await detectFrameworkRecord({
308
- frameworkList: frameworks,
309
- rootPath: process.cwd()
310
- });
311
- const isNextJs = detectedFramework?.slug === 'nextjs';
312
- let remoteTemplateInfo;
313
- if (this.flags.template && checkIsRemoteTemplate(this.flags.template)) {
314
- remoteTemplateInfo = await getGitHubRepoInfo(this.flags.template, this.flags['template-token']);
315
- }
316
- if (detectedFramework && detectedFramework.slug !== 'sanity' && remoteTemplateInfo) {
317
- this.error(`A remote template cannot be used with a detected framework. Detected: ${detectedFramework.name}`, {
318
- exit: 1
319
- });
320
- }
321
- const isAppTemplate = this.flags.template ? determineAppTemplate(this.flags.template) : false // Default to false
322
- ;
323
- // Checks flags are present when in unattended mode
324
- if (this.isUnattended()) {
325
- this.checkFlagsInUnattendedMode({
326
- createProjectName,
327
- isAppTemplate,
328
- isNextJs
329
- });
330
- }
331
- this._trace.start();
332
- this._trace.log({
333
- flags: {
334
- bare: this.flags.bare,
335
- coupon: this.flags.coupon,
336
- defaultConfig,
337
- env: this.flags.env,
338
- git: this.flags.git,
339
- plan: this.flags['project-plan'],
340
- reconfigure: this.flags.reconfigure,
341
- unattended: this.isUnattended()
342
- },
343
- step: 'start'
344
- });
345
- // Plan can be set through `--project-plan`, or implied through `--coupon`.
346
- // As coupons can expire and project plans might change/be removed, we need to
347
- // verify that the passed flags are valid. The complexity of this is hidden in the
348
- // below plan methods, eventually returning a plan ID or undefined if we are told to
349
- // use the default plan.
350
- const planId = await this.getPlan();
351
- let envFilenameDefault = '.env';
352
- if (detectedFramework && detectedFramework.slug === 'nextjs') {
353
- envFilenameDefault = '.env.local';
354
- }
355
- const envFilename = typeof this.flags.env === 'string' ? this.flags.env : envFilenameDefault;
356
- // If the user isn't already autenticated, make it so
357
- const { user } = await this.ensureAuthenticated();
358
- if (!isAppTemplate) {
359
- this.log(`${logSymbols.success} Fetching existing projects`);
360
- this.log('');
361
- }
362
- let newProject;
363
- if (createProjectName) {
364
- newProject = await this.createProjectFromName({
365
- createProjectName,
366
- planId,
367
- user
368
- });
369
- }
370
- const { datasetName, displayName, isFirstProject, organizationId, projectId } = await this.getProjectDetails({
371
- isAppTemplate,
372
- newProject,
373
- planId,
374
- showDefaultConfigPrompt,
375
- user
376
- });
377
- // If user doesn't want to output any template code
378
- if (this.flags.bare) {
379
- this.log(`${logSymbols.success} Below are your project details`);
380
- this.log('');
381
- this.log(`Project ID: ${styleText('cyan', projectId)}`);
382
- this.log(`Dataset: ${styleText('cyan', datasetName)}`);
383
- this.log(`\nYou can find your project on Sanity Manage — https://www.sanity.io/manage/project/${projectId}\n`);
384
- return;
385
- }
386
- let initNext = flagOrDefault(this.flags['nextjs-add-config-files'], false);
387
- if (isNextJs && shouldPrompt(this.isUnattended(), this.flags['nextjs-add-config-files'])) {
388
- initNext = await promptForConfigFiles();
389
- }
390
- this._trace.log({
391
- detectedFramework: detectedFramework?.name,
392
- selectedOption: initNext ? 'yes' : 'no',
393
- step: 'useDetectedFramework'
394
- });
395
- const sluggedName = deburr(displayName.toLowerCase()).replaceAll(/\s+/g, '-').replaceAll(/[^a-z0-9-]/g, '');
396
- // add more frameworks to this as we add support for them
397
- // this is used to skip the getProjectInfo prompt
398
- const initFramework = initNext;
399
- // Gather project defaults based on environment
400
- const defaults = await getProjectDefaults({
401
- isPlugin: false,
402
- workDir
403
- });
404
- // Prompt the user for required information
405
- const outputPath = await this.getProjectOutputPath({
406
- initFramework,
407
- sluggedName,
408
- workDir
409
- });
410
- // Set up MCP integration (skip in non-production environments)
411
241
  let mcpMode = 'prompt';
412
242
  if (!this.flags.mcp || !this.resolveIsInteractive() || getSanityEnv() !== 'production') {
413
243
  mcpMode = 'skip';
414
244
  } else if (this.flags.yes) {
415
245
  mcpMode = 'auto';
416
246
  }
417
- const mcpResult = await setupMCP({
418
- mode: mcpMode
419
- });
420
- this._trace.log({
421
- configuredEditors: mcpResult.configuredEditors,
422
- detectedEditors: mcpResult.detectedEditors,
423
- skipped: mcpResult.skipped,
424
- step: 'mcpSetup'
425
- });
426
- if (mcpResult.error) {
427
- this._trace.error(mcpResult.error);
428
- }
429
- const mcpConfigured = mcpResult.configuredEditors;
430
- // Show checkmark for editors that were already configured
431
- const { alreadyConfiguredEditors } = mcpResult;
432
- if (alreadyConfiguredEditors.length > 0) {
433
- const label = alreadyConfiguredEditors.length === 1 ? `${alreadyConfiguredEditors[0]} already configured for Sanity MCP` : `${alreadyConfiguredEditors.length} editors already configured for Sanity MCP`;
434
- spinner(label).start().succeed();
435
- }
436
- if (isNextJs) {
437
- await checkNextJsReactCompatibility({
438
- detectedFramework,
439
- output: this.output,
440
- outputPath
441
- });
442
- }
443
- if (initNext) {
444
- await initNextJs({
445
- datasetName,
446
- detectedFramework,
447
- envFilename,
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'],
454
- projectId,
455
- template: this.flags.template,
456
- trace: this._trace,
457
- typescript: this.flags.typescript,
458
- unattended: this.isUnattended(),
459
- workDir
460
- });
461
- this._trace.complete();
462
- return;
463
- }
464
- // user wants to write environment variables to file
465
- if (this.flags.env) {
466
- await createOrAppendEnvVars({
467
- envVars: {
468
- DATASET: datasetName,
469
- PROJECT_ID: projectId
470
- },
471
- filename: envFilename,
472
- framework: detectedFramework,
473
- log: false,
474
- output: this.output,
475
- outputPath
476
- });
477
- await writeStagingEnvIfNeeded(this.output, outputPath);
478
- this.exit(0);
479
- }
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,
488
- output: this.output,
489
- outputPath,
490
- overwriteFiles: this.flags['overwrite-files'],
491
- packageManager: this.flags['package-manager'],
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(),
499
- workDir
500
- };
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
- }));
513
- this._trace.complete();
514
- }
515
- checkFlagsInUnattendedMode({ createProjectName, isAppTemplate, isNextJs }) {
516
- debug('Unattended mode, validating required options');
517
- // App templates only require --organization and --output-path
518
- if (isAppTemplate) {
519
- if (!this.flags['output-path']) {
520
- this.error('`--output-path` must be specified in unattended mode', {
521
- exit: 1
522
- });
523
- }
524
- if (!this.flags.organization) {
525
- this.error('The --organization flag is required for app templates in unattended mode. ' + 'Use --organization <id> to specify which organization to use.', {
526
- exit: 1
527
- });
528
- }
529
- return;
530
- }
531
- // output-path is required in unattended mode when not using nextjs or bare
532
- if (!isNextJs && !this.flags.bare && !this.flags['output-path']) {
533
- this.error(`\`--output-path\` must be specified in unattended mode`, {
534
- exit: 1
535
- });
536
- }
537
- if (!this.flags.project && !createProjectName) {
538
- this.error('`--project <id>` or `--project-name <name>` must be specified in unattended mode', {
539
- exit: 1
540
- });
541
- }
542
- if (createProjectName && !this.flags.organization) {
543
- this.error('`--project-name` requires `--organization <id>` in unattended mode', {
544
- exit: 1
545
- });
546
- }
547
- }
548
- async createProjectFromName({ createProjectName, planId, user }) {
549
- debug('--project-name specified, creating a new project');
550
- let orgForCreateProjectFlag = this.flags.organization;
551
- if (!orgForCreateProjectFlag) {
552
- debug('no organization specified, selecting one');
553
- const organizations = await listOrganizations();
554
- orgForCreateProjectFlag = await this.promptUserForOrganization({
555
- organizations,
556
- user
557
- });
558
- }
559
- debug('creating a new project');
560
- const createdProject = await createProject({
561
- displayName: createProjectName.trim(),
562
- metadata: {
563
- coupon: this.flags.coupon
564
- },
565
- organizationId: orgForCreateProjectFlag,
566
- subscription: planId ? {
567
- planId
568
- } : undefined
569
- });
570
- debug('Project with ID %s created', createdProject.projectId);
571
- if (this.flags.dataset) {
572
- debug('--dataset specified, creating dataset (%s)', this.flags.dataset);
573
- const spin = spinner('Creating dataset').start();
574
- await createDatasetService({
575
- aclMode: this.flags.visibility,
576
- datasetName: this.flags.dataset,
577
- projectId: createdProject.projectId
578
- });
579
- spin.succeed();
580
- }
581
- return createdProject.projectId;
582
- }
583
- // @todo do we actually need to be authenticated for init? check flags and determine.
584
- async ensureAuthenticated() {
585
- const user = await validateSession();
586
- if (user) {
587
- this._trace.log({
588
- alreadyLoggedIn: true,
589
- step: 'login'
590
- });
591
- this.log(`${logSymbols.success} You are logged in as ${user.email} using ${getProviderName(user.provider)}`);
592
- return {
593
- user
594
- };
595
- }
596
- if (this.isUnattended()) {
597
- this.error('Must be logged in to run this command in unattended mode, run `sanity login`', {
598
- exit: 1
599
- });
600
- }
601
- this._trace.log({
602
- step: 'login'
603
- });
604
247
  try {
605
- await login({
248
+ await initAction(flagsToInitOptions(this.flags, this.isUnattended(), this.args, mcpMode), {
606
249
  output: this.output,
607
- telemetry: this._trace.newContext('login')
250
+ telemetry: this.telemetry,
251
+ workDir: process.cwd()
608
252
  });
609
253
  } catch (error) {
610
- const message = error instanceof Error ? error.message : String(error);
611
- this.error(`Login failed: ${message}`, {
612
- exit: 1
613
- });
614
- }
615
- const loggedInUser = await getCliUser();
616
- this.log(`${logSymbols.success} You are logged in as ${loggedInUser.email} using ${getProviderName(loggedInUser.provider)}`);
617
- return {
618
- user: loggedInUser
619
- };
620
- }
621
- async getOrCreateDataset(opts) {
622
- const visibility = this.flags.visibility;
623
- const dataset = this.flags.dataset;
624
- let defaultConfig = this.flags['dataset-default'];
625
- if (dataset && this.isUnattended()) {
626
- return {
627
- datasetName: dataset,
628
- userAction: 'none'
629
- };
630
- }
631
- const [datasets, projectFeatures] = await Promise.all([
632
- listDatasets(opts.projectId),
633
- getProjectFeatures(opts.projectId)
634
- ]);
635
- if (dataset) {
636
- debug('User has specified dataset through a flag (%s)', dataset);
637
- const existing = datasets.find((ds)=>ds.name === dataset);
638
- if (!existing) {
639
- debug('Specified dataset not found, creating it');
640
- await createDataset({
641
- datasetName: dataset,
642
- forcePublic: defaultConfig,
643
- output: this.output,
644
- projectFeatures,
645
- projectId: opts.projectId,
646
- visibility
647
- });
648
- }
649
- return {
650
- datasetName: dataset,
651
- userAction: 'none'
652
- };
653
- }
654
- // In unattended mode without --dataset, default to "production" with public visibility
655
- // (same behavior as --dataset-default)
656
- if (this.isUnattended()) {
657
- debug('Unattended mode without --dataset, defaulting to "production" dataset');
658
- const datasetName = 'production';
659
- const existing = datasets.find((ds)=>ds.name === datasetName);
660
- if (!existing) {
661
- await createDataset({
662
- datasetName,
663
- forcePublic: visibility === undefined,
664
- isUnattended: true,
665
- output: this.output,
666
- projectFeatures,
667
- projectId: opts.projectId,
668
- visibility
669
- });
670
- }
671
- return {
672
- datasetName,
673
- userAction: existing ? 'none' : 'create'
674
- };
675
- }
676
- if (datasets.length === 0) {
677
- debug('No datasets found for project, prompting for name');
678
- if (opts.showDefaultConfigPrompt) {
679
- defaultConfig = await promptForDefaultConfig();
680
- }
681
- const name = defaultConfig ? 'production' : await promptForDatasetName({
682
- message: 'Name of your first dataset:'
683
- });
684
- await createDataset({
685
- datasetName: name,
686
- forcePublic: defaultConfig,
687
- output: this.output,
688
- projectFeatures,
689
- projectId: opts.projectId,
690
- visibility
691
- });
692
- return {
693
- datasetName: name,
694
- userAction: 'create'
695
- };
696
- }
697
- debug(`User has ${datasets.length} dataset(s) already, showing list of choices`);
698
- const datasetChoices = datasets.map((dataset)=>({
699
- value: dataset.name
700
- }));
701
- const selected = await select({
702
- choices: [
703
- {
704
- name: 'Create new dataset',
705
- value: 'new'
706
- },
707
- new Separator(),
708
- ...datasetChoices
709
- ],
710
- message: 'Select dataset to use'
711
- });
712
- if (selected === 'new') {
713
- const existingDatasetNames = datasets.map((ds)=>ds.name);
714
- debug('User wants to create a new dataset, prompting for name');
715
- if (opts.showDefaultConfigPrompt && !existingDatasetNames.includes('production')) {
716
- defaultConfig = await promptForDefaultConfig();
717
- }
718
- const newDatasetName = defaultConfig ? 'production' : await promptForDatasetName({
719
- message: 'Dataset name:'
720
- }, existingDatasetNames);
721
- await createDataset({
722
- datasetName: newDatasetName,
723
- forcePublic: defaultConfig,
724
- output: this.output,
725
- projectFeatures,
726
- projectId: opts.projectId,
727
- visibility
728
- });
729
- return {
730
- datasetName: newDatasetName,
731
- userAction: 'create'
732
- };
733
- }
734
- debug(`Returning selected dataset (${selected})`);
735
- return {
736
- datasetName: selected,
737
- userAction: 'select'
738
- };
739
- }
740
- async getOrCreateProject({ newProject, planId, user }) {
741
- const projectId = this.flags.project || newProject;
742
- const organizationId = this.flags.organization;
743
- let projects;
744
- let organizations;
745
- try {
746
- const [allProjects, allOrgs] = await Promise.all([
747
- listProjects(),
748
- listOrganizations()
749
- ]);
750
- projects = allProjects.toSorted((a, b)=>b.createdAt.localeCompare(a.createdAt));
751
- organizations = allOrgs;
752
- } catch (err) {
753
- if (this.isUnattended() && projectId) {
754
- return {
755
- displayName: 'Unknown project',
756
- isFirstProject: false,
757
- projectId,
758
- userAction: 'select'
759
- };
760
- }
761
- this.error(`Failed to communicate with the Sanity API:\n${err.message}`, {
762
- exit: 1
763
- });
764
- }
765
- if (projects.length === 0 && this.isUnattended()) {
766
- this.error('No projects found for current user', {
767
- exit: 1
768
- });
769
- }
770
- if (projectId) {
771
- const project = projects.find((proj)=>proj.id === projectId);
772
- if (!project && !this.isUnattended()) {
773
- this.error(`Given project ID (${projectId}) not found, or you do not have access to it`, {
774
- exit: 1
775
- });
776
- }
777
- return {
778
- displayName: project ? project.displayName : 'Unknown project',
779
- isFirstProject: false,
780
- projectId,
781
- userAction: 'select'
782
- };
783
- }
784
- if (organizationId) {
785
- const organization = organizations.find((org)=>org.id === organizationId) || organizations.find((org)=>org.slug === organizationId);
786
- if (!organization) {
787
- this.error(`Given organization ID (${organizationId}) not found, or you do not have access to it`, {
788
- exit: 1
789
- });
790
- }
791
- if (!await hasProjectAttachGrant(organizationId)) {
792
- this.error('You lack the necessary permissions to attach a project to this organization', {
793
- exit: 1
794
- });
795
- }
796
- }
797
- // If the user has no projects or is using a coupon (which can only be applied to new projects)
798
- // just ask for project details instead of showing a list of projects
799
- const isUsersFirstProject = projects.length === 0;
800
- if (isUsersFirstProject || this.flags.coupon) {
801
- debug(isUsersFirstProject ? 'No projects found for user, prompting for name' : 'Using a coupon - skipping project selection');
802
- const newProject = await this.promptForProjectCreation({
803
- isUsersFirstProject,
804
- organizationId,
805
- organizations,
806
- planId,
807
- user
808
- });
809
- return {
810
- ...newProject,
811
- isFirstProject: isUsersFirstProject,
812
- userAction: 'create'
813
- };
814
- }
815
- debug(`User has ${projects.length} project(s) already, showing list of choices`);
816
- const projectChoices = projects.map((project)=>({
817
- name: `${project.displayName} (${project.id})`,
818
- value: project.id
819
- }));
820
- const selected = await select({
821
- choices: [
822
- {
823
- name: 'Create new project',
824
- value: 'new'
825
- },
826
- new Separator(),
827
- ...projectChoices
828
- ],
829
- message: 'Create a new project or select an existing one'
830
- });
831
- if (selected === 'new') {
832
- debug('User wants to create a new project, prompting for name');
833
- const newProject = await this.promptForProjectCreation({
834
- isUsersFirstProject,
835
- organizationId,
836
- organizations,
837
- planId,
838
- user
839
- });
840
- return {
841
- ...newProject,
842
- isFirstProject: isUsersFirstProject,
843
- userAction: 'create'
844
- };
845
- }
846
- debug(`Returning selected project (${selected})`);
847
- return {
848
- displayName: projects.find((proj)=>proj.id === selected)?.displayName || '',
849
- isFirstProject: isUsersFirstProject,
850
- projectId: selected,
851
- userAction: 'select'
852
- };
853
- }
854
- async getPlan() {
855
- const intendedPlan = this.flags['project-plan'];
856
- const intendedCoupon = this.flags.coupon;
857
- if (intendedCoupon) {
858
- return this.verifyCoupon(intendedCoupon);
859
- } else if (intendedPlan) {
860
- return this.verifyPlan(intendedPlan);
861
- } else {
862
- return undefined;
863
- }
864
- }
865
- async getProjectDetails({ isAppTemplate, newProject, planId, showDefaultConfigPrompt, user }) {
866
- if (isAppTemplate) {
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
- });
882
- }
883
- const { datasetName, displayName, projectId } = await this.promptForAppTemplateSetup({
884
- newProject,
885
- organizationId,
886
- planId,
887
- user
888
- });
889
- return {
890
- datasetName,
891
- displayName,
892
- isFirstProject: false,
893
- organizationId,
894
- projectId
895
- };
896
- }
897
- debug('Prompting user to select or create a project');
898
- const project = await this.getOrCreateProject({
899
- newProject,
900
- planId,
901
- user
902
- });
903
- debug(`Project with name ${project.displayName} selected`);
904
- // Now let's pick or create a dataset
905
- debug('Prompting user to select or create a dataset');
906
- const dataset = await this.getOrCreateDataset({
907
- displayName: project.displayName,
908
- projectId: project.projectId,
909
- showDefaultConfigPrompt
910
- });
911
- debug(`Dataset with name ${dataset.datasetName} selected`);
912
- this._trace.log({
913
- datasetName: dataset.datasetName,
914
- selectedOption: dataset.userAction,
915
- step: 'createOrSelectDataset',
916
- visibility: this.flags.visibility
917
- });
918
- return {
919
- datasetName: dataset.datasetName,
920
- displayName: project.displayName,
921
- isFirstProject: project.isFirstProject,
922
- projectId: project.projectId
923
- };
924
- }
925
- async getProjectOutputPath({ initFramework, sluggedName, workDir }) {
926
- const outputPath = this.flags['output-path'];
927
- const specifiedPath = outputPath && path.resolve(outputPath);
928
- if (this.isUnattended() || specifiedPath || this.flags.env || initFramework) {
929
- return specifiedPath || workDir;
930
- }
931
- const inputPath = await input({
932
- default: path.join(workDir, sluggedName),
933
- message: 'Project output path:',
934
- validate: validateEmptyPath
935
- });
936
- return absolutify(inputPath);
937
- }
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
- };
946
- }
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
- };
962
- }
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
975
- },
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'
991
- });
992
- return {
993
- datasetName: '',
994
- displayName: '',
995
- projectId: ''
996
- };
997
- }
998
- this._trace.log({
999
- selectedOption: selected === NEW_PROJECT ? 'create' : 'existing',
1000
- step: 'configureAppProject'
1001
- });
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
1011
- };
1012
- const dataset = await this.getOrCreateDataset({
1013
- displayName: project.displayName,
1014
- projectId: project.projectId,
1015
- showDefaultConfigPrompt: false
1016
- });
1017
- return {
1018
- datasetName: dataset.datasetName,
1019
- displayName: project.displayName,
1020
- projectId: project.projectId
1021
- };
1022
- }
1023
- async promptForProjectCreation({ isUsersFirstProject, organizationId, organizations, planId, user }) {
1024
- const projectName = await input({
1025
- default: 'My Sanity Project',
1026
- message: 'Project name:',
1027
- validate (input) {
1028
- if (!input || input.trim() === '') {
1029
- return 'Project name cannot be empty';
1030
- }
1031
- if (input.length > 80) {
1032
- return 'Project name cannot be longer than 80 characters';
1033
- }
1034
- return true;
1035
- }
1036
- });
1037
- const organization = organizationId || await this.promptUserForOrganization({
1038
- organizations,
1039
- user
1040
- });
1041
- const newProject = await createProject({
1042
- displayName: projectName,
1043
- metadata: {
1044
- coupon: this.flags.coupon
1045
- },
1046
- organizationId: organization,
1047
- subscription: planId ? {
1048
- planId
1049
- } : undefined
1050
- });
1051
- return {
1052
- ...newProject,
1053
- isFirstProject: isUsersFirstProject,
1054
- userAction: 'create'
1055
- };
1056
- }
1057
- async promptUserForNewOrganization(user) {
1058
- const name = await promptForOrganizationName(user);
1059
- const spin = spinner('Creating organization').start();
1060
- const organization = await createOrganization(name);
1061
- spin.succeed();
1062
- return organization;
1063
- }
1064
- async promptUserForOrganization({ isAppTemplate = false, organizations, user }) {
1065
- // If the user has no organizations, prompt them to create one with the same name as
1066
- // their user, but allow them to customize it if they want
1067
- if (organizations.length === 0) {
1068
- const newOrganization = await this.promptUserForNewOrganization(user);
1069
- return newOrganization.id;
1070
- }
1071
- let organizationChoices;
1072
- let defaultOrganizationId;
1073
- if (isAppTemplate) {
1074
- // For app templates, all organizations are valid — no attach grant check needed
1075
- organizationChoices = getOrganizationChoices(organizations);
1076
- defaultOrganizationId = organizations.length === 1 ? organizations[0].id : findOrganizationByUserName(organizations, user);
1077
- } else {
1078
- // For studio projects, check which organizations the user can attach projects to
1079
- debug(`User has ${organizations.length} organization(s), checking attach access`);
1080
- const withGrantInfo = await getOrganizationsWithAttachGrantInfo(organizations);
1081
- const withAttach = withGrantInfo.filter(({ hasAttachGrant })=>hasAttachGrant);
1082
- debug('User has attach access to %d organizations.', withAttach.length);
1083
- organizationChoices = getOrganizationChoices(withGrantInfo);
1084
- defaultOrganizationId = withAttach.length === 1 ? withAttach[0].organization.id : findOrganizationByUserName(organizations, user);
1085
- }
1086
- const chosenOrg = await select({
1087
- choices: organizationChoices,
1088
- default: defaultOrganizationId || undefined,
1089
- message: 'Select organization:'
1090
- });
1091
- if (chosenOrg === '-new-') {
1092
- const newOrganization = await this.promptUserForNewOrganization(user);
1093
- return newOrganization.id;
1094
- }
1095
- return chosenOrg || undefined;
1096
- }
1097
- async verifyCoupon(intendedCoupon) {
1098
- try {
1099
- const planId = await getPlanIdFromCoupon(intendedCoupon);
1100
- this.log(`Coupon "${intendedCoupon}" validated!\n`);
1101
- return planId;
1102
- } catch (err) {
1103
- if (!isHttpError(err) || err.statusCode !== 404) {
1104
- const message = err instanceof Error ? err.message : `${err}`;
1105
- this.error(`Unable to validate coupon, please try again later:\n\n${message}`, {
1106
- exit: 1
1107
- });
1108
- }
1109
- const useDefaultPlan = this.isUnattended() || await confirm({
1110
- default: true,
1111
- message: `Coupon "${intendedCoupon}" is not available, use default plan instead?`
1112
- });
1113
- if (this.isUnattended()) {
1114
- this.warn(`Coupon "${intendedCoupon}" is not available - using default plan`);
1115
- }
1116
- this._trace.log({
1117
- coupon: intendedCoupon,
1118
- selectedOption: useDefaultPlan ? 'yes' : 'no',
1119
- step: 'useDefaultPlanCoupon'
1120
- });
1121
- if (useDefaultPlan) {
1122
- this.log('Using default plan.');
1123
- } else {
1124
- this.error(`Coupon "${intendedCoupon}" does not exist`, {
1125
- exit: 1
1126
- });
1127
- }
1128
- }
1129
- }
1130
- async verifyPlan(intendedPlan) {
1131
- try {
1132
- const planId = await getPlanId(intendedPlan);
1133
- return planId;
1134
- } catch (err) {
1135
- if (!isHttpError(err) || err.statusCode !== 404) {
1136
- const message = err instanceof Error ? err.message : `${err}`;
1137
- this.error(`Unable to validate plan, please try again later:\n\n${message}`, {
1138
- exit: 1
1139
- });
1140
- }
1141
- const useDefaultPlan = this.isUnattended() || await confirm({
1142
- default: true,
1143
- message: `Project plan "${intendedPlan}" does not exist, use default plan instead?`
1144
- });
1145
- if (this.isUnattended()) {
1146
- this.warn(`Project plan "${intendedPlan}" does not exist - using default plan`);
1147
- }
1148
- this._trace.log({
1149
- planId: intendedPlan,
1150
- selectedOption: useDefaultPlan ? 'yes' : 'no',
1151
- step: 'useDefaultPlanId'
1152
- });
1153
- if (useDefaultPlan) {
1154
- this.log('Using default plan.');
1155
- } else {
1156
- this.error(`Plan id "${intendedPlan}" does not exist`, {
1157
- exit: 1
254
+ if (error instanceof InitError) {
255
+ this.error(error.message, {
256
+ exit: error.exitCode
1158
257
  });
1159
258
  }
259
+ throw error;
1160
260
  }
1161
261
  }
1162
262
  }