@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.
- package/lib/_chunks-cjs/cli.js +5 -5
- package/lib/_chunks-cjs/cli.js.map +1 -1
- package/lib/_chunks-cjs/cliWorker.js.map +1 -1
- package/lib/_chunks-cjs/generateAction.js.map +1 -1
- package/lib/_chunks-cjs/getCliConfig.js.map +1 -1
- package/lib/_chunks-cjs/journeyConfig.js.map +1 -1
- package/lib/_chunks-cjs/loadEnv.js.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/index.mjs.map +1 -1
- package/lib/run.js.map +1 -1
- package/lib/workers/getAndWriteJourneySchema.js.map +1 -1
- package/lib/workers/getCliConfig.js.map +1 -1
- package/lib/workers/typegenGenerate.js.map +1 -1
- package/package.json +6 -7
- package/templates/blog/README.md +1 -1
- package/templates/clean/README.md +1 -1
- package/templates/ecommerce/README.md +1 -1
- package/templates/get-started/README.md +1 -1
- package/templates/moviedb/README.md +1 -1
- package/templates/quickstart/README.md +1 -1
- package/src/CommandRunner.ts +0 -219
- package/src/__telemetry__/cli.telemetry.ts +0 -29
- package/src/__telemetry__/init.telemetry.ts +0 -95
- package/src/actions/codemod/codemodAction.ts +0 -109
- package/src/actions/codemod/mods/deskRename.ts +0 -19
- package/src/actions/codemod/mods/index.ts +0 -9
- package/src/actions/codemod/mods/partsTypeDirective.ts +0 -28
- package/src/actions/codemod/mods/reactIconsV3.ts +0 -54
- package/src/actions/codemod/types.ts +0 -10
- package/src/actions/init-plugin/bootstrapFromTemplate.ts +0 -196
- package/src/actions/init-project/bootstrapLocalTemplate.ts +0 -201
- package/src/actions/init-project/bootstrapRemoteTemplate.ts +0 -118
- package/src/actions/init-project/createAppCliConfig.ts +0 -24
- package/src/actions/init-project/createCliConfig.ts +0 -31
- package/src/actions/init-project/createPackageManifest.ts +0 -102
- package/src/actions/init-project/createStudioConfig.ts +0 -53
- package/src/actions/init-project/determineAppTemplate.ts +0 -13
- package/src/actions/init-project/fsUtils.ts +0 -40
- package/src/actions/init-project/git.ts +0 -59
- package/src/actions/init-project/initProject.ts +0 -1708
- package/src/actions/init-project/processTemplate.ts +0 -55
- package/src/actions/init-project/promptForDatasetName.ts +0 -68
- package/src/actions/init-project/prompts/index.ts +0 -51
- package/src/actions/init-project/prompts/nextjs.ts +0 -59
- package/src/actions/init-project/readPackageJson.ts +0 -17
- package/src/actions/init-project/templates/appQuickstart.ts +0 -23
- package/src/actions/init-project/templates/appSanityUi.ts +0 -25
- package/src/actions/init-project/templates/blog.ts +0 -5
- package/src/actions/init-project/templates/clean.ts +0 -5
- package/src/actions/init-project/templates/getStarted.ts +0 -37
- package/src/actions/init-project/templates/index.ts +0 -24
- package/src/actions/init-project/templates/moviedb.ts +0 -35
- package/src/actions/init-project/templates/nextjs/index.ts +0 -238
- package/src/actions/init-project/templates/nextjs/schemaTypes/blog.ts +0 -252
- package/src/actions/init-project/templates/quickstart.ts +0 -5
- package/src/actions/init-project/templates/shopify.ts +0 -78
- package/src/actions/init-project/templates/shopifyOnline.ts +0 -50
- package/src/actions/init-project/updateInitialTemplateMetadata.ts +0 -24
- package/src/actions/login/login.telemetry.ts +0 -18
- package/src/actions/login/login.ts +0 -350
- package/src/actions/login/types.ts +0 -19
- package/src/actions/project/createProject.ts +0 -36
- package/src/actions/telemetry/setTelemetryConsent.ts +0 -101
- package/src/actions/typegen/generate.telemetry.ts +0 -20
- package/src/actions/typegen/generateAction.ts +0 -210
- package/src/actions/versions/findSanityModuleVersions.ts +0 -185
- package/src/actions/yarn/events.ts +0 -73
- package/src/cli.ts +0 -350
- package/src/cliClient.ts +0 -62
- package/src/commands/blueprints/addBlueprintsCommand.ts +0 -155
- package/src/commands/blueprints/blueprintsGroup.ts +0 -10
- package/src/commands/blueprints/configBlueprintsCommand.ts +0 -86
- package/src/commands/blueprints/deployBlueprintsCommand.ts +0 -63
- package/src/commands/blueprints/destroyBlueprintsCommand.ts +0 -78
- package/src/commands/blueprints/infoBlueprintsCommand.ts +0 -55
- package/src/commands/blueprints/initBlueprintsCommand.ts +0 -99
- package/src/commands/blueprints/logsBlueprintsCommand.ts +0 -66
- package/src/commands/blueprints/planBlueprintsCommand.ts +0 -51
- package/src/commands/blueprints/stacksBlueprintsCommand.ts +0 -66
- package/src/commands/codemod/codemodCommand.ts +0 -32
- package/src/commands/debug/debugCommand.ts +0 -26
- package/src/commands/debug/printDebugInfo.ts +0 -243
- package/src/commands/docs/docsCommand.ts +0 -20
- package/src/commands/functions/devFunctionsCommand.ts +0 -73
- package/src/commands/functions/envFunctionsCommand.ts +0 -105
- package/src/commands/functions/functionsGroup.ts +0 -10
- package/src/commands/functions/logsFunctionsCommand.ts +0 -94
- package/src/commands/functions/testFunctionsCommand.ts +0 -93
- package/src/commands/help/helpCommand.ts +0 -21
- package/src/commands/help/showHelp.ts +0 -58
- package/src/commands/index.ts +0 -72
- package/src/commands/init/initCommand.ts +0 -123
- package/src/commands/install/installCommand.ts +0 -38
- package/src/commands/learn/learnCommand.ts +0 -20
- package/src/commands/login/loginCommand.ts +0 -29
- package/src/commands/logout/logoutCommand.ts +0 -52
- package/src/commands/manage/manageCommand.ts +0 -24
- package/src/commands/projects/listProjectsCommand.ts +0 -62
- package/src/commands/projects/projectsGroup.ts +0 -10
- package/src/commands/telemetry/disableTelemetryCommand.ts +0 -19
- package/src/commands/telemetry/enableTelemetryCommand.ts +0 -19
- package/src/commands/telemetry/telemetryGroup.ts +0 -10
- package/src/commands/telemetry/telemetryStatusCommand.ts +0 -89
- package/src/commands/typegen/generateTypesCommand.ts +0 -56
- package/src/commands/typegen/typegenGroup.ts +0 -10
- package/src/commands/versions/printVersionResult.ts +0 -51
- package/src/commands/versions/versionsCommand.ts +0 -13
- package/src/config.ts +0 -14
- package/src/debug.ts +0 -3
- package/src/index.ts +0 -6
- package/src/outputters/cliOutputter.ts +0 -47
- package/src/packageManager/getInstallCommand.ts +0 -26
- package/src/packageManager/getUpgradeCommand.ts +0 -52
- package/src/packageManager/index.ts +0 -33
- package/src/packageManager/installPackages.ts +0 -99
- package/src/packageManager/packageManagerChoice.ts +0 -204
- package/src/packageManager/uninstallPackages.ts +0 -44
- package/src/packageManager/yarnStub.ts +0 -22
- package/src/prompters/cliPrompter.ts +0 -9
- package/src/run.ts +0 -8
- package/src/studioDependencies.ts +0 -25
- package/src/types.ts +0 -387
- package/src/util/canLaunchBrowser.ts +0 -12
- package/src/util/cliWorker.ts +0 -34
- package/src/util/clientWrapper.ts +0 -119
- package/src/util/copy.ts +0 -50
- package/src/util/createExpiringConfig.ts +0 -80
- package/src/util/createTelemetryStore.ts +0 -204
- package/src/util/detectRuntime.ts +0 -12
- package/src/util/dynamicRequire.ts +0 -17
- package/src/util/frameworkPort.ts +0 -63
- package/src/util/generateCommandsDocumentation.ts +0 -62
- package/src/util/getCliConfig.ts +0 -172
- package/src/util/getCliVersion.ts +0 -22
- package/src/util/getLocalVersion.ts +0 -35
- package/src/util/getProjectDefaults.ts +0 -103
- package/src/util/getProviderName.ts +0 -9
- package/src/util/getUserConfig.ts +0 -15
- package/src/util/isCi.ts +0 -7
- package/src/util/isCommandGroup.ts +0 -7
- package/src/util/isInteractive.ts +0 -3
- package/src/util/isTrueish.ts +0 -8
- package/src/util/journeyConfig.ts +0 -280
- package/src/util/loadEnv.ts +0 -110
- package/src/util/mergeCommands.ts +0 -78
- package/src/util/neatStack.ts +0 -30
- package/src/util/noSuchCommandText.ts +0 -91
- package/src/util/parseArguments.ts +0 -76
- package/src/util/pathExists.ts +0 -14
- package/src/util/readJson.ts +0 -6
- package/src/util/readdirRecursive.ts +0 -26
- package/src/util/remoteTemplate.ts +0 -320
- package/src/util/resolveLatestVersions.ts +0 -24
- package/src/util/resolveRootDir.ts +0 -59
- package/src/util/telemetryDisclosure.ts +0 -45
- package/src/util/updateNotifier.ts +0 -158
- package/src/util/writeJson.ts +0 -6
- package/src/workers/getAndWriteJourneySchema.ts +0 -7
- package/src/workers/getCliConfig.ts +0 -14
- 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
|
-
}
|