Skip to content

Commit 171ad3f

Browse files
authored
feat(init): improve flags for the init command (#729)
1 parent cffaf22 commit 171ad3f

File tree

3 files changed

+137
-19
lines changed

3 files changed

+137
-19
lines changed

packages/@sanity/cli/src/commands/__tests__/init/init.create-new-project.test.ts

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ import {CREATE_PROJECT_API_VERSION, PROJECTS_API_VERSION} from '../../../service
1010
import {InitCommand} from '../../init.js'
1111

1212
const mocks = vi.hoisted(() => ({
13+
confirm: vi.fn(),
1314
datasetsCreate: vi.fn(),
1415
detectFrameworkRecord: vi.fn(),
1516
getOrganizationChoices: vi.fn(),
1617
getOrganizationsWithAttachGrantInfo: vi.fn(),
18+
importDatasetRun: vi.fn(),
1719
input: vi.fn(),
1820
listDatasets: vi.fn(),
1921
select: vi.fn(),
@@ -29,6 +31,7 @@ vi.mock('@sanity/cli-core/ux', async () => {
2931

3032
return {
3133
...actual,
34+
confirm: mocks.confirm,
3235
input: mocks.input,
3336
select: mocks.select,
3437
}
@@ -43,6 +46,7 @@ vi.mock('@sanity/cli-core', async (importOriginal) => {
4346

4447
return {
4548
...actual,
49+
getCliToken: vi.fn().mockResolvedValue('test-token'),
4650
getGlobalCliClient: vi.fn().mockResolvedValue({
4751
projects: {
4852
list: vi.fn().mockResolvedValue([
@@ -116,6 +120,10 @@ vi.mock('../../../actions/init/bootstrapTemplate.js', () => ({
116120
bootstrapTemplate: vi.fn().mockResolvedValue(undefined),
117121
}))
118122

123+
vi.mock('../../dataset/import.js', () => ({
124+
ImportDatasetCommand: {run: mocks.importDatasetRun},
125+
}))
126+
119127
vi.mock('../../../actions/init/resolvePackageManager.js', () => ({
120128
resolvePackageManager: vi.fn().mockResolvedValue('npm'),
121129
}))
@@ -215,7 +223,7 @@ describe('#init: create new project', () => {
215223
await testCommand(
216224
InitCommand,
217225
[
218-
'--create-project=Test Project',
226+
'--project-name=Test Project',
219227
'--dataset=production',
220228
'--output-path=./test-project',
221229
'--no-nextjs-add-config-files',
@@ -314,7 +322,7 @@ describe('#init: create new project', () => {
314322
await testCommand(
315323
InitCommand,
316324
[
317-
'--create-project=Test Project',
325+
'--project-name=Test Project',
318326
'--dataset=production',
319327
'--output-path=./test-project',
320328
'--no-nextjs-add-config-files',
@@ -487,4 +495,102 @@ describe('#init: create new project', () => {
487495
expect.stringContaining('already configured for Sanity MCP'),
488496
)
489497
})
498+
499+
test('--no-import-dataset skips dataset import for template with sample data', async () => {
500+
mocks.detectFrameworkRecord.mockResolvedValueOnce(null)
501+
502+
setupInitSuccessMocks()
503+
504+
const {error} = await testCommand(
505+
InitCommand,
506+
[
507+
'--project=project-123',
508+
'--dataset=production',
509+
'--output-path=./test-project',
510+
'--no-nextjs-add-config-files',
511+
'--no-nextjs-append-env',
512+
'--no-nextjs-embed-studio',
513+
'--no-typescript',
514+
'--no-overwrite-files',
515+
'--template=moviedb',
516+
'--no-import-dataset',
517+
],
518+
{mocks: {...defaultMocks, isInteractive: true}},
519+
)
520+
521+
if (error) throw error
522+
expect(mocks.confirm).not.toHaveBeenCalled()
523+
expect(mocks.importDatasetRun).not.toHaveBeenCalled()
524+
})
525+
526+
test('--import-dataset forces import in unattended mode', async () => {
527+
mocks.detectFrameworkRecord.mockResolvedValueOnce(null)
528+
mocks.importDatasetRun.mockResolvedValueOnce(undefined)
529+
530+
// Only mock endpoints actually hit in unattended mode with --project and --dataset
531+
mockApi({
532+
apiVersion: PROJECTS_API_VERSION,
533+
method: 'get',
534+
uri: '/projects/project-123',
535+
}).reply(200, {id: 'project-123', metadata: {cliInitializedAt: ''}})
536+
537+
const {error} = await testCommand(
538+
InitCommand,
539+
[
540+
'--yes',
541+
'--project=project-123',
542+
'--dataset=production',
543+
'--output-path=./test-project',
544+
'--template=moviedb',
545+
'--import-dataset',
546+
],
547+
{mocks: defaultMocks},
548+
)
549+
550+
if (error) throw error
551+
expect(mocks.confirm).not.toHaveBeenCalled()
552+
expect(mocks.importDatasetRun).toHaveBeenCalledWith(
553+
expect.arrayContaining([
554+
'https://public.sanity.io/moviesdb-2018-03-06.tar.gz',
555+
'--project-id',
556+
'project-123',
557+
'--dataset',
558+
'production',
559+
'--token',
560+
'test-token',
561+
]),
562+
expect.objectContaining({root: expect.any(String)}),
563+
)
564+
})
565+
566+
test('prompts for dataset import when flag is not set in interactive mode', async () => {
567+
mocks.detectFrameworkRecord.mockResolvedValueOnce(null)
568+
mocks.confirm.mockResolvedValueOnce(false)
569+
570+
setupInitSuccessMocks()
571+
572+
const {error} = await testCommand(
573+
InitCommand,
574+
[
575+
'--project=project-123',
576+
'--dataset=production',
577+
'--output-path=./test-project',
578+
'--no-nextjs-add-config-files',
579+
'--no-nextjs-append-env',
580+
'--no-nextjs-embed-studio',
581+
'--no-typescript',
582+
'--no-overwrite-files',
583+
'--template=moviedb',
584+
],
585+
{mocks: {...defaultMocks, isInteractive: true}},
586+
)
587+
588+
if (error) throw error
589+
expect(mocks.confirm).toHaveBeenCalledWith(
590+
expect.objectContaining({
591+
message: 'Add a sampling of sci-fi movies to your dataset on the hosted backend?',
592+
}),
593+
)
594+
expect(mocks.importDatasetRun).not.toHaveBeenCalled()
595+
})
490596
})

packages/@sanity/cli/src/commands/__tests__/init/init.setup.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ describe('#init: oclif command setup', () => {
6565
{flag1: 'template=test', flag2: 'bare'},
6666
{flag1: 'typescript', flag2: 'bare'},
6767
{flag1: 'project=test', flag2: 'create-project=test'},
68+
{flag1: 'project=test', flag2: 'project-name=test'},
69+
{flag1: 'project-name=test', flag2: 'create-project=test'},
6870
])('throws error when `$flag1` and `$flag2` flags are both passed', async ({flag1, flag2}) => {
6971
const {error} = await testCommand(InitCommand, [`--${flag1}`, `--${flag2}`], {
7072
mocks: {
@@ -203,7 +205,7 @@ describe('#init: oclif command setup', () => {
203205
expect(error?.oclif?.exit).toBe(1)
204206
})
205207

206-
test('throws error when in unattended mode and `project` and `create-project` not set', async () => {
208+
test('throws error when in unattended mode and `project` and `project-name` not set', async () => {
207209
mocks.detectFrameworkRecord.mockResolvedValueOnce({
208210
name: 'Next.js',
209211
slug: 'nextjs',
@@ -214,7 +216,7 @@ describe('#init: oclif command setup', () => {
214216
[
215217
'--yes',
216218
'--dataset=production',
217-
// Deliberately omitting --project and --create-project
219+
// Deliberately omitting --project and --project-name
218220
],
219221
{
220222
mocks: {
@@ -224,20 +226,20 @@ describe('#init: oclif command setup', () => {
224226
)
225227

226228
expect(error?.message).toContain(
227-
'`--project <id>` or `--create-project <name>` must be specified in unattended mode',
229+
'`--project <id>` or `--project-name <name>` must be specified in unattended mode',
228230
)
229231
expect(error?.oclif?.exit).toBe(1)
230232
})
231233

232-
test('throws error when in unattended mode and `create-project` not set with `organization`', async () => {
234+
test('throws error when in unattended mode and `project-name` set without `organization`', async () => {
233235
mocks.detectFrameworkRecord.mockResolvedValueOnce({
234236
name: 'Next.js',
235237
slug: 'nextjs',
236238
})
237239

238240
const {error} = await testCommand(
239241
InitCommand,
240-
['--yes', '--dataset=production', '--create-project=test'],
242+
['--yes', '--dataset=production', '--project-name=test'],
241243
{
242244
mocks: {
243245
...defaultMocks,
@@ -246,7 +248,7 @@ describe('#init: oclif command setup', () => {
246248
)
247249

248250
expect(error?.message).toContain(
249-
'--create-project is not supported in unattended mode without an organization, please specify an organization with `--organization <id>`',
251+
'`--project-name` requires `--organization <id>` in unattended mode',
250252
)
251253
expect(error?.oclif?.exit).toBe(1)
252254
})

packages/@sanity/cli/src/commands/init.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export class InitCommand extends SanityCommand<typeof InitCommand> {
116116
},
117117
{
118118
command:
119-
'<%= config.bin %> <%= command.id %> -y --create-project "Movies Unlimited" --dataset moviedb --visibility private --template moviedb --output-path /Users/espenh/movies-unlimited',
119+
'<%= config.bin %> <%= command.id %> -y --project-name "Movies Unlimited" --dataset moviedb --visibility private --template moviedb --output-path /Users/espenh/movies-unlimited',
120120
description: 'Create a brand new project with name "Movies Unlimited"',
121121
},
122122
] satisfies Array<Command.Example>
@@ -139,8 +139,10 @@ export class InitCommand extends SanityCommand<typeof InitCommand> {
139139
helpValue: '<code>',
140140
}),
141141
'create-project': Flags.string({
142+
deprecated: {message: 'Use --project-name instead'},
142143
description: 'Create a new project with the given name',
143144
helpValue: '<name>',
145+
hidden: true,
144146
}),
145147
dataset: Flags.string({
146148
description: 'Dataset name for the studio',
@@ -173,6 +175,11 @@ export class InitCommand extends SanityCommand<typeof InitCommand> {
173175
helpLabel: ' --[no-]git',
174176
helpValue: '<message>',
175177
}),
178+
'import-dataset': Flags.boolean({
179+
allowNo: true,
180+
default: undefined,
181+
description: 'Import template sample dataset',
182+
}),
176183
mcp: Flags.boolean({
177184
allowNo: true,
178185
default: true,
@@ -227,9 +234,14 @@ export class InitCommand extends SanityCommand<typeof InitCommand> {
227234
project: Flags.string({
228235
aliases: ['project-id'],
229236
description: 'Project ID to use for the studio',
230-
exclusive: ['create-project'],
237+
exclusive: ['create-project', 'project-name'],
231238
helpValue: '<id>',
232239
}),
240+
'project-name': Flags.string({
241+
description: 'Create a new project with the given name',
242+
exclusive: ['project', 'create-project'],
243+
helpValue: '<name>',
244+
}),
233245
'project-plan': Flags.string({
234246
description: 'Optionally select a plan for a new project',
235247
helpValue: '<name>',
@@ -287,7 +299,7 @@ export class InitCommand extends SanityCommand<typeof InitCommand> {
287299
public async run(): Promise<void> {
288300
const workDir = process.cwd()
289301

290-
const createProjectName = this.flags['create-project']
302+
const createProjectName = this.flags['project-name'] ?? this.flags['create-project']
291303
// For backwards "compatibility" - we used to allow `sanity init plugin`,
292304
// and no longer do - but instead of printing an error about an unknown
293305
// _command_, we want to acknowledge that the user is trying to do something
@@ -534,10 +546,11 @@ export class InitCommand extends SanityCommand<typeof InitCommand> {
534546
}
535547

536548
// If the template has a sample dataset, prompt the user whether or not we should import it
549+
const importDatasetFlag = this.flags['import-dataset']
537550
const shouldImport =
538-
!this.isUnattended() &&
539551
template?.datasetUrl &&
540-
(await this.promptForDatasetImport(template.importPrompt))
552+
(importDatasetFlag ??
553+
(!this.isUnattended() && (await this.promptForDatasetImport(template.importPrompt))))
541554

542555
this._trace.log({
543556
selectedOption: shouldImport ? 'yes' : 'no',
@@ -725,16 +738,13 @@ export class InitCommand extends SanityCommand<typeof InitCommand> {
725738

726739
if (!this.flags.project && !createProjectName) {
727740
this.error(
728-
'`--project <id>` or `--create-project <name>` must be specified in unattended mode',
741+
'`--project <id>` or `--project-name <name>` must be specified in unattended mode',
729742
{exit: 1},
730743
)
731744
}
732745

733746
if (createProjectName && !this.flags.organization) {
734-
this.error(
735-
'--create-project is not supported in unattended mode without an organization, please specify an organization with `--organization <id>`',
736-
{exit: 1},
737-
)
747+
this.error('`--project-name` requires `--organization <id>` in unattended mode', {exit: 1})
738748
}
739749
}
740750

@@ -747,7 +757,7 @@ export class InitCommand extends SanityCommand<typeof InitCommand> {
747757
planId: string | undefined
748758
user: SanityOrgUser
749759
}) {
750-
debug('--create-project specified, creating a new project')
760+
debug('--project-name specified, creating a new project')
751761

752762
let orgForCreateProjectFlag = this.flags.organization
753763

0 commit comments

Comments
 (0)