@sanity/cli 3.65.2-corel.472 → 3.66.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.
@@ -1,446 +0,0 @@
1
- import path from 'node:path'
2
-
3
- import {type DatasetAclMode} from '@sanity/client'
4
- import {noop} from 'lodash'
5
-
6
- import {type InitFlags} from '../../commands/init/initCommand'
7
- import {debug} from '../../debug'
8
- import {type CliCommandArguments, type CliCommandContext, type SanityJson} from '../../types'
9
- import {getUserConfig} from '../../util/getUserConfig'
10
- import {pathExists} from '../../util/pathExists'
11
- import {readJson} from '../../util/readJson'
12
- import {writeJson} from '../../util/writeJson'
13
- import {login, type LoginFlags} from '../login/login'
14
- import {createProject} from '../project/createProject'
15
- import {promptForDatasetName} from './promptForDatasetName'
16
- import {promptForAclMode, promptForDefaultConfig} from './prompts'
17
-
18
- /* eslint-disable no-process-env */
19
- const isCI = process.env.CI
20
- /* eslint-enable no-process-env */
21
-
22
- // eslint-disable-next-line max-statements, complexity
23
- export async function reconfigureV2Project(
24
- args: CliCommandArguments<InitFlags>,
25
- context: CliCommandContext,
26
- ): Promise<void> {
27
- const {output, prompt, workDir, apiClient, yarn, chalk} = context
28
- const cliFlags = args.extOptions
29
- const unattended = cliFlags.y || cliFlags.yes
30
- const print = unattended ? noop : output.print
31
-
32
- let defaultConfig = cliFlags['dataset-default']
33
- let showDefaultConfigPrompt = !defaultConfig
34
-
35
- let selectedPlan: string | undefined
36
-
37
- // Check if we have a project manifest already
38
- const manifestPath = path.join(workDir, 'sanity.json')
39
- let projectManifest = await readJson<SanityJson>(manifestPath)
40
-
41
- // If we are in a Sanity studio project folder and the project manifest has projectId/dataset,
42
- // ASK if we want to reconfigure. If no projectId/dataset is present, we assume reconfigure
43
- const hasProjectId = projectManifest && projectManifest.api && projectManifest.api.projectId
44
-
45
- print(`The Sanity Studio in this folder will be tied to a new project on Sanity.io!`)
46
- if (hasProjectId) {
47
- print('The previous project configuration will be overwritten.')
48
- }
49
- print(`We're first going to make sure you have an account with Sanity.io. Hang on.`)
50
- print('Press ctrl + C at any time to quit.\n')
51
-
52
- // If the user isn't already authenticated, make it so
53
- const userConfig = getUserConfig()
54
- const hasToken = userConfig.get('authToken')
55
-
56
- debug(hasToken ? 'User already has a token' : 'User has no token')
57
-
58
- if (hasToken) {
59
- print('Looks like you already have a Sanity-account. Sweet!\n')
60
- } else if (!unattended) {
61
- await getOrCreateUser()
62
- }
63
-
64
- const flags = await prepareFlags()
65
-
66
- // We're authenticated, now lets select or create a project
67
- debug('Prompting user to select or create a project')
68
- const {projectId, displayName, isFirstProject} = await getOrCreateProject()
69
- debug(`Project with name ${displayName} selected`)
70
-
71
- // Now let's pick or create a dataset
72
- debug('Prompting user to select or create a dataset')
73
- const {datasetName} = await getOrCreateDataset({
74
- projectId,
75
- displayName,
76
- dataset: flags.dataset,
77
- aclMode: flags.visibility,
78
- defaultConfig: flags['dataset-default'],
79
- })
80
-
81
- debug(`Dataset with name ${datasetName} selected`)
82
-
83
- const outputPath = workDir
84
- let successMessage
85
-
86
- // Rewrite project manifest (sanity.json)
87
- const projectInfo = projectManifest.project || {}
88
- const newProps = {
89
- root: true,
90
- api: {
91
- ...(projectManifest.api || {}),
92
- projectId,
93
- dataset: datasetName,
94
- },
95
- project: {
96
- ...projectInfo,
97
- // Keep original name if present
98
- name: projectInfo.name || displayName,
99
- },
100
- }
101
-
102
- // Ensure root, api and project keys are at top to follow sanity.json key order convention
103
- projectManifest = {
104
- ...newProps,
105
- ...projectManifest,
106
- ...newProps,
107
- }
108
-
109
- await writeJson(manifestPath, projectManifest)
110
-
111
- const hasNodeModules = await pathExists(path.join(workDir, 'node_modules'))
112
- if (hasNodeModules) {
113
- print('Skipping installation of dependencies since node_modules exists.')
114
- print('Run sanity install to reinstall dependencies')
115
- } else {
116
- try {
117
- await yarn(['install'], {...output, rootDir: workDir})
118
- } catch (err) {
119
- throw err
120
- }
121
- }
122
-
123
- print(`\n${chalk.green('Success!')} Now what?\n`)
124
-
125
- const isCurrentDir = outputPath === process.cwd()
126
- if (!isCurrentDir) {
127
- print(`▪ ${chalk.cyan(`cd ${outputPath}`)}, then:`)
128
- }
129
-
130
- print(`▪ ${chalk.cyan('sanity docs')} to open the documentation in a browser`)
131
- print(`▪ ${chalk.cyan('sanity manage')} to open the project settings in a browser`)
132
- print(`▪ ${chalk.cyan('sanity help')} to explore the CLI manual`)
133
- print(`▪ ${chalk.green('sanity start')} to run your studio\n`) // v2 uses `start`, not `dev`
134
-
135
- if (successMessage) {
136
- print(`\n${successMessage}`)
137
- }
138
-
139
- const sendInvite =
140
- isFirstProject &&
141
- (await prompt.single({
142
- type: 'confirm',
143
- message:
144
- 'We have an excellent developer community, would you like us to send you an invitation to join?',
145
- default: true,
146
- }))
147
-
148
- if (sendInvite) {
149
- // Intentionally leave the promise "dangling" since we don't want to stall while waiting for this
150
- apiClient({requireProject: false})
151
- .request({
152
- uri: '/invitations/community',
153
- method: 'POST',
154
- })
155
- .catch(noop)
156
- }
157
-
158
- async function getOrCreateUser() {
159
- print(`We can't find any auth credentials in your Sanity config`)
160
- print('- log in or create a new account\n')
161
-
162
- // Provide login options (`sanity login`)
163
- const {extOptions, ...otherArgs} = args
164
- const loginArgs: CliCommandArguments<LoginFlags> = {...otherArgs, extOptions: {}}
165
- await login(loginArgs, context)
166
-
167
- print("Good stuff, you're now authenticated. You'll need a project to keep your")
168
- print('datasets and collaborators safe and snug.')
169
- }
170
-
171
- async function getOrCreateProject(): Promise<{
172
- projectId: string
173
- displayName: string
174
- isFirstProject: boolean
175
- }> {
176
- let projects
177
- try {
178
- projects = await apiClient({requireProject: false}).projects.list({includeMembers: false})
179
- } catch (err) {
180
- if (unattended && flags.project) {
181
- return {projectId: flags.project, displayName: 'Unknown project', isFirstProject: false}
182
- }
183
-
184
- throw new Error(`Failed to communicate with the Sanity API:\n${err.message}`)
185
- }
186
-
187
- if (projects.length === 0 && unattended) {
188
- throw new Error('No projects found for current user')
189
- }
190
-
191
- if (flags.project) {
192
- const project = projects.find((proj) => proj.id === flags.project)
193
- if (!project && !unattended) {
194
- throw new Error(
195
- `Given project ID (${flags.project}) not found, or you do not have access to it`,
196
- )
197
- }
198
-
199
- return {
200
- projectId: flags.project,
201
- displayName: project ? project.displayName : 'Unknown project',
202
- isFirstProject: false,
203
- }
204
- }
205
-
206
- // If the user has no projects or is using a coupon (which can only be applied to new projects)
207
- // just ask for project details instead of showing a list of projects
208
- const isUsersFirstProject = projects.length === 0
209
- if (isUsersFirstProject) {
210
- debug('No projects found for user, prompting for name')
211
-
212
- const projectName = await prompt.single({type: 'input', message: 'Project name:'})
213
- return createProject(apiClient, {
214
- displayName: projectName,
215
- subscription: selectedPlan ? {planId: selectedPlan} : undefined,
216
- }).then((response) => ({
217
- ...response,
218
- isFirstProject: isUsersFirstProject,
219
- }))
220
- }
221
-
222
- debug(`User has ${projects.length} project(s) already, showing list of choices`)
223
-
224
- const projectChoices = projects.map((project) => ({
225
- value: project.id,
226
- name: `${project.displayName} [${project.id}]`,
227
- }))
228
-
229
- const selected = await prompt.single({
230
- message: 'Select project to use',
231
- type: 'list',
232
- choices: [
233
- {value: 'new', name: 'Create new project'},
234
- new prompt.Separator(),
235
- ...projectChoices,
236
- ],
237
- })
238
-
239
- if (selected === 'new') {
240
- debug('User wants to create a new project, prompting for name')
241
- return createProject(apiClient, {
242
- displayName: await prompt.single({
243
- type: 'input',
244
- message: 'Your project name:',
245
- default: 'My Sanity Project',
246
- }),
247
- subscription: selectedPlan ? {planId: selectedPlan} : undefined,
248
- }).then((response) => ({
249
- ...response,
250
- isFirstProject: isUsersFirstProject,
251
- }))
252
- }
253
-
254
- debug(`Returning selected project (${selected})`)
255
- return {
256
- projectId: selected,
257
- displayName: projects.find((proj) => proj.id === selected)?.displayName || '',
258
- isFirstProject: isUsersFirstProject,
259
- }
260
- }
261
-
262
- async function getOrCreateDataset(opts: {
263
- projectId: string
264
- displayName: string
265
- dataset?: string
266
- aclMode?: string
267
- defaultConfig?: boolean
268
- }) {
269
- if (opts.dataset && isCI) {
270
- return {datasetName: opts.dataset}
271
- }
272
-
273
- const client = apiClient({api: {projectId: opts.projectId}})
274
- const [datasets, projectFeatures] = await Promise.all([
275
- client.datasets.list(),
276
- client.request({uri: '/features'}),
277
- ])
278
-
279
- const privateDatasetsAllowed = projectFeatures.includes('privateDataset')
280
- const allowedModes = privateDatasetsAllowed ? ['public', 'private'] : ['public']
281
-
282
- if (opts.aclMode && !allowedModes.includes(opts.aclMode)) {
283
- throw new Error(`Visibility mode "${opts.aclMode}" not allowed`)
284
- }
285
-
286
- // Getter in order to present prompts in a more logical order
287
- const getAclMode = async (): Promise<string> => {
288
- if (opts.aclMode) {
289
- return opts.aclMode
290
- }
291
-
292
- if (unattended || !privateDatasetsAllowed || defaultConfig) {
293
- return 'public'
294
- }
295
-
296
- if (privateDatasetsAllowed) {
297
- const mode = await promptForAclMode(prompt, output)
298
- return mode
299
- }
300
-
301
- return 'public'
302
- }
303
-
304
- if (opts.dataset) {
305
- debug('User has specified dataset through a flag (%s)', opts.dataset)
306
- const existing = datasets.find((ds) => ds.name === opts.dataset)
307
-
308
- if (!existing) {
309
- debug('Specified dataset not found, creating it')
310
- const aclMode = await getAclMode()
311
- const spinner = context.output.spinner('Creating dataset').start()
312
- await client.datasets.create(opts.dataset, {aclMode: aclMode as DatasetAclMode})
313
- spinner.succeed()
314
- }
315
-
316
- return {datasetName: opts.dataset}
317
- }
318
-
319
- const datasetInfo =
320
- '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".'
321
-
322
- if (datasets.length === 0) {
323
- debug('No datasets found for project, prompting for name')
324
- if (showDefaultConfigPrompt) {
325
- output.print(datasetInfo)
326
- defaultConfig = await promptForDefaultConfig(prompt)
327
- }
328
- const name = defaultConfig
329
- ? 'production'
330
- : await promptForDatasetName(prompt, {
331
- message: 'Name of your first dataset:',
332
- })
333
- const aclMode = await getAclMode()
334
- const spinner = context.output.spinner('Creating dataset').start()
335
- await client.datasets.create(name, {aclMode: aclMode as DatasetAclMode})
336
- spinner.succeed()
337
- return {datasetName: name}
338
- }
339
-
340
- debug(`User has ${datasets.length} dataset(s) already, showing list of choices`)
341
- const datasetChoices = datasets.map((dataset) => ({value: dataset.name}))
342
-
343
- const selected = await prompt.single({
344
- message: 'Select dataset to use',
345
- type: 'list',
346
- choices: [
347
- {value: 'new', name: 'Create new dataset'},
348
- new prompt.Separator(),
349
- ...datasetChoices,
350
- ],
351
- })
352
-
353
- if (selected === 'new') {
354
- const existingDatasetNames = datasets.map((ds) => ds.name)
355
- debug('User wants to create a new dataset, prompting for name')
356
- if (showDefaultConfigPrompt && !existingDatasetNames.includes('production')) {
357
- output.print(datasetInfo)
358
- defaultConfig = await promptForDefaultConfig(prompt)
359
- }
360
-
361
- const newDatasetName = defaultConfig
362
- ? 'production'
363
- : await promptForDatasetName(
364
- prompt,
365
- {
366
- message: 'Dataset name:',
367
- },
368
- existingDatasetNames,
369
- )
370
- const aclMode = await getAclMode()
371
- const spinner = context.output.spinner('Creating dataset').start()
372
- await client.datasets.create(newDatasetName, {aclMode: aclMode as DatasetAclMode})
373
- spinner.succeed()
374
- return {datasetName: newDatasetName}
375
- }
376
-
377
- debug(`Returning selected dataset (${selected})`)
378
- return {datasetName: selected}
379
- }
380
-
381
- async function prepareFlags() {
382
- const createProjectName = cliFlags['create-project']
383
- if (cliFlags.dataset || cliFlags.visibility || cliFlags['dataset-default'] || unattended) {
384
- showDefaultConfigPrompt = false
385
- }
386
-
387
- if (cliFlags.project && createProjectName) {
388
- throw new Error(
389
- 'Both `--project` and `--create-project` specified, only a single is supported',
390
- )
391
- }
392
-
393
- if (createProjectName === true) {
394
- throw new Error('Please specify a project name (`--create-project <name>`)')
395
- }
396
-
397
- if (typeof createProjectName === 'string' && createProjectName.trim().length === 0) {
398
- throw new Error('Please specify a project name (`--create-project <name>`)')
399
- }
400
-
401
- if (unattended) {
402
- debug('Unattended mode, validating required options')
403
- const requiredForUnattended = ['dataset', 'output-path'] as const
404
- requiredForUnattended.forEach((flag) => {
405
- if (!cliFlags[flag]) {
406
- throw new Error(`\`--${flag}\` must be specified in unattended mode`)
407
- }
408
- })
409
-
410
- if (!cliFlags.project && !createProjectName) {
411
- throw new Error(
412
- '`--project <id>` or `--create-project <name>` must be specified in unattended mode',
413
- )
414
- }
415
- }
416
-
417
- if (createProjectName) {
418
- debug('--create-project specified, creating a new project')
419
- const createdProject = await createProject(apiClient, {
420
- displayName: createProjectName.trim(),
421
- subscription: selectedPlan ? {planId: selectedPlan} : undefined,
422
- })
423
- debug('Project with ID %s created', createdProject.projectId)
424
-
425
- if (cliFlags.dataset) {
426
- debug('--dataset specified, creating dataset (%s)', cliFlags.dataset)
427
- const client = apiClient({api: {projectId: createdProject.projectId}})
428
- const spinner = context.output.spinner('Creating dataset').start()
429
-
430
- const createBody = cliFlags.visibility
431
- ? {aclMode: cliFlags.visibility as DatasetAclMode}
432
- : {}
433
-
434
- await client.datasets.create(cliFlags.dataset, createBody)
435
- spinner.succeed()
436
- }
437
-
438
- const newFlags = {...cliFlags, project: createdProject.projectId}
439
- delete newFlags['create-project']
440
-
441
- return newFlags
442
- }
443
-
444
- return cliFlags
445
- }
446
- }
@@ -1,38 +0,0 @@
1
- import {type CliCommandDefinition} from '../../types'
2
- import upgradeDependencies from './upgradeDependencies'
3
-
4
- const helpText = `
5
- Upgrades installed Sanity modules to the latest available version within the
6
- semantic versioning range specified in "package.json".
7
-
8
- If a specific module name is provided, only that module will be upgraded.
9
-
10
- Options
11
- --range [range] Version range to upgrade to, eg '^2.2.7' or '2.1.x'
12
- --tag [tag] Tagged release to upgrade to, eg 'canary' or 'some-feature'
13
- --save-exact Pin the resolved version numbers in package.json (no ^ prefix)
14
-
15
- Examples
16
- # Upgrade modules to the latest semver compatible versions
17
- sanity upgrade
18
-
19
- # Update to the latest within the 2.2 range
20
- sanity upgrade --range 2.2.x
21
-
22
- # Update to the latest semver compatible versions and pin the versions
23
- sanity upgrade --save-exact
24
-
25
- # Update to the latest 'canary' npm tag
26
- sanity upgrade --tag canary
27
- `
28
-
29
- const upgradeCommand: CliCommandDefinition = {
30
- name: 'upgrade',
31
- signature: '[--tag DIST_TAG] [--range SEMVER_RANGE] [--save-exact]',
32
- description: 'Upgrades all (or some) Sanity modules to their latest versions',
33
- action: upgradeDependencies,
34
- hideFromHelp: true,
35
- helpText,
36
- }
37
-
38
- export default upgradeCommand