@sanity/cli 3.93.0 → 3.93.1-bump-pkg-utils.34.5d0c0b3d22

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 (160) hide show
  1. package/lib/_chunks-cjs/cli.js +5 -5
  2. package/lib/_chunks-cjs/cli.js.map +1 -1
  3. package/lib/_chunks-cjs/cliWorker.js.map +1 -1
  4. package/lib/_chunks-cjs/generateAction.js.map +1 -1
  5. package/lib/_chunks-cjs/getCliConfig.js.map +1 -1
  6. package/lib/_chunks-cjs/journeyConfig.js.map +1 -1
  7. package/lib/_chunks-cjs/loadEnv.js.map +1 -1
  8. package/lib/index.js.map +1 -1
  9. package/lib/index.mjs.map +1 -1
  10. package/lib/run.js.map +1 -1
  11. package/lib/workers/getAndWriteJourneySchema.js.map +1 -1
  12. package/lib/workers/getCliConfig.js.map +1 -1
  13. package/lib/workers/typegenGenerate.js.map +1 -1
  14. package/package.json +6 -7
  15. package/templates/blog/README.md +1 -1
  16. package/templates/clean/README.md +1 -1
  17. package/templates/ecommerce/README.md +1 -1
  18. package/templates/get-started/README.md +1 -1
  19. package/templates/moviedb/README.md +1 -1
  20. package/templates/quickstart/README.md +1 -1
  21. package/src/CommandRunner.ts +0 -219
  22. package/src/__telemetry__/cli.telemetry.ts +0 -29
  23. package/src/__telemetry__/init.telemetry.ts +0 -95
  24. package/src/actions/codemod/codemodAction.ts +0 -109
  25. package/src/actions/codemod/mods/deskRename.ts +0 -19
  26. package/src/actions/codemod/mods/index.ts +0 -9
  27. package/src/actions/codemod/mods/partsTypeDirective.ts +0 -28
  28. package/src/actions/codemod/mods/reactIconsV3.ts +0 -54
  29. package/src/actions/codemod/types.ts +0 -10
  30. package/src/actions/init-plugin/bootstrapFromTemplate.ts +0 -196
  31. package/src/actions/init-project/bootstrapLocalTemplate.ts +0 -201
  32. package/src/actions/init-project/bootstrapRemoteTemplate.ts +0 -118
  33. package/src/actions/init-project/createAppCliConfig.ts +0 -24
  34. package/src/actions/init-project/createCliConfig.ts +0 -31
  35. package/src/actions/init-project/createPackageManifest.ts +0 -102
  36. package/src/actions/init-project/createStudioConfig.ts +0 -53
  37. package/src/actions/init-project/determineAppTemplate.ts +0 -13
  38. package/src/actions/init-project/fsUtils.ts +0 -40
  39. package/src/actions/init-project/git.ts +0 -59
  40. package/src/actions/init-project/initProject.ts +0 -1708
  41. package/src/actions/init-project/processTemplate.ts +0 -55
  42. package/src/actions/init-project/promptForDatasetName.ts +0 -68
  43. package/src/actions/init-project/prompts/index.ts +0 -51
  44. package/src/actions/init-project/prompts/nextjs.ts +0 -59
  45. package/src/actions/init-project/readPackageJson.ts +0 -17
  46. package/src/actions/init-project/templates/appQuickstart.ts +0 -23
  47. package/src/actions/init-project/templates/appSanityUi.ts +0 -25
  48. package/src/actions/init-project/templates/blog.ts +0 -5
  49. package/src/actions/init-project/templates/clean.ts +0 -5
  50. package/src/actions/init-project/templates/getStarted.ts +0 -37
  51. package/src/actions/init-project/templates/index.ts +0 -24
  52. package/src/actions/init-project/templates/moviedb.ts +0 -35
  53. package/src/actions/init-project/templates/nextjs/index.ts +0 -238
  54. package/src/actions/init-project/templates/nextjs/schemaTypes/blog.ts +0 -252
  55. package/src/actions/init-project/templates/quickstart.ts +0 -5
  56. package/src/actions/init-project/templates/shopify.ts +0 -78
  57. package/src/actions/init-project/templates/shopifyOnline.ts +0 -50
  58. package/src/actions/init-project/updateInitialTemplateMetadata.ts +0 -24
  59. package/src/actions/login/login.telemetry.ts +0 -18
  60. package/src/actions/login/login.ts +0 -350
  61. package/src/actions/login/types.ts +0 -19
  62. package/src/actions/project/createProject.ts +0 -36
  63. package/src/actions/telemetry/setTelemetryConsent.ts +0 -101
  64. package/src/actions/typegen/generate.telemetry.ts +0 -20
  65. package/src/actions/typegen/generateAction.ts +0 -210
  66. package/src/actions/versions/findSanityModuleVersions.ts +0 -185
  67. package/src/actions/yarn/events.ts +0 -73
  68. package/src/cli.ts +0 -350
  69. package/src/cliClient.ts +0 -62
  70. package/src/commands/blueprints/addBlueprintsCommand.ts +0 -155
  71. package/src/commands/blueprints/blueprintsGroup.ts +0 -10
  72. package/src/commands/blueprints/configBlueprintsCommand.ts +0 -86
  73. package/src/commands/blueprints/deployBlueprintsCommand.ts +0 -63
  74. package/src/commands/blueprints/destroyBlueprintsCommand.ts +0 -78
  75. package/src/commands/blueprints/infoBlueprintsCommand.ts +0 -55
  76. package/src/commands/blueprints/initBlueprintsCommand.ts +0 -99
  77. package/src/commands/blueprints/logsBlueprintsCommand.ts +0 -66
  78. package/src/commands/blueprints/planBlueprintsCommand.ts +0 -51
  79. package/src/commands/blueprints/stacksBlueprintsCommand.ts +0 -66
  80. package/src/commands/codemod/codemodCommand.ts +0 -32
  81. package/src/commands/debug/debugCommand.ts +0 -26
  82. package/src/commands/debug/printDebugInfo.ts +0 -243
  83. package/src/commands/docs/docsCommand.ts +0 -20
  84. package/src/commands/functions/devFunctionsCommand.ts +0 -73
  85. package/src/commands/functions/envFunctionsCommand.ts +0 -105
  86. package/src/commands/functions/functionsGroup.ts +0 -10
  87. package/src/commands/functions/logsFunctionsCommand.ts +0 -94
  88. package/src/commands/functions/testFunctionsCommand.ts +0 -93
  89. package/src/commands/help/helpCommand.ts +0 -21
  90. package/src/commands/help/showHelp.ts +0 -58
  91. package/src/commands/index.ts +0 -72
  92. package/src/commands/init/initCommand.ts +0 -123
  93. package/src/commands/install/installCommand.ts +0 -38
  94. package/src/commands/learn/learnCommand.ts +0 -20
  95. package/src/commands/login/loginCommand.ts +0 -29
  96. package/src/commands/logout/logoutCommand.ts +0 -52
  97. package/src/commands/manage/manageCommand.ts +0 -24
  98. package/src/commands/projects/listProjectsCommand.ts +0 -62
  99. package/src/commands/projects/projectsGroup.ts +0 -10
  100. package/src/commands/telemetry/disableTelemetryCommand.ts +0 -19
  101. package/src/commands/telemetry/enableTelemetryCommand.ts +0 -19
  102. package/src/commands/telemetry/telemetryGroup.ts +0 -10
  103. package/src/commands/telemetry/telemetryStatusCommand.ts +0 -89
  104. package/src/commands/typegen/generateTypesCommand.ts +0 -56
  105. package/src/commands/typegen/typegenGroup.ts +0 -10
  106. package/src/commands/versions/printVersionResult.ts +0 -51
  107. package/src/commands/versions/versionsCommand.ts +0 -13
  108. package/src/config.ts +0 -14
  109. package/src/debug.ts +0 -3
  110. package/src/index.ts +0 -6
  111. package/src/outputters/cliOutputter.ts +0 -47
  112. package/src/packageManager/getInstallCommand.ts +0 -26
  113. package/src/packageManager/getUpgradeCommand.ts +0 -52
  114. package/src/packageManager/index.ts +0 -33
  115. package/src/packageManager/installPackages.ts +0 -99
  116. package/src/packageManager/packageManagerChoice.ts +0 -204
  117. package/src/packageManager/uninstallPackages.ts +0 -44
  118. package/src/packageManager/yarnStub.ts +0 -22
  119. package/src/prompters/cliPrompter.ts +0 -9
  120. package/src/run.ts +0 -8
  121. package/src/studioDependencies.ts +0 -25
  122. package/src/types.ts +0 -387
  123. package/src/util/canLaunchBrowser.ts +0 -12
  124. package/src/util/cliWorker.ts +0 -34
  125. package/src/util/clientWrapper.ts +0 -119
  126. package/src/util/copy.ts +0 -50
  127. package/src/util/createExpiringConfig.ts +0 -80
  128. package/src/util/createTelemetryStore.ts +0 -204
  129. package/src/util/detectRuntime.ts +0 -12
  130. package/src/util/dynamicRequire.ts +0 -17
  131. package/src/util/frameworkPort.ts +0 -63
  132. package/src/util/generateCommandsDocumentation.ts +0 -62
  133. package/src/util/getCliConfig.ts +0 -172
  134. package/src/util/getCliVersion.ts +0 -22
  135. package/src/util/getLocalVersion.ts +0 -35
  136. package/src/util/getProjectDefaults.ts +0 -103
  137. package/src/util/getProviderName.ts +0 -9
  138. package/src/util/getUserConfig.ts +0 -15
  139. package/src/util/isCi.ts +0 -7
  140. package/src/util/isCommandGroup.ts +0 -7
  141. package/src/util/isInteractive.ts +0 -3
  142. package/src/util/isTrueish.ts +0 -8
  143. package/src/util/journeyConfig.ts +0 -280
  144. package/src/util/loadEnv.ts +0 -110
  145. package/src/util/mergeCommands.ts +0 -78
  146. package/src/util/neatStack.ts +0 -30
  147. package/src/util/noSuchCommandText.ts +0 -91
  148. package/src/util/parseArguments.ts +0 -76
  149. package/src/util/pathExists.ts +0 -14
  150. package/src/util/readJson.ts +0 -6
  151. package/src/util/readdirRecursive.ts +0 -26
  152. package/src/util/remoteTemplate.ts +0 -320
  153. package/src/util/resolveLatestVersions.ts +0 -24
  154. package/src/util/resolveRootDir.ts +0 -59
  155. package/src/util/telemetryDisclosure.ts +0 -45
  156. package/src/util/updateNotifier.ts +0 -158
  157. package/src/util/writeJson.ts +0 -6
  158. package/src/workers/getAndWriteJourneySchema.ts +0 -7
  159. package/src/workers/getCliConfig.ts +0 -14
  160. package/src/workers/typegenGenerate.ts +0 -228
@@ -1,1708 +0,0 @@
1
- import {existsSync} from 'node:fs'
2
- import fs from 'node:fs/promises'
3
- import path from 'node:path'
4
-
5
- import {type DatasetAclMode, type SanityProject} from '@sanity/client'
6
- import {type Framework} from '@vercel/frameworks'
7
- import {type detectFrameworkRecord} from '@vercel/fs-detectors'
8
- import dotenv from 'dotenv'
9
- import execa, {type CommonOptions} from 'execa'
10
- import {deburr, noop} from 'lodash'
11
- import pMap from 'p-map'
12
- import resolveFrom from 'resolve-from'
13
- import semver from 'semver'
14
-
15
- import {CLIInitStepCompleted} from '../../__telemetry__/init.telemetry'
16
- import {type InitFlags} from '../../commands/init/initCommand'
17
- import {debug} from '../../debug'
18
- import {
19
- getPackageManagerChoice,
20
- installDeclaredPackages,
21
- installNewPackages,
22
- } from '../../packageManager'
23
- import {
24
- ALLOWED_PACKAGE_MANAGERS,
25
- allowedPackageManagersString,
26
- getPartialEnvWithNpmPath,
27
- type PackageManager,
28
- } from '../../packageManager/packageManagerChoice'
29
- import {
30
- type CliApiClient,
31
- type CliCommandArguments,
32
- type CliCommandContext,
33
- type CliCommandDefinition,
34
- type SanityCore,
35
- type SanityModuleInternal,
36
- type SanityUser,
37
- } from '../../types'
38
- import {getClientWrapper} from '../../util/clientWrapper'
39
- import {dynamicRequire} from '../../util/dynamicRequire'
40
- import {getProjectDefaults, type ProjectDefaults} from '../../util/getProjectDefaults'
41
- import {getProviderName} from '../../util/getProviderName'
42
- import {getUserConfig} from '../../util/getUserConfig'
43
- import {isCommandGroup} from '../../util/isCommandGroup'
44
- import {isInteractive} from '../../util/isInteractive'
45
- import {fetchJourneyConfig} from '../../util/journeyConfig'
46
- import {checkIsRemoteTemplate, getGitHubRepoInfo, type RepoInfo} from '../../util/remoteTemplate'
47
- import {login, type LoginFlags} from '../login/login'
48
- import {createProject} from '../project/createProject'
49
- import {bootstrapLocalTemplate} from './bootstrapLocalTemplate'
50
- import {bootstrapRemoteTemplate} from './bootstrapRemoteTemplate'
51
- import {type GenerateConfigOptions} from './createStudioConfig'
52
- import {determineAppTemplate} from './determineAppTemplate'
53
- import {absolutify, validateEmptyPath} from './fsUtils'
54
- import {tryGitInit} from './git'
55
- import {promptForDatasetName} from './promptForDatasetName'
56
- import {promptForAclMode, promptForDefaultConfig, promptForTypeScript} from './prompts'
57
- import {
58
- promptForAppendEnv,
59
- promptForEmbeddedStudio,
60
- promptForNextTemplate,
61
- promptForStudioPath,
62
- } from './prompts/nextjs'
63
- import {readPackageJson} from './readPackageJson'
64
- import templates from './templates'
65
- import {
66
- sanityCliTemplate,
67
- sanityConfigTemplate,
68
- sanityFolder,
69
- sanityStudioTemplate,
70
- } from './templates/nextjs'
71
-
72
- // eslint-disable-next-line no-process-env
73
- const isCI = Boolean(process.env.CI)
74
-
75
- /**
76
- * @deprecated - No longer used
77
- */
78
- export interface InitOptions {
79
- template: string
80
- outputDir: string
81
- name: string
82
- displayName: string
83
- dataset: string
84
- projectId: string
85
- author: string | undefined
86
- description: string | undefined
87
- gitRemote: string | undefined
88
- license: string | undefined
89
- outputPath: string | undefined
90
- projectName: string
91
- useTypeScript: boolean
92
- }
93
-
94
- export interface ProjectTemplate {
95
- datasetUrl?: string
96
- dependencies?: Record<string, string>
97
- devDependencies?: Record<string, string>
98
- importPrompt?: string
99
- configTemplate?: string | ((variables: GenerateConfigOptions['variables']) => string)
100
- typescriptOnly?: boolean
101
- entry?: string
102
- scripts?: Record<string, string>
103
- }
104
-
105
- export interface ProjectOrganization {
106
- id: string
107
- name: string
108
- slug: string
109
- }
110
-
111
- interface OrganizationCreateResponse {
112
- id: string
113
- name: string
114
- createdByUserId: string
115
- slug: string | null
116
- defaultRoleName: string | null
117
- members: unknown[]
118
- features: unknown[]
119
- }
120
-
121
- // eslint-disable-next-line max-statements, complexity
122
- export default async function initSanity(
123
- args: CliCommandArguments<InitFlags>,
124
- context: CliCommandContext & {
125
- detectedFramework: Awaited<ReturnType<typeof detectFrameworkRecord>>
126
- },
127
- ): Promise<void> {
128
- const {output, prompt, workDir, apiClient, chalk, telemetry, detectedFramework} = context
129
-
130
- const trace = telemetry.trace(CLIInitStepCompleted)
131
-
132
- const cliFlags = args.extOptions
133
- const unattended = cliFlags.y || cliFlags.yes
134
- const print = unattended ? noop : output.print
135
- const success = output.success
136
- const warn = output.warn
137
-
138
- const intendedPlan = cliFlags['project-plan']
139
- const intendedCoupon = cliFlags.coupon
140
- const reconfigure = cliFlags.reconfigure
141
- const commitMessage = cliFlags.git
142
- const useGit = typeof commitMessage === 'undefined' ? true : Boolean(commitMessage)
143
- const bareOutput = cliFlags.bare
144
- const env = cliFlags.env
145
- const packageManager = cliFlags['package-manager']
146
-
147
- let remoteTemplateInfo: RepoInfo | undefined
148
- if (cliFlags.template && checkIsRemoteTemplate(cliFlags.template)) {
149
- remoteTemplateInfo = await getGitHubRepoInfo(cliFlags.template, cliFlags['template-token'])
150
- }
151
-
152
- let defaultConfig = cliFlags['dataset-default']
153
- let showDefaultConfigPrompt = !defaultConfig
154
-
155
- trace.start()
156
- trace.log({
157
- step: 'start',
158
- flags: {
159
- defaultConfig,
160
- unattended,
161
- plan: intendedPlan,
162
- coupon: intendedCoupon,
163
- reconfigure,
164
- git: commitMessage,
165
- bare: bareOutput,
166
- env,
167
- },
168
- })
169
-
170
- if (detectedFramework && detectedFramework.slug !== 'sanity' && remoteTemplateInfo) {
171
- throw new Error(
172
- `A remote template cannot be used with a detected framework. Detected: ${detectedFramework.name}`,
173
- )
174
- }
175
-
176
- // Only allow either --project-plan or --coupon
177
- if (intendedCoupon && intendedPlan) {
178
- throw new Error(
179
- 'Error! --project-plan and --coupon cannot be used together; please select only one flag',
180
- )
181
- }
182
-
183
- // Don't allow --coupon and --project
184
- if (intendedCoupon && cliFlags.project) {
185
- throw new Error(
186
- 'Error! --project and --coupon cannot be used together; coupons can only be applied to new projects',
187
- )
188
- }
189
-
190
- let selectedPlan: string | undefined
191
- if (intendedCoupon) {
192
- try {
193
- selectedPlan = await getPlanFromCoupon(apiClient, intendedCoupon)
194
- print(`Coupon "${intendedCoupon}" validated!\n`)
195
- } catch (err) {
196
- if (err.statusCode == '404') {
197
- const useDefaultPlan =
198
- unattended ??
199
- (await prompt.single({
200
- type: 'confirm',
201
- message: `Coupon "${intendedCoupon}" is not available, use default plan instead?`,
202
- default: true,
203
- }))
204
- if (unattended) {
205
- output.warn(`Coupon "${intendedCoupon}" is not available - using default plan`)
206
- }
207
- trace.log({
208
- step: 'useDefaultPlanCoupon',
209
- selectedOption: useDefaultPlan ? 'yes' : 'no',
210
- coupon: intendedCoupon,
211
- })
212
- if (useDefaultPlan) {
213
- print(`Using default plan.`)
214
- } else {
215
- throw new Error(`Coupon "${intendedCoupon}" does not exist`)
216
- }
217
- } else {
218
- throw new Error(`Unable to validate coupon, please try again later:\n\n${err.message}`)
219
- }
220
- }
221
- } else if (intendedPlan) {
222
- try {
223
- selectedPlan = await getPlanFromId(apiClient, intendedPlan)
224
- } catch (err) {
225
- if (err.statusCode == '404') {
226
- const useDefaultPlan =
227
- unattended ??
228
- (await prompt.single({
229
- type: 'confirm',
230
- message: `Project plan "${intendedPlan}" does not exist, use default plan instead?`,
231
- default: true,
232
- }))
233
- if (unattended) {
234
- output.warn(`Project plan "${intendedPlan}" does not exist - using default plan`)
235
- }
236
- trace.log({
237
- step: 'useDefaultPlanId',
238
- selectedOption: useDefaultPlan ? 'yes' : 'no',
239
- planId: intendedPlan,
240
- })
241
- if (useDefaultPlan) {
242
- print(`Using default plan.`)
243
- } else {
244
- throw new Error(`Plan id "${intendedPlan}" does not exist`)
245
- }
246
- } else {
247
- throw new Error(`Unable to validate plan, please try again later:\n\n${err.message}`)
248
- }
249
- }
250
- }
251
-
252
- if (reconfigure) {
253
- throw new Error('`--reconfigure` is deprecated - manual configuration is now required')
254
- }
255
-
256
- let envFilenameDefault = '.env'
257
- if (detectedFramework && detectedFramework.slug === 'nextjs') {
258
- envFilenameDefault = '.env.local'
259
- }
260
- const envFilename = typeof env === 'string' ? env : envFilenameDefault
261
- if (!envFilename.startsWith('.env')) {
262
- throw new Error('Env filename must start with .env')
263
- }
264
-
265
- // If the user isn't already authenticated, make it so
266
- const userConfig = getUserConfig()
267
- const hasToken = userConfig.get('authToken')
268
-
269
- debug(hasToken ? 'User already has a token' : 'User has no token')
270
- let user: SanityUser | undefined
271
- if (hasToken) {
272
- trace.log({step: 'login', alreadyLoggedIn: true})
273
- user = await getUserData(apiClient)
274
- success('You are logged in as %s using %s', user.email, getProviderName(user.provider))
275
- } else if (!unattended) {
276
- trace.log({step: 'login'})
277
- user = await getOrCreateUser()
278
- }
279
-
280
- // skip project / dataset prompting
281
- const isAppTemplate = cliFlags.template ? determineAppTemplate(cliFlags.template) : false // Default to false
282
-
283
- let introMessage = 'Fetching existing projects'
284
- if (cliFlags.quickstart) {
285
- introMessage = "Eject your existing project's Sanity configuration"
286
- }
287
-
288
- if (!isAppTemplate) {
289
- success(introMessage)
290
- print('')
291
- }
292
-
293
- const isNextJs = detectedFramework?.slug === 'nextjs'
294
-
295
- const flags = await prepareFlags()
296
-
297
- // We're authenticated, now lets select or create a project (for studios) or org (for custom apps)
298
- const {projectId, displayName, isFirstProject, datasetName, schemaUrl, organizationId} =
299
- await getProjectDetails()
300
-
301
- const sluggedName = deburr(displayName.toLowerCase())
302
- .replace(/\s+/g, '-')
303
- .replace(/[^a-z0-9-]/g, '')
304
-
305
- // If user doesn't want to output any template code
306
- if (bareOutput) {
307
- success('Below are your project details')
308
- print('')
309
- print(`Project ID: ${chalk.cyan(projectId)}`)
310
- print(`Dataset: ${chalk.cyan(datasetName)}`)
311
- print(
312
- `\nYou can find your project on Sanity Manage — https://www.sanity.io/manage/project/${projectId}\n`,
313
- )
314
- return
315
- }
316
-
317
- let initNext = false
318
- if (isNextJs) {
319
- initNext =
320
- unattended ||
321
- (await prompt.single({
322
- type: 'confirm',
323
- message:
324
- 'Would you like to add configuration files for a Sanity project in this Next.js folder?',
325
- default: true,
326
- }))
327
-
328
- trace.log({
329
- step: 'useDetectedFramework',
330
- selectedOption: initNext ? 'yes' : 'no',
331
- detectedFramework: detectedFramework?.name,
332
- })
333
- }
334
-
335
- // add more frameworks to this as we add support for them
336
- // this is used to skip the getProjectInfo prompt
337
- const initFramework = initNext
338
-
339
- let outputPath = workDir
340
-
341
- // Gather project defaults based on environment
342
- const defaults = await getProjectDefaults(workDir, {isPlugin: false, context})
343
-
344
- // Prompt the user for required information
345
- const answers = await getProjectInfo()
346
-
347
- // Ensure we are using the output path provided by user
348
- outputPath = answers.outputPath
349
-
350
- if (isNextJs) {
351
- const packageJson = readPackageJson(`${outputPath}/package.json`)
352
- const reactVersion = packageJson?.dependencies?.react
353
-
354
- if (reactVersion) {
355
- const isUsingReact19 = semver.coerce(reactVersion)?.major === 19
356
- const isUsingNextJs15 = semver.coerce(detectedFramework?.detectedVersion)?.major === 15
357
-
358
- if (isUsingNextJs15 && isUsingReact19) {
359
- warn('╭────────────────────────────────────────────────────────────╮')
360
- warn('│ │')
361
- warn('│ It looks like you are using Next.js 15 and React 19 │')
362
- warn('│ Please read our compatibility guide. │')
363
- warn('│ https://www.sanity.io/help/react-19 │')
364
- warn('│ │')
365
- warn('╰────────────────────────────────────────────────────────────╯')
366
- }
367
- }
368
- }
369
-
370
- if (initNext) {
371
- const useTypeScript = unattended ? true : await promptForTypeScript(prompt)
372
- trace.log({step: 'useTypeScript', selectedOption: useTypeScript ? 'yes' : 'no'})
373
- const fileExtension = useTypeScript ? 'ts' : 'js'
374
-
375
- const embeddedStudio = unattended ? true : await promptForEmbeddedStudio(prompt)
376
- let hasSrcFolder = false
377
-
378
- if (embeddedStudio) {
379
- // find source path (app or src/app)
380
- const appDir = 'app'
381
- let srcPath = path.join(workDir, appDir)
382
-
383
- if (!existsSync(srcPath)) {
384
- srcPath = path.join(workDir, 'src', appDir)
385
- hasSrcFolder = true
386
- if (!existsSync(srcPath)) {
387
- await fs
388
- .mkdir(srcPath, {recursive: true})
389
- .catch(() => debug('Error creating folder %s', srcPath))
390
- }
391
- }
392
-
393
- const studioPath = unattended ? '/studio' : await promptForStudioPath(prompt)
394
-
395
- const embeddedStudioRouteFilePath = path.join(
396
- srcPath,
397
- `${studioPath}/`,
398
- `[[...tool]]/page.${fileExtension}x`,
399
- )
400
-
401
- // this selects the correct template string based on whether the user is using the app or pages directory and
402
- // replaces the ":configPath:" placeholder in the template with the correct path to the sanity.config.ts file.
403
- // we account for the user-defined embeddedStudioPath (default /studio) is accounted for by creating enough "../"
404
- // relative paths to reach the root level of the project
405
- await writeOrOverwrite(
406
- embeddedStudioRouteFilePath,
407
- sanityStudioTemplate.replace(
408
- ':configPath:',
409
- new Array(countNestedFolders(embeddedStudioRouteFilePath.slice(workDir.length)))
410
- .join('../')
411
- .concat('sanity.config'),
412
- ),
413
- )
414
-
415
- const sanityConfigPath = path.join(workDir, `sanity.config.${fileExtension}`)
416
- await writeOrOverwrite(
417
- sanityConfigPath,
418
- sanityConfigTemplate(hasSrcFolder)
419
- .replace(':route:', embeddedStudioRouteFilePath.slice(workDir.length).replace('src/', ''))
420
- .replace(':basePath:', studioPath),
421
- )
422
- }
423
-
424
- const sanityCliPath = path.join(workDir, `sanity.cli.${fileExtension}`)
425
- await writeOrOverwrite(sanityCliPath, sanityCliTemplate)
426
-
427
- // write sanity folder files
428
- const writeSourceFiles = async (
429
- files: Record<string, string | Record<string, string>>,
430
- folderPath?: string,
431
- srcFolderPrefix?: boolean,
432
- ) => {
433
- for (const [filePath, content] of Object.entries(files)) {
434
- // check if file ends with full stop to indicate it's file and not directory (this only works with our template tree structure)
435
- if (filePath.includes('.') && typeof content === 'string') {
436
- await writeOrOverwrite(
437
- path.join(
438
- workDir,
439
- srcFolderPrefix ? 'src' : '',
440
- 'sanity',
441
- folderPath || '',
442
- `${filePath}${fileExtension}`,
443
- ),
444
- content,
445
- )
446
- } else {
447
- await fs.mkdir(path.join(workDir, srcFolderPrefix ? 'src' : '', 'sanity', filePath), {
448
- recursive: true,
449
- })
450
- if (typeof content === 'object') {
451
- await writeSourceFiles(content, filePath, srcFolderPrefix)
452
- }
453
- }
454
- }
455
- }
456
-
457
- // ask what kind of schema setup the user wants
458
- const templateToUse = unattended ? 'clean' : await promptForNextTemplate(prompt)
459
-
460
- await writeSourceFiles(sanityFolder(useTypeScript, templateToUse), undefined, hasSrcFolder)
461
-
462
- const appendEnv = unattended ? true : await promptForAppendEnv(prompt, envFilename)
463
-
464
- if (appendEnv) {
465
- await createOrAppendEnvVars(envFilename, detectedFramework, {log: true})
466
- }
467
-
468
- if (embeddedStudio) {
469
- const nextjsLocalDevOrigin = 'http://localhost:3000'
470
- const existingCorsOrigins = await apiClient({api: {projectId}}).request({
471
- method: 'GET',
472
- uri: '/cors',
473
- })
474
- const hasExistingCorsOrigin = existingCorsOrigins.some(
475
- (item: {origin: string}) => item.origin === nextjsLocalDevOrigin,
476
- )
477
- if (!hasExistingCorsOrigin) {
478
- await apiClient({api: {projectId}})
479
- .request({
480
- method: 'POST',
481
- url: '/cors',
482
- body: {origin: nextjsLocalDevOrigin, allowCredentials: true},
483
- maxRedirects: 0,
484
- })
485
- .then((res) => {
486
- print(
487
- res.id
488
- ? `Added ${nextjsLocalDevOrigin} to CORS origins`
489
- : `Failed to add ${nextjsLocalDevOrigin} to CORS origins`,
490
- )
491
- })
492
- .catch((error) => {
493
- print(`Failed to add ${nextjsLocalDevOrigin} to CORS origins`, error)
494
- })
495
- }
496
- }
497
-
498
- const {chosen} = await getPackageManagerChoice(workDir, {interactive: false})
499
- trace.log({step: 'selectPackageManager', selectedOption: chosen})
500
- const packages = ['@sanity/vision@3', 'sanity@3', '@sanity/image-url@1', 'styled-components@6']
501
- if (templateToUse === 'blog') {
502
- packages.push('@sanity/icons')
503
- }
504
- await installNewPackages(
505
- {
506
- packageManager: chosen,
507
- packages,
508
- },
509
- {
510
- output: context.output,
511
- workDir,
512
- },
513
- )
514
-
515
- // will refactor this later
516
- const execOptions: CommonOptions<'utf8'> = {
517
- encoding: 'utf8',
518
- env: getPartialEnvWithNpmPath(workDir),
519
- cwd: workDir,
520
- stdio: 'inherit',
521
- }
522
-
523
- if (chosen === 'npm') {
524
- await execa('npm', ['install', '--legacy-peer-deps', 'next-sanity@9'], execOptions)
525
- } else if (chosen === 'yarn') {
526
- await execa('npx', ['install-peerdeps', '--yarn', 'next-sanity@9'], execOptions)
527
- } else if (chosen === 'pnpm') {
528
- await execa('pnpm', ['install', 'next-sanity@9'], execOptions)
529
- }
530
-
531
- print(
532
- `\n${chalk.green('Success!')} Your Sanity configuration files has been added to this project`,
533
- )
534
-
535
- return
536
- }
537
-
538
- // eslint-disable-next-line @typescript-eslint/no-shadow
539
- function countNestedFolders(path: string): number {
540
- const separator = path.includes('\\') ? '\\' : '/'
541
- return path.split(separator).filter(Boolean).length
542
- }
543
-
544
- async function writeOrOverwrite(filePath: string, content: string) {
545
- if (existsSync(filePath)) {
546
- const overwrite = await prompt.single({
547
- type: 'confirm',
548
- message: `File ${chalk.yellow(
549
- filePath.replace(workDir, ''),
550
- )} already exists. Do you want to overwrite it?`,
551
- default: false,
552
- })
553
-
554
- if (!overwrite) {
555
- return
556
- }
557
- }
558
-
559
- // make folder if not exists
560
- const folderPath = path.dirname(filePath)
561
-
562
- await fs
563
- .mkdir(folderPath, {recursive: true})
564
- .catch(() => debug('Error creating folder %s', folderPath))
565
-
566
- await fs.writeFile(filePath, content, {
567
- encoding: 'utf8',
568
- })
569
- }
570
-
571
- // user wants to write environment variables to file
572
- if (env) {
573
- await createOrAppendEnvVars(envFilename, detectedFramework)
574
- return
575
- }
576
-
577
- // Prompt for template to use
578
- const templateName = await selectProjectTemplate()
579
- trace.log({step: 'selectProjectTemplate', selectedOption: templateName})
580
- const template = templates[templateName]
581
- if (!remoteTemplateInfo && !template) {
582
- throw new Error(`Template "${templateName}" not found`)
583
- }
584
-
585
- // Use typescript?
586
- let useTypeScript = true
587
- if (!remoteTemplateInfo && template) {
588
- const typescriptOnly = template.typescriptOnly === true
589
- if (!typescriptOnly && typeof cliFlags.typescript === 'boolean') {
590
- useTypeScript = cliFlags.typescript
591
- } else if (!typescriptOnly && !unattended) {
592
- useTypeScript = await promptForTypeScript(prompt)
593
- trace.log({step: 'useTypeScript', selectedOption: useTypeScript ? 'yes' : 'no'})
594
- }
595
- }
596
-
597
- // we enable auto-updates by default, but allow users to specify otherwise
598
- let autoUpdates = true
599
- if (typeof cliFlags['auto-updates'] === 'boolean') {
600
- autoUpdates = cliFlags['auto-updates']
601
- }
602
-
603
- // If the template has a sample dataset, prompt the user whether or not we should import it
604
- const shouldImport =
605
- !unattended && template?.datasetUrl && (await promptForDatasetImport(template.importPrompt))
606
-
607
- trace.log({step: 'importTemplateDataset', selectedOption: shouldImport ? 'yes' : 'no'})
608
-
609
- const [_, bootstrapPromise] = await Promise.allSettled([
610
- updateProjectCliInitializedMetadata(),
611
- bootstrapTemplate(),
612
- ])
613
-
614
- if (bootstrapPromise.status === 'rejected' && bootstrapPromise.reason instanceof Error) {
615
- throw bootstrapPromise.reason
616
- }
617
-
618
- let pkgManager: PackageManager
619
-
620
- // If the user has specified a package manager, and it's allowed use that
621
- if (packageManager && ALLOWED_PACKAGE_MANAGERS.includes(packageManager)) {
622
- pkgManager = packageManager
623
- } else {
624
- // Otherwise, try to find the most optimal package manager to use
625
- pkgManager = (
626
- await getPackageManagerChoice(outputPath, {
627
- prompt,
628
- interactive: unattended ? false : isInteractive,
629
- })
630
- ).chosen
631
-
632
- // only log warning if a package manager flag is passed
633
- if (packageManager) {
634
- output.warn(
635
- chalk.yellow(
636
- `Given package manager "${packageManager}" is not supported. Supported package managers are ${allowedPackageManagersString}.`,
637
- ),
638
- )
639
- output.print(`Using ${pkgManager} as package manager`)
640
- }
641
- }
642
-
643
- trace.log({step: 'selectPackageManager', selectedOption: pkgManager})
644
-
645
- // Now for the slow part... installing dependencies
646
- await installDeclaredPackages(outputPath, pkgManager, context)
647
-
648
- // Try initializing a git repository
649
- if (useGit) {
650
- tryGitInit(outputPath, typeof commitMessage === 'string' ? commitMessage : undefined)
651
- }
652
-
653
- // Prompt for dataset import (if a dataset is defined)
654
- if (shouldImport) {
655
- const importCommand = getImportCommand(outputPath, 3)
656
- await doDatasetImport({
657
- projectId,
658
- outputPath,
659
- importCommand,
660
- template,
661
- datasetName,
662
- context,
663
- })
664
-
665
- print('')
666
- print('If you want to delete the imported data, use')
667
- print(` ${chalk.cyan(`npx sanity dataset delete ${datasetName}`)}`)
668
- print('and create a new clean dataset with')
669
- print(` ${chalk.cyan(`npx sanity dataset create <name>`)}\n`)
670
- }
671
-
672
- const devCommandMap: Record<PackageManager, string> = {
673
- yarn: 'yarn dev',
674
- npm: 'npm run dev',
675
- pnpm: 'pnpm dev',
676
- bun: 'bun dev',
677
- manual: 'npm run dev',
678
- }
679
- const devCommand = devCommandMap[pkgManager]
680
-
681
- const isCurrentDir = outputPath === process.cwd()
682
- const goToProjectDir = `(${chalk.cyan(`cd ${outputPath}`)} to navigate to your new project directory)`
683
-
684
- if (isAppTemplate) {
685
- //output for custom apps here
686
- print(`✅ ${chalk.green.bold('Success!')} Your custom app has been scaffolded.`)
687
- if (!isCurrentDir) print(goToProjectDir)
688
- print(
689
- `\n${chalk.bold('Next')}, configure the project(s) and dataset(s) your app should work with.`,
690
- )
691
- print('\nGet started in `src/App.tsx`, or refer to our documentation for a walkthrough:')
692
- print(chalk.blue.underline('https://www.sanity.io/docs/app-sdk/sdk-configuration'))
693
- print('\n')
694
- print(`Other helpful commands:`)
695
- print(`npx sanity docs to open the documentation in a browser`)
696
- print(`npx sanity dev to start the development server for your app`)
697
- print(`npx sanity deploy to deploy your app`)
698
- } else {
699
- //output for Studios here
700
- print(`✅ ${chalk.green.bold('Success!')} Your Studio has been created.`)
701
- if (!isCurrentDir) print(goToProjectDir)
702
- print(
703
- `Get started by running ${chalk.cyan(devCommand)} to launch your Studio’s development server`,
704
- )
705
- print('\n')
706
- print(`Other helpful commands:`)
707
- print(`npx sanity docs to open the documentation in a browser`)
708
- print(`npx sanity manage to open the project settings in a browser`)
709
- print(`npx sanity help to explore the CLI manual`)
710
- }
711
-
712
- if (isFirstProject) {
713
- trace.log({step: 'sendCommunityInvite', selectedOption: 'yes'})
714
-
715
- const DISCORD_INVITE_LINK = 'https://snty.link/community'
716
-
717
- print(`\nJoin our wonderful developer community as well: ${chalk.cyan(DISCORD_INVITE_LINK)}`)
718
- print('We look forward to seeing you there!\n')
719
- }
720
-
721
- trace.complete()
722
-
723
- async function getOrCreateUser() {
724
- warn('No authentication credentials found in your Sanity config')
725
- print('')
726
-
727
- // Provide login options (`sanity login`)
728
- const {extOptions: _extOptions, ...otherArgs} = args
729
- const loginArgs: CliCommandArguments<LoginFlags> = {...otherArgs, extOptions: {}}
730
- await login(loginArgs, {...context, telemetry: trace.newContext('login')})
731
- return getUserData(apiClient)
732
- }
733
-
734
- async function getProjectDetails(): Promise<{
735
- projectId: string
736
- datasetName: string
737
- displayName: string
738
- isFirstProject: boolean
739
- schemaUrl?: string
740
- organizationId?: string
741
- }> {
742
- // If we're doing a quickstart, we don't need to prompt for project details
743
- if (flags.quickstart) {
744
- debug('Fetching project details from Journey API')
745
- const data = await fetchJourneyConfig(apiClient, flags.quickstart)
746
- trace.log({
747
- step: 'fetchJourneyConfig',
748
- projectId: data.projectId,
749
- datasetName: data.datasetName,
750
- displayName: data.displayName,
751
- isFirstProject: data.isFirstProject,
752
- })
753
- return data
754
- }
755
-
756
- if (isAppTemplate) {
757
- const client = apiClient({requireUser: true, requireProject: false})
758
- const organizations = await client.request({
759
- uri: '/organizations',
760
- query: {
761
- includeMembers: 'true',
762
- includeImplicitMemberships: 'true',
763
- },
764
- })
765
-
766
- const appOrganizationId = await getOrganizationIdForAppTemplate(organizations)
767
-
768
- return {
769
- projectId: '',
770
- displayName: '',
771
- datasetName: '',
772
- isFirstProject: false,
773
- organizationId: appOrganizationId,
774
- }
775
- }
776
-
777
- debug('Prompting user to select or create a project')
778
- const project = await getOrCreateProject()
779
- debug(`Project with name ${project.displayName} selected`)
780
-
781
- // Now let's pick or create a dataset
782
- debug('Prompting user to select or create a dataset')
783
- const dataset = await getOrCreateDataset({
784
- projectId: project.projectId,
785
- displayName: project.displayName,
786
- dataset: flags.dataset,
787
- aclMode: flags.visibility,
788
- defaultConfig: flags['dataset-default'],
789
- })
790
- debug(`Dataset with name ${dataset.datasetName} selected`)
791
-
792
- trace.log({
793
- step: 'createOrSelectDataset',
794
- selectedOption: dataset.userAction,
795
- datasetName: dataset.datasetName,
796
- visibility: flags.visibility as 'private' | 'public',
797
- })
798
-
799
- return {
800
- projectId: project.projectId,
801
- displayName: project.displayName,
802
- isFirstProject: project.isFirstProject,
803
- datasetName: dataset.datasetName,
804
- }
805
- }
806
-
807
- // eslint-disable-next-line complexity
808
- async function getOrCreateProject(): Promise<{
809
- projectId: string
810
- displayName: string
811
- isFirstProject: boolean
812
- userAction: 'create' | 'select'
813
- }> {
814
- const client = apiClient({requireUser: true, requireProject: false})
815
- let projects
816
- let organizations: ProjectOrganization[]
817
-
818
- try {
819
- const [allProjects, allOrgs] = await Promise.all([
820
- client.projects.list({includeMembers: false}),
821
- client.request({uri: '/organizations'}),
822
- ])
823
- projects = allProjects.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
824
- organizations = allOrgs
825
- } catch (err) {
826
- if (unattended && flags.project) {
827
- return {
828
- projectId: flags.project,
829
- displayName: 'Unknown project',
830
- isFirstProject: false,
831
- userAction: 'select',
832
- }
833
- }
834
- throw new Error(`Failed to communicate with the Sanity API:\n${err.message}`)
835
- }
836
-
837
- if (projects.length === 0 && unattended) {
838
- throw new Error('No projects found for current user')
839
- }
840
-
841
- if (flags.project) {
842
- const project = projects.find((proj) => proj.id === flags.project)
843
- if (!project && !unattended) {
844
- throw new Error(
845
- `Given project ID (${flags.project}) not found, or you do not have access to it`,
846
- )
847
- }
848
-
849
- return {
850
- projectId: flags.project,
851
- displayName: project ? project.displayName : 'Unknown project',
852
- isFirstProject: false,
853
- userAction: 'select',
854
- }
855
- }
856
-
857
- if (flags.organization) {
858
- const organization =
859
- organizations.find((org) => org.id === flags.organization) ||
860
- organizations.find((org) => org.slug === flags.organization)
861
-
862
- if (!organization) {
863
- throw new Error(
864
- `Given organization ID (${flags.organization}) not found, or you do not have access to it`,
865
- )
866
- }
867
-
868
- if (!(await hasProjectAttachGrant(flags.organization))) {
869
- throw new Error(
870
- 'You lack the necessary permissions to attach a project to this organization',
871
- )
872
- }
873
- }
874
-
875
- // If the user has no projects or is using a coupon (which can only be applied to new projects)
876
- // just ask for project details instead of showing a list of projects
877
- const isUsersFirstProject = projects.length === 0
878
- if (isUsersFirstProject || intendedCoupon) {
879
- debug(
880
- isUsersFirstProject
881
- ? 'No projects found for user, prompting for name'
882
- : 'Using a coupon - skipping project selection',
883
- )
884
- const projectName = await prompt.single({
885
- type: 'input',
886
- message: 'Project name:',
887
- default: 'My Sanity Project',
888
- validate(input) {
889
- if (!input || input.trim() === '') {
890
- return 'Project name cannot be empty'
891
- }
892
-
893
- if (input.length > 80) {
894
- return 'Project name cannot be longer than 80 characters'
895
- }
896
-
897
- return true
898
- },
899
- })
900
-
901
- return createProject(apiClient, {
902
- displayName: projectName,
903
- organizationId: cliFlags.organization || (await getOrganizationId(organizations)),
904
- subscription: selectedPlan ? {planId: selectedPlan} : undefined,
905
- metadata: {coupon: intendedCoupon},
906
- }).then((response) => ({
907
- ...response,
908
- isFirstProject: isUsersFirstProject,
909
- userAction: 'create',
910
- coupon: intendedCoupon,
911
- }))
912
- }
913
-
914
- debug(`User has ${projects.length} project(s) already, showing list of choices`)
915
-
916
- const projectChoices = projects.map((project) => ({
917
- value: project.id,
918
- name: `${project.displayName} (${project.id})`,
919
- }))
920
-
921
- const selected = await prompt.single({
922
- message: 'Create a new project or select an existing one',
923
- type: 'list',
924
- choices: [
925
- {value: 'new', name: 'Create new project'},
926
- new prompt.Separator(),
927
- ...projectChoices,
928
- ],
929
- })
930
-
931
- if (selected === 'new') {
932
- debug('User wants to create a new project, prompting for name')
933
- return createProject(apiClient, {
934
- displayName: await prompt.single({
935
- type: 'input',
936
- message: 'Your project name:',
937
- default: 'My Sanity Project',
938
- }),
939
- organizationId: cliFlags.organization || (await getOrganizationId(organizations)),
940
- subscription: selectedPlan ? {planId: selectedPlan} : undefined,
941
- metadata: {coupon: intendedCoupon},
942
- }).then((response) => ({
943
- ...response,
944
- isFirstProject: isUsersFirstProject,
945
- userAction: 'create',
946
- }))
947
- }
948
-
949
- debug(`Returning selected project (${selected})`)
950
- return {
951
- projectId: selected,
952
- displayName: projects.find((proj) => proj.id === selected)?.displayName || '',
953
- isFirstProject: isUsersFirstProject,
954
- userAction: 'select',
955
- }
956
- }
957
-
958
- async function getOrCreateDataset(opts: {
959
- projectId: string
960
- displayName: string
961
- dataset?: string
962
- aclMode?: string
963
- defaultConfig?: boolean
964
- }): Promise<{datasetName: string; userAction: 'create' | 'select' | 'none'}> {
965
- if (opts.dataset && (isCI || unattended)) {
966
- return {datasetName: opts.dataset, userAction: 'none'}
967
- }
968
-
969
- const client = apiClient({api: {projectId: opts.projectId}})
970
- const [datasets, projectFeatures] = await Promise.all([
971
- client.datasets.list(),
972
- client.request({uri: '/features'}),
973
- ])
974
-
975
- const privateDatasetsAllowed = projectFeatures.includes('privateDataset')
976
- const allowedModes = privateDatasetsAllowed ? ['public', 'private'] : ['public']
977
-
978
- if (opts.aclMode && !allowedModes.includes(opts.aclMode)) {
979
- throw new Error(`Visibility mode "${opts.aclMode}" not allowed`)
980
- }
981
-
982
- // Getter in order to present prompts in a more logical order
983
- const getAclMode = async (): Promise<string> => {
984
- if (opts.aclMode) {
985
- return opts.aclMode
986
- }
987
-
988
- if (unattended || !privateDatasetsAllowed || defaultConfig) {
989
- return 'public'
990
- }
991
-
992
- if (privateDatasetsAllowed) {
993
- const mode = await promptForAclMode(prompt, output)
994
- return mode
995
- }
996
-
997
- return 'public'
998
- }
999
-
1000
- if (opts.dataset) {
1001
- debug('User has specified dataset through a flag (%s)', opts.dataset)
1002
- const existing = datasets.find((ds) => ds.name === opts.dataset)
1003
-
1004
- if (!existing) {
1005
- debug('Specified dataset not found, creating it')
1006
- const aclMode = await getAclMode()
1007
- const spinner = context.output.spinner('Creating dataset').start()
1008
- await client.datasets.create(opts.dataset, {aclMode: aclMode as DatasetAclMode})
1009
- spinner.succeed()
1010
- }
1011
-
1012
- return {datasetName: opts.dataset, userAction: 'none'}
1013
- }
1014
-
1015
- const datasetInfo =
1016
- 'Your content will be stored in a dataset that can be public or private, depending on\nwhether you want to query your content with or without authentication.\nThe default dataset configuration has a public dataset named "production".'
1017
-
1018
- if (datasets.length === 0) {
1019
- debug('No datasets found for project, prompting for name')
1020
- if (showDefaultConfigPrompt) {
1021
- output.print(datasetInfo)
1022
- defaultConfig = await promptForDefaultConfig(prompt)
1023
- }
1024
- const name = defaultConfig
1025
- ? 'production'
1026
- : await promptForDatasetName(prompt, {
1027
- message: 'Name of your first dataset:',
1028
- })
1029
- const aclMode = await getAclMode()
1030
- const spinner = context.output.spinner('Creating dataset').start()
1031
- await client.datasets.create(name, {aclMode: aclMode as DatasetAclMode})
1032
- spinner.succeed()
1033
- return {datasetName: name, userAction: 'create'}
1034
- }
1035
-
1036
- debug(`User has ${datasets.length} dataset(s) already, showing list of choices`)
1037
- const datasetChoices = datasets.map((dataset) => ({value: dataset.name}))
1038
-
1039
- const selected = await prompt.single({
1040
- message: 'Select dataset to use',
1041
- type: 'list',
1042
- choices: [
1043
- {value: 'new', name: 'Create new dataset'},
1044
- new prompt.Separator(),
1045
- ...datasetChoices,
1046
- ],
1047
- })
1048
-
1049
- if (selected === 'new') {
1050
- const existingDatasetNames = datasets.map((ds) => ds.name)
1051
- debug('User wants to create a new dataset, prompting for name')
1052
- if (showDefaultConfigPrompt && !existingDatasetNames.includes('production')) {
1053
- output.print(datasetInfo)
1054
- defaultConfig = await promptForDefaultConfig(prompt)
1055
- }
1056
-
1057
- const newDatasetName = defaultConfig
1058
- ? 'production'
1059
- : await promptForDatasetName(
1060
- prompt,
1061
- {
1062
- message: 'Dataset name:',
1063
- },
1064
- existingDatasetNames,
1065
- )
1066
- const aclMode = await getAclMode()
1067
- const spinner = context.output.spinner('Creating dataset').start()
1068
- await client.datasets.create(newDatasetName, {aclMode: aclMode as DatasetAclMode})
1069
- spinner.succeed()
1070
- return {datasetName: newDatasetName, userAction: 'create'}
1071
- }
1072
-
1073
- debug(`Returning selected dataset (${selected})`)
1074
- return {datasetName: selected, userAction: 'select'}
1075
- }
1076
-
1077
- function promptForDatasetImport(message?: string) {
1078
- return prompt.single({
1079
- type: 'confirm',
1080
- message: message || 'This template includes a sample dataset, would you like to use it?',
1081
- default: true,
1082
- })
1083
- }
1084
-
1085
- function selectProjectTemplate() {
1086
- // Make sure the --quickstart and --template are not used together
1087
- if (flags.quickstart) {
1088
- return 'quickstart'
1089
- }
1090
-
1091
- const defaultTemplate = unattended || flags.template ? flags.template || 'clean' : null
1092
- if (defaultTemplate) {
1093
- return defaultTemplate
1094
- }
1095
-
1096
- return prompt.single({
1097
- message: 'Select project template',
1098
- type: 'list',
1099
- choices: [
1100
- {
1101
- value: 'clean',
1102
- name: 'Clean project with no predefined schema types',
1103
- },
1104
- {
1105
- value: 'blog',
1106
- name: 'Blog (schema)',
1107
- },
1108
- {
1109
- value: 'shopify',
1110
- name: 'E-commerce (Shopify)',
1111
- },
1112
- {
1113
- value: 'moviedb',
1114
- name: 'Movie project (schema + sample data)',
1115
- },
1116
- ],
1117
- })
1118
- }
1119
-
1120
- async function updateProjectCliInitializedMetadata() {
1121
- try {
1122
- const client = apiClient({api: {projectId}})
1123
- const project = await client.request<SanityProject>({uri: `/projects/${projectId}`})
1124
-
1125
- if (!project?.metadata?.cliInitializedAt) {
1126
- await client.request({
1127
- method: 'PATCH',
1128
- uri: `/projects/${projectId}`,
1129
- body: {metadata: {cliInitializedAt: new Date().toISOString()}},
1130
- })
1131
- }
1132
- } catch {
1133
- // Non-critical update
1134
- debug('Failed to update cliInitializedAt metadata')
1135
- }
1136
- }
1137
-
1138
- async function bootstrapTemplate() {
1139
- const bootstrapVariables: GenerateConfigOptions['variables'] = {
1140
- autoUpdates,
1141
- dataset: datasetName,
1142
- projectId,
1143
- projectName: displayName || answers.projectName,
1144
- organizationId,
1145
- }
1146
-
1147
- if (remoteTemplateInfo) {
1148
- return bootstrapRemoteTemplate(
1149
- {
1150
- outputPath,
1151
- packageName: sluggedName,
1152
- repoInfo: remoteTemplateInfo,
1153
- bearerToken: cliFlags['template-token'],
1154
- variables: bootstrapVariables,
1155
- },
1156
- context,
1157
- )
1158
- }
1159
-
1160
- return bootstrapLocalTemplate(
1161
- {
1162
- outputPath,
1163
- packageName: sluggedName,
1164
- templateName,
1165
- schemaUrl,
1166
- useTypeScript,
1167
- variables: bootstrapVariables,
1168
- },
1169
- context,
1170
- )
1171
- }
1172
-
1173
- async function getProjectInfo(): Promise<ProjectDefaults & {outputPath: string}> {
1174
- const specifiedPath = flags['output-path'] && path.resolve(flags['output-path'])
1175
-
1176
- if (unattended || specifiedPath || env || initFramework) {
1177
- return {
1178
- ...defaults,
1179
- outputPath: specifiedPath || workDir,
1180
- }
1181
- }
1182
-
1183
- const workDirIsEmpty = (await fs.readdir(workDir)).length === 0
1184
- const projectOutputPath = await prompt.single({
1185
- type: 'input',
1186
- message: 'Project output path:',
1187
- default: workDirIsEmpty ? workDir : path.join(workDir, sluggedName),
1188
- validate: validateEmptyPath,
1189
- filter: absolutify,
1190
- })
1191
-
1192
- return {
1193
- ...defaults,
1194
- outputPath: projectOutputPath,
1195
- }
1196
- }
1197
-
1198
- // eslint-disable-next-line complexity
1199
- async function prepareFlags() {
1200
- const createProjectName = cliFlags['create-project']
1201
- if (cliFlags.dataset || cliFlags.visibility || cliFlags['dataset-default'] || unattended) {
1202
- showDefaultConfigPrompt = false
1203
- }
1204
-
1205
- if (cliFlags.project && createProjectName) {
1206
- throw new Error(
1207
- 'Both `--project` and `--create-project` specified, only a single is supported',
1208
- )
1209
- }
1210
-
1211
- if (cliFlags.project && cliFlags.organization) {
1212
- throw new Error(
1213
- 'You have specified both a project and an organization. To move a project to an organization please visit https://www.sanity.io/manage',
1214
- )
1215
- }
1216
-
1217
- if (
1218
- cliFlags.quickstart &&
1219
- (cliFlags.project || cliFlags.dataset || cliFlags.visibility || cliFlags.template)
1220
- ) {
1221
- const disallowed = ['project', 'dataset', 'visibility', 'template']
1222
- const usedDisallowed = disallowed.filter((flag) => cliFlags[flag as keyof InitFlags])
1223
- const usedDisallowedStr = usedDisallowed.map((flag) => `--${flag}`).join(', ')
1224
- throw new Error(`\`--quickstart\` cannot be combined with ${usedDisallowedStr}`)
1225
- }
1226
-
1227
- if (createProjectName === true) {
1228
- throw new Error('Please specify a project name (`--create-project <name>`)')
1229
- }
1230
-
1231
- if (typeof createProjectName === 'string' && createProjectName.trim().length === 0) {
1232
- throw new Error('Please specify a project name (`--create-project <name>`)')
1233
- }
1234
-
1235
- if (unattended) {
1236
- debug('Unattended mode, validating required options')
1237
-
1238
- if (!cliFlags['dataset' as const]) {
1239
- throw new Error(`\`--dataset\` must be specified in unattended mode`)
1240
- }
1241
-
1242
- // output-path is not used in unattended mode within nextjs
1243
- if (!isNextJs && !cliFlags['output-path' as const]) {
1244
- throw new Error(`\`--output-path\` must be specified in unattended mode`)
1245
- }
1246
-
1247
- if (!cliFlags.project && !createProjectName) {
1248
- throw new Error(
1249
- '`--project <id>` or `--create-project <name>` must be specified in unattended mode',
1250
- )
1251
- }
1252
-
1253
- if (createProjectName && !cliFlags.organization) {
1254
- throw new Error(
1255
- '--create-project is not supported in unattended mode without an organization, please specify an organization with `--organization <id>`',
1256
- )
1257
- }
1258
- }
1259
-
1260
- if (createProjectName) {
1261
- debug('--create-project specified, creating a new project')
1262
-
1263
- let orgForCreateProjectFlag = cliFlags.organization
1264
- if (!orgForCreateProjectFlag) {
1265
- debug('no organization specified, selecting one')
1266
- const client = apiClient({requireUser: true, requireProject: false})
1267
- const organizations = await client.request({uri: '/organizations'})
1268
- orgForCreateProjectFlag = await getOrganizationId(organizations)
1269
- }
1270
-
1271
- debug('creating a new project')
1272
- const createdProject = await createProject(apiClient, {
1273
- displayName: createProjectName.trim(),
1274
- organizationId: orgForCreateProjectFlag,
1275
- subscription: selectedPlan ? {planId: selectedPlan} : undefined,
1276
- metadata: {coupon: intendedCoupon},
1277
- })
1278
- debug('Project with ID %s created', createdProject.projectId)
1279
-
1280
- if (cliFlags.dataset) {
1281
- debug('--dataset specified, creating dataset (%s)', cliFlags.dataset)
1282
- const client = apiClient({api: {projectId: createdProject.projectId}})
1283
- const spinner = context.output.spinner('Creating dataset').start()
1284
-
1285
- const createBody = cliFlags.visibility
1286
- ? {aclMode: cliFlags.visibility as DatasetAclMode}
1287
- : {}
1288
-
1289
- await client.datasets.create(cliFlags.dataset, createBody)
1290
- spinner.succeed()
1291
- }
1292
-
1293
- const newFlags = {...cliFlags, project: createdProject.projectId}
1294
- delete newFlags['create-project']
1295
-
1296
- return newFlags
1297
- }
1298
-
1299
- return cliFlags
1300
- }
1301
-
1302
- async function createOrganization(
1303
- props: {name?: string} = {},
1304
- ): Promise<OrganizationCreateResponse> {
1305
- const name =
1306
- props.name ||
1307
- (await prompt.single({
1308
- type: 'input',
1309
- message: 'Organization name:',
1310
- default: user ? user.name : undefined,
1311
- validate(input) {
1312
- if (input.length === 0) {
1313
- return 'Organization name cannot be empty'
1314
- } else if (input.length > 100) {
1315
- return 'Organization name cannot be longer than 100 characters'
1316
- }
1317
- return true
1318
- },
1319
- }))
1320
-
1321
- const spinner = context.output.spinner('Creating organization').start()
1322
- const client = apiClient({requireProject: false, requireUser: true})
1323
- const organization = await client.request({
1324
- uri: '/organizations',
1325
- method: 'POST',
1326
- body: {name},
1327
- })
1328
- spinner.succeed()
1329
-
1330
- return organization
1331
- }
1332
-
1333
- async function getOrganizationIdForAppTemplate(organizations: ProjectOrganization[]) {
1334
- // If the user is using an app template, we don't need to check for attach access
1335
- const organizationChoices = [
1336
- ...organizations.map((organization) => ({
1337
- value: organization.id,
1338
- name: `${organization.name} [${organization.id}]`,
1339
- })),
1340
- new prompt.Separator(),
1341
- {value: '-new-', name: 'Create new organization'},
1342
- new prompt.Separator(),
1343
- ]
1344
-
1345
- // If the user only has a single organization, we'll default to that one.
1346
- const defaultOrganizationId =
1347
- organizations.length === 1
1348
- ? organizations[0].id
1349
- : organizations.find((org) => org.name === user?.name)?.id
1350
-
1351
- const chosenOrg = await prompt.single({
1352
- message: 'Select organization:',
1353
- type: 'list',
1354
- default: defaultOrganizationId || undefined,
1355
- choices: organizationChoices,
1356
- })
1357
-
1358
- if (chosenOrg === '-new-') {
1359
- return createOrganization().then((org) => org.id)
1360
- }
1361
-
1362
- return chosenOrg || undefined
1363
- }
1364
-
1365
- async function getOrganizationId(organizations: ProjectOrganization[]) {
1366
- // If the user has no organizations, prompt them to create one with the same name as
1367
- // their user, but allow them to customize it if they want
1368
- if (organizations.length === 0) {
1369
- return createOrganization().then((org) => org.id)
1370
- }
1371
-
1372
- // If the user has organizations, let them choose from them, but also allow them to
1373
- // create a new one in case they do not have access to any of them, or they want to
1374
- // create a personal/other organization.
1375
- debug(`User has ${organizations.length} organization(s), checking attach access`)
1376
- const withGrantInfo = await getOrganizationsWithAttachGrantInfo(organizations)
1377
- const withAttach = withGrantInfo.filter(({hasAttachGrant}) => hasAttachGrant)
1378
-
1379
- debug('User has attach access to %d organizations.', withAttach.length)
1380
- const organizationChoices = [
1381
- ...withGrantInfo.map(({organization, hasAttachGrant}) => ({
1382
- value: organization.id,
1383
- name: `${organization.name} [${organization.id}]`,
1384
- disabled: hasAttachGrant ? false : 'Insufficient permissions',
1385
- })),
1386
- new prompt.Separator(),
1387
- {value: '-new-', name: 'Create new organization'},
1388
- new prompt.Separator(),
1389
- ]
1390
-
1391
- // If the user only has a single organization (and they have attach access to it),
1392
- // we'll default to that one. Otherwise, we'll default to the organization with the
1393
- // same name as the user if it exists.
1394
- const defaultOrganizationId =
1395
- withAttach.length === 1
1396
- ? withAttach[0].organization.id
1397
- : organizations.find((org) => org.name === user?.name)?.id
1398
-
1399
- const chosenOrg = await prompt.single({
1400
- message: 'Select organization:',
1401
- type: 'list',
1402
- default: defaultOrganizationId || undefined,
1403
- choices: organizationChoices,
1404
- })
1405
-
1406
- if (chosenOrg === '-new-') {
1407
- return createOrganization().then((org) => org.id)
1408
- }
1409
-
1410
- return chosenOrg || undefined
1411
- }
1412
-
1413
- async function hasProjectAttachGrant(orgId: string) {
1414
- const requiredGrantGroup = 'sanity.organization.projects'
1415
- const requiredGrant = 'attach'
1416
-
1417
- const client = apiClient({requireProject: false, requireUser: true})
1418
- .clone()
1419
- .config({apiVersion: 'v2021-06-07'})
1420
-
1421
- try {
1422
- const grants = await client.request({uri: `organizations/${orgId}/grants`})
1423
- const group: {grants: {name: string}[]}[] = grants[requiredGrantGroup] || []
1424
- return group.some(
1425
- (resource) =>
1426
- resource.grants && resource.grants.some((grant) => grant.name === requiredGrant),
1427
- )
1428
- } catch (err) {
1429
- // If we get a 401, it means we don't have access to this organization
1430
- // probably because of implicit membership
1431
- if (err.statusCode === 401) {
1432
- debug('No access to organization %s (401)', orgId)
1433
- return false
1434
- }
1435
- // For other errors, log them but still return false
1436
- debug('Error checking grants for organization %s: %s', orgId, err.message)
1437
- return false
1438
- }
1439
- }
1440
-
1441
- function getOrganizationsWithAttachGrantInfo(organizations: ProjectOrganization[]) {
1442
- return pMap(
1443
- organizations,
1444
- async (organization) => ({
1445
- hasAttachGrant: await hasProjectAttachGrant(organization.id),
1446
- organization,
1447
- }),
1448
- {concurrency: 3},
1449
- )
1450
- }
1451
-
1452
- async function createOrAppendEnvVars(
1453
- filename: string,
1454
- framework: Framework | null,
1455
- options?: {log?: boolean},
1456
- ) {
1457
- // we will prepend SANITY_ to these variables later, together with the prefix
1458
- const envVars = {
1459
- PROJECT_ID: projectId,
1460
- DATASET: datasetName,
1461
- }
1462
-
1463
- try {
1464
- if (framework && framework.envPrefix && !options?.log) {
1465
- print(
1466
- `\nDetected framework ${chalk.blue(framework?.name)}, using prefix '${
1467
- framework.envPrefix
1468
- }'`,
1469
- )
1470
- }
1471
-
1472
- await writeEnvVarsToFile(filename, envVars, {
1473
- framework,
1474
- outputPath,
1475
- log: options?.log,
1476
- })
1477
- } catch (err) {
1478
- print(err)
1479
- throw new Error('An error occurred while creating .env', {cause: err})
1480
- }
1481
- }
1482
-
1483
- async function writeEnvVarsToFile(
1484
- filename: string,
1485
- envVars: Record<string, string>,
1486
- options: {framework: Framework | null; outputPath: string; log?: boolean},
1487
- ) {
1488
- const envPrefix = options.framework?.envPrefix || ''
1489
- const keyPrefix = envPrefix.includes('SANITY') ? envPrefix : `${envPrefix}SANITY_`
1490
- const fileOutputPath = path.join(options.outputPath, filename)
1491
-
1492
- // prepend framework and sanity prefix to envVars
1493
- for (const key of Object.keys(envVars)) {
1494
- envVars[`${keyPrefix}${key}`] = envVars[key]
1495
- delete envVars[key]
1496
- }
1497
-
1498
- // make folder if not exists (if output path is specified)
1499
- await fs
1500
- .mkdir(options.outputPath, {recursive: true})
1501
- .catch(() => debug('Error creating folder %s', options.outputPath))
1502
-
1503
- // time to update or create the file
1504
- const existingEnv = await fs
1505
- .readFile(fileOutputPath, {encoding: 'utf8'})
1506
- .catch((err) => (err.code === 'ENOENT' ? '' : Promise.reject(err)))
1507
-
1508
- const updatedEnv = parseAndUpdateEnvVars(existingEnv, envVars, {
1509
- log: options.log,
1510
- })
1511
-
1512
- const warningComment = [
1513
- '# Warning: Do not add secrets (API keys and similar) to this file, as it source controlled!',
1514
- '# Use `.env.local` for any secrets, and ensure it is not added to source control',
1515
- ].join('\n')
1516
- const shouldPrependWarning = filename !== '.env.local' && !existingEnv.includes(warningComment)
1517
- if (shouldPrependWarning) {
1518
- await fs.writeFile(fileOutputPath, `${warningComment}\n\n${updatedEnv}`, {
1519
- encoding: 'utf8',
1520
- })
1521
- return
1522
- }
1523
-
1524
- await fs.writeFile(fileOutputPath, updatedEnv, {
1525
- encoding: 'utf8',
1526
- })
1527
-
1528
- if (!options.log) {
1529
- print(`\n${chalk.green('Success!')} Environment variables written to ${fileOutputPath}`)
1530
- }
1531
- }
1532
-
1533
- function parseAndUpdateEnvVars(
1534
- fileContents: string,
1535
- envVars: Record<string, string>,
1536
- options?: {
1537
- log?: boolean
1538
- },
1539
- ): string {
1540
- const existingKeys = dotenv.parse(fileContents) // this will contain all vars
1541
- const updatedKeys: Record<string, string> = {} // this will only contain our vars
1542
-
1543
- // find and update keys
1544
- for (const [key, value] of Object.entries(envVars)) {
1545
- if (!existingKeys[key]) {
1546
- updatedKeys[key] = value
1547
- if (!options?.log) {
1548
- print(`Appended ${key}="${envVars[key]}"`)
1549
- }
1550
- continue
1551
- }
1552
-
1553
- if (!options?.log) {
1554
- print(`Found existing ${key}, replacing value.`)
1555
- }
1556
- updatedKeys[key] = value
1557
- }
1558
-
1559
- // clone fileContents and replace existing keys by string matching
1560
- let updatedEnv = fileContents
1561
- for (const [key, value] of Object.entries(updatedKeys)) {
1562
- if (existingKeys[key]) {
1563
- const existingValue = existingKeys[key]
1564
-
1565
- updatedEnv = updatedEnv
1566
- .split('\n')
1567
- .map((line) => {
1568
- if (
1569
- !line.trim().startsWith('#') && // ignore comments
1570
- new RegExp(`(^\\s*${key})((: )|( *=))`).test(line) // match key
1571
- ) {
1572
- return line.replace(existingValue, value)
1573
- }
1574
- return line
1575
- })
1576
- .join('\n')
1577
- } else {
1578
- updatedEnv = updatedEnv.trim().concat(`\n${key}="${value}"`)
1579
- }
1580
- }
1581
-
1582
- // if file is empty, add a newline
1583
- return updatedEnv.concat(fileContents === '' ? '\n' : '')
1584
- }
1585
- }
1586
-
1587
- function doDatasetImport(options: {
1588
- projectId: string
1589
- outputPath: string
1590
- template: ProjectTemplate
1591
- datasetName: string
1592
- context: CliCommandContext
1593
- importCommand: CliCommandDefinition | undefined
1594
- }): Promise<unknown> {
1595
- const {outputPath, importCommand, template, datasetName, projectId, context} = options
1596
- if (!template.datasetUrl) {
1597
- return Promise.resolve(undefined)
1598
- }
1599
-
1600
- if (!importCommand) {
1601
- throw new Error('Failed to find `sanity dataset import` command')
1602
- }
1603
-
1604
- const commandArgs: CliCommandArguments = {
1605
- argv: [template.datasetUrl, datasetName],
1606
- argsWithoutOptions: [template.datasetUrl, datasetName],
1607
- extOptions: {},
1608
- groupOrCommand: 'import',
1609
- extraArguments: [],
1610
- }
1611
-
1612
- const configPath = context.cliConfigPath || path.join(outputPath, 'sanity.cli.js')
1613
- const apiClient = getClientWrapper({projectId, dataset: datasetName}, configPath)
1614
-
1615
- const commandContext: CliCommandContext & {fromInitCommand: boolean} = {
1616
- ...context,
1617
- apiClient,
1618
- workDir: outputPath,
1619
- fromInitCommand: true,
1620
- }
1621
-
1622
- return importCommand.action(commandArgs, commandContext)
1623
- }
1624
-
1625
- async function getPlanFromCoupon(apiClient: CliApiClient, couponCode: string): Promise<string> {
1626
- const response = await apiClient({
1627
- requireUser: false,
1628
- requireProject: false,
1629
- }).request({
1630
- method: 'GET',
1631
- uri: `plans/coupon/${couponCode}`,
1632
- })
1633
-
1634
- const planId = response[0].id
1635
- if (!planId) {
1636
- throw new Error('Unable to find a plan from coupon code')
1637
- }
1638
- return planId
1639
- }
1640
-
1641
- async function getUserData(apiClient: CliApiClient): Promise<SanityUser> {
1642
- return await apiClient({
1643
- requireUser: true,
1644
- requireProject: false,
1645
- }).request({
1646
- method: 'GET',
1647
- uri: 'users/me',
1648
- })
1649
- }
1650
-
1651
- async function getPlanFromId(apiClient: CliApiClient, planId: string): Promise<string> {
1652
- const response = await apiClient({
1653
- requireUser: false,
1654
- requireProject: false,
1655
- }).request({
1656
- method: 'GET',
1657
- uri: `plans/${planId}`,
1658
- })
1659
-
1660
- const id = response[0].id
1661
- if (!id) {
1662
- throw new Error(`Unable to find a plan with id ${planId}`)
1663
- }
1664
- return id
1665
- }
1666
-
1667
- function getImportCommand(
1668
- outputPath: string,
1669
- studioVersion: 2 | 3,
1670
- ): CliCommandDefinition | undefined {
1671
- if (studioVersion === 3) {
1672
- const pkgPath = resolveFrom.silent(outputPath, 'sanity/_internal')
1673
- if (!pkgPath) {
1674
- throw new Error('Failed to resolve `sanity` module - problem with dependency installation?')
1675
- }
1676
-
1677
- debug('`sanity` module path resolved to %s (from %s)', pkgPath, outputPath)
1678
- const cliInternals = dynamicRequire<SanityModuleInternal>(pkgPath)
1679
- if (!('cliProjectCommands' in cliInternals)) {
1680
- throw new Error('Incorrect version of the `sanity` module installed')
1681
- }
1682
-
1683
- return cliInternals.cliProjectCommands.commands.find(
1684
- (cmd): cmd is CliCommandDefinition =>
1685
- !isCommandGroup(cmd) && cmd.name === 'import' && cmd.group === 'dataset',
1686
- )
1687
- }
1688
-
1689
- const pkgPath = resolveFrom.silent(outputPath, '@sanity/core')
1690
- if (!pkgPath) {
1691
- throw new Error(
1692
- 'Failed to resolve `@sanity/core` module - problem with dependency installation?',
1693
- )
1694
- }
1695
-
1696
- debug('`@sanity/core` module path resolved to %s (from %s)', pkgPath, outputPath)
1697
- const coreModule = dynamicRequire<SanityCore>(pkgPath)
1698
- const coreCommands = coreModule && coreModule.commands
1699
-
1700
- if (!coreCommands || !Array.isArray(coreCommands)) {
1701
- throw new Error('Incorrect version of the `@sanity/core` module installed')
1702
- }
1703
-
1704
- return coreCommands.find(
1705
- (cmd): cmd is CliCommandDefinition =>
1706
- !isCommandGroup(cmd) && cmd.name === 'import' && cmd.group === 'dataset',
1707
- )
1708
- }