@sanity/cli 3.78.0 → 3.78.1-asset-library.59

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sanity/cli",
3
- "version": "3.78.0",
3
+ "version": "3.78.1-asset-library.59+e5bf69a007",
4
4
  "description": "Sanity CLI tool for managing Sanity installations, managing plugins, schemas and datasets",
5
5
  "keywords": [
6
6
  "sanity",
@@ -58,10 +58,11 @@
58
58
  "dependencies": {
59
59
  "@babel/traverse": "^7.23.5",
60
60
  "@sanity/client": "^6.28.2",
61
- "@sanity/codegen": "3.78.0",
61
+ "@sanity/codegen": "3.78.1-asset-library.59+e5bf69a007",
62
+ "@sanity/runtime-cli": "^1.1.1",
62
63
  "@sanity/telemetry": "^0.7.7",
63
64
  "@sanity/template-validator": "^2.4.0",
64
- "@sanity/util": "3.78.0",
65
+ "@sanity/util": "3.78.1-asset-library.59+e5bf69a007",
65
66
  "chalk": "^4.1.2",
66
67
  "debug": "^4.3.4",
67
68
  "decompress": "^4.2.0",
@@ -81,7 +82,7 @@
81
82
  "@rollup/plugin-node-resolve": "^15.2.3",
82
83
  "@sanity/eslint-config-studio": "^4.0.0",
83
84
  "@sanity/generate-help-url": "^3.0.0",
84
- "@sanity/types": "3.78.0",
85
+ "@sanity/types": "3.78.1-asset-library.59+e5bf69a007",
85
86
  "@types/babel__traverse": "^7.20.5",
86
87
  "@types/configstore": "^5.0.1",
87
88
  "@types/cpx": "^1.5.2",
@@ -115,7 +116,7 @@
115
116
  "minimist": "^1.2.5",
116
117
  "open": "^8.4.0",
117
118
  "ora": "^8.0.1",
118
- "p-filter": "^2.1.0",
119
+ "p-map": "^4.0.0",
119
120
  "p-timeout": "^4.0.0",
120
121
  "preferred-pm": "^3.0.3",
121
122
  "promise-props-recursive": "^2.0.2",
@@ -133,5 +134,5 @@
133
134
  "engines": {
134
135
  "node": ">=18"
135
136
  },
136
- "gitHead": "713cb61cbbf13846883f9af0484601f805d83946"
137
+ "gitHead": "e5bf69a0072ed33808a82f6c854f13138ac48cf4"
137
138
  }
@@ -8,7 +8,7 @@ import {type detectFrameworkRecord} from '@vercel/fs-detectors'
8
8
  import dotenv from 'dotenv'
9
9
  import execa, {type CommonOptions} from 'execa'
10
10
  import {deburr, noop} from 'lodash'
11
- import pFilter from 'p-filter'
11
+ import pMap from 'p-map'
12
12
  import resolveFrom from 'resolve-from'
13
13
  import semver from 'semver'
14
14
 
@@ -108,6 +108,16 @@ export interface ProjectOrganization {
108
108
  slug: string
109
109
  }
110
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
+
111
121
  // eslint-disable-next-line max-statements, complexity
112
122
  export default async function initSanity(
113
123
  args: CliCommandArguments<InitFlags>,
@@ -257,13 +267,14 @@ export default async function initSanity(
257
267
  const hasToken = userConfig.get('authToken')
258
268
 
259
269
  debug(hasToken ? 'User already has a token' : 'User has no token')
270
+ let user: SanityUser | undefined
260
271
  if (hasToken) {
261
272
  trace.log({step: 'login', alreadyLoggedIn: true})
262
- const user = await getUserData(apiClient)
273
+ user = await getUserData(apiClient)
263
274
  success('You are logged in as %s using %s', user.email, getProviderName(user.provider))
264
275
  } else if (!unattended) {
265
276
  trace.log({step: 'login'})
266
- await getOrCreateUser()
277
+ user = await getOrCreateUser()
267
278
  }
268
279
 
269
280
  // skip project / dataset prompting
@@ -712,6 +723,7 @@ export default async function initSanity(
712
723
  const {extOptions, ...otherArgs} = args
713
724
  const loginArgs: CliCommandArguments<LoginFlags> = {...otherArgs, extOptions: {}}
714
725
  await login(loginArgs, {...context, telemetry: trace.newContext('login')})
726
+ return getUserData(apiClient)
715
727
  }
716
728
 
717
729
  async function getProjectDetails(): Promise<{
@@ -858,7 +870,22 @@ export default async function initSanity(
858
870
  ? 'No projects found for user, prompting for name'
859
871
  : 'Using a coupon - skipping project selection',
860
872
  )
861
- const projectName = await prompt.single({type: 'input', message: 'Project name:'})
873
+ const projectName = await prompt.single({
874
+ type: 'input',
875
+ message: 'Project name:',
876
+ default: 'My Sanity Project',
877
+ validate(input) {
878
+ if (!input || input.trim() === '') {
879
+ return 'Project name cannot be empty'
880
+ }
881
+
882
+ if (input.length > 80) {
883
+ return 'Project name cannot be longer than 80 characters'
884
+ }
885
+
886
+ return true
887
+ },
888
+ })
862
889
 
863
890
  return createProject(apiClient, {
864
891
  displayName: projectName,
@@ -1242,47 +1269,90 @@ export default async function initSanity(
1242
1269
  return cliFlags
1243
1270
  }
1244
1271
 
1272
+ async function createOrganization(
1273
+ props: {name?: string} = {},
1274
+ ): Promise<OrganizationCreateResponse> {
1275
+ const name =
1276
+ props.name ||
1277
+ (await prompt.single({
1278
+ type: 'input',
1279
+ message: 'Organization name:',
1280
+ default: user ? user.name : undefined,
1281
+ validate(input) {
1282
+ if (input.length === 0) {
1283
+ return 'Organization name cannot be empty'
1284
+ } else if (input.length > 100) {
1285
+ return 'Organization name cannot be longer than 100 characters'
1286
+ }
1287
+ return true
1288
+ },
1289
+ }))
1290
+
1291
+ const spinner = context.output.spinner('Creating organization').start()
1292
+ const client = apiClient({requireProject: false, requireUser: true})
1293
+ const organization = await client.request({
1294
+ uri: '/organizations',
1295
+ method: 'POST',
1296
+ body: {name},
1297
+ })
1298
+ spinner.succeed()
1299
+
1300
+ return organization
1301
+ }
1302
+
1245
1303
  async function getOrganizationId(organizations: ProjectOrganization[]) {
1246
- let orgId = flags.organization
1247
- if (unattended) {
1248
- return orgId || undefined
1304
+ // In unattended mode, if the user hasn't specified an organization, sending null as
1305
+ // organization ID to the API will create a new organization for the user with their
1306
+ // user name. If they _have_ specified an organization, we'll use that.
1307
+ if (unattended || flags.organization) {
1308
+ return flags.organization || undefined
1249
1309
  }
1250
1310
 
1251
- const shouldPrompt = organizations.length > 0 && !orgId
1252
- if (shouldPrompt) {
1253
- debug(`User has ${organizations.length} organization(s), checking attach access`)
1254
- const withGrant = await getOrganizationsWithAttachGrant(organizations)
1255
- if (withGrant.length === 0) {
1256
- debug('User lacks project attach grant in all organizations, not prompting')
1257
- return undefined
1258
- }
1311
+ // If the user has no organizations, prompt them to create one with the same name as
1312
+ // their user, but allow them to customize it if they want
1313
+ if (organizations.length === 0) {
1314
+ return createOrganization().then((org) => org.id)
1315
+ }
1259
1316
 
1260
- debug('User has attach access to %d organizations, prompting.', withGrant.length)
1261
- const organizationChoices = [
1262
- {value: 'none', name: 'None'},
1263
- new prompt.Separator(),
1264
- ...withGrant.map((organization) => ({
1265
- value: organization.id,
1266
- name: `${organization.name} [${organization.id}]`,
1267
- })),
1268
- ]
1269
-
1270
- const chosenOrg = await prompt.single({
1271
- message: `Select organization to attach ${isCoreAppTemplate ? 'application' : 'project'} to`,
1272
- type: 'list',
1273
- choices: organizationChoices,
1274
- })
1317
+ // If the user has organizations, let them choose from them, but also allow them to
1318
+ // create a new one in case they do not have access to any of them, or they want to
1319
+ // create a personal/other organization.
1320
+ debug(`User has ${organizations.length} organization(s), checking attach access`)
1321
+ const withGrantInfo = await getOrganizationsWithAttachGrantInfo(organizations)
1322
+ const withAttach = withGrantInfo.filter(({hasAttachGrant}) => hasAttachGrant)
1323
+
1324
+ debug('User has attach access to %d organizations.', withAttach.length)
1325
+ const organizationChoices = [
1326
+ ...withGrantInfo.map(({organization, hasAttachGrant}) => ({
1327
+ value: organization.id,
1328
+ name: `${organization.name} [${organization.id}]`,
1329
+ disabled: hasAttachGrant ? false : 'Insufficient permissions',
1330
+ })),
1331
+ new prompt.Separator(),
1332
+ {value: '-new-', name: 'Create new organization'},
1333
+ new prompt.Separator(),
1334
+ ]
1335
+
1336
+ // If the user only has a single organization (and they have attach access to it),
1337
+ // we'll default to that one. Otherwise, we'll default to the organization with the
1338
+ // same name as the user if it exists.
1339
+ const defaultOrganizationId =
1340
+ withAttach.length === 1
1341
+ ? withAttach[0].organization.id
1342
+ : organizations.find((org) => org.name === user?.name)?.id
1343
+
1344
+ const chosenOrg = await prompt.single({
1345
+ message: 'Select organization:',
1346
+ type: 'list',
1347
+ default: defaultOrganizationId || undefined,
1348
+ choices: organizationChoices,
1349
+ })
1275
1350
 
1276
- if (chosenOrg && chosenOrg !== 'none') {
1277
- orgId = chosenOrg
1278
- }
1279
- } else if (orgId) {
1280
- debug(`User has defined organization flag explicitly (%s)`, orgId)
1281
- } else if (organizations.length === 0) {
1282
- debug('User has no organizations, skipping selection prompt')
1351
+ if (chosenOrg === '-new-') {
1352
+ return createOrganization().then((org) => org.id)
1283
1353
  }
1284
1354
 
1285
- return orgId || undefined
1355
+ return chosenOrg || undefined
1286
1356
  }
1287
1357
 
1288
1358
  async function hasProjectAttachGrant(orgId: string) {
@@ -1301,8 +1371,15 @@ export default async function initSanity(
1301
1371
  )
1302
1372
  }
1303
1373
 
1304
- function getOrganizationsWithAttachGrant(organizations: ProjectOrganization[]) {
1305
- return pFilter(organizations, (org) => hasProjectAttachGrant(org.id), {concurrency: 3})
1374
+ function getOrganizationsWithAttachGrantInfo(organizations: ProjectOrganization[]) {
1375
+ return pMap(
1376
+ organizations,
1377
+ async (organization) => ({
1378
+ hasAttachGrant: await hasProjectAttachGrant(organization.id),
1379
+ organization,
1380
+ }),
1381
+ {concurrency: 3},
1382
+ )
1306
1383
  }
1307
1384
 
1308
1385
  async function createOrAppendEnvVars(
@@ -0,0 +1,42 @@
1
+ import open from 'open'
2
+
3
+ import {type CliCommandDefinition} from '../../types'
4
+
5
+ const helpText = `
6
+ Options
7
+ --port <port> Port to start emulator on
8
+
9
+ Examples
10
+ # Start dev server on default port
11
+ sanity functions dev
12
+
13
+ # Start dev server on specific port
14
+ sanity functions dev --port 3333
15
+ `
16
+
17
+ const defaultFlags = {
18
+ port: 8080,
19
+ }
20
+
21
+ const devFunctionsCommand: CliCommandDefinition = {
22
+ name: 'dev',
23
+ group: 'functions',
24
+ helpText,
25
+ signature: '',
26
+ description: 'Start the Sanity Function emulator',
27
+ hideFromHelp: true,
28
+ async action(args, context) {
29
+ const {output} = context
30
+ const {print} = output
31
+ const flags = {...defaultFlags, ...args.extOptions}
32
+
33
+ const {devAction} = await import('@sanity/runtime-cli')
34
+
35
+ devAction(flags.port)
36
+
37
+ print(`Server is running on port ${flags.port}\n`)
38
+ open(`http://localhost:${flags.port}`)
39
+ },
40
+ }
41
+
42
+ export default devFunctionsCommand
@@ -0,0 +1,11 @@
1
+ import {type CliCommandGroupDefinition} from '../../types'
2
+
3
+ const functionsGroup: CliCommandGroupDefinition = {
4
+ name: 'functions',
5
+ signature: '[COMMAND]',
6
+ isGroupRoot: true,
7
+ description: 'Test Sanity Functions locally and retrieve logs',
8
+ hideFromHelp: true,
9
+ }
10
+
11
+ export default functionsGroup
@@ -0,0 +1,46 @@
1
+ import {type CliCommandDefinition} from '../../types'
2
+
3
+ const helpText = `
4
+ Options
5
+ --id <id> The ID of the function to retrieve logs for
6
+
7
+ Examples
8
+ # Retrieve logs for Sanity Function abcd1234
9
+ sanity functions logs --id abcd1234
10
+ `
11
+
12
+ const defaultFlags = {
13
+ id: undefined,
14
+ }
15
+
16
+ const logsFunctionsCommand: CliCommandDefinition = {
17
+ name: 'logs',
18
+ group: 'functions',
19
+ helpText,
20
+ signature: '',
21
+ description: 'Retrieve logs for a Sanity Function',
22
+ hideFromHelp: true,
23
+ async action(args, context) {
24
+ const {apiClient, output} = context
25
+ const {print} = output
26
+ const flags = {...defaultFlags, ...args.extOptions}
27
+
28
+ const client = apiClient({
29
+ requireUser: true,
30
+ requireProject: false,
31
+ })
32
+
33
+ if (flags.id) {
34
+ const token = client.config().token
35
+ if (token) {
36
+ const {logsAction} = await import('@sanity/runtime-cli')
37
+ const result = await logsAction(flags.id, token)
38
+ print(JSON.stringify(result, null, 2))
39
+ }
40
+ } else {
41
+ print('You must provide a function ID')
42
+ }
43
+ },
44
+ }
45
+
46
+ export default logsFunctionsCommand
@@ -0,0 +1,62 @@
1
+ import {type CliCommandDefinition} from '../../types'
2
+
3
+ const helpText = `
4
+ Options
5
+ --data <data> Data to send to the function
6
+ --file <file> Read data from file and send to the function
7
+ --path <path> Path to your Sanity Function code
8
+ --timeout <timeout> Execution timeout value in seconds
9
+
10
+ Examples
11
+ # Test function passing event data on command line
12
+ sanity functions test --path ./test.ts --data '{ "id": 1 }'
13
+
14
+ # Test function passing event data via a file
15
+ sanity functions test -path ./test.js --file 'payload.json'
16
+
17
+ # Test function passing event data on command line and cap execution time to 60 seconds
18
+ sanity functions test -path ./test.ts --data '{ "id": 1 }' --timeout 60
19
+ `
20
+
21
+ const defaultFlags = {
22
+ data: undefined,
23
+ file: undefined,
24
+ path: undefined,
25
+ timeout: 5, // seconds
26
+ }
27
+
28
+ const testFunctionsCommand: CliCommandDefinition = {
29
+ name: 'test',
30
+ group: 'functions',
31
+ helpText,
32
+ signature: '',
33
+ description: 'Invoke a local Sanity Function',
34
+ hideFromHelp: true,
35
+ async action(args, context) {
36
+ const {output} = context
37
+ const {print} = output
38
+ const flags = {...defaultFlags, ...args.extOptions}
39
+
40
+ if (flags.path) {
41
+ const {testAction} = await import('@sanity/runtime-cli')
42
+ const {json, logs, error} = await testAction(flags.path, {
43
+ data: flags.data,
44
+ file: flags.file,
45
+ timeout: flags.timeout,
46
+ })
47
+
48
+ if (error) {
49
+ print(error.toString())
50
+ } else {
51
+ print('Logs:')
52
+ print(logs)
53
+ print('Response:')
54
+ print(JSON.stringify(json, null, 2))
55
+ }
56
+ } else {
57
+ print('You must provide a path to the Sanity Function code')
58
+ }
59
+ },
60
+ }
61
+
62
+ export default testFunctionsCommand
@@ -2,6 +2,10 @@ import {type CliCommandDefinition, type CliCommandGroupDefinition} from '../type
2
2
  import codemodCommand from './codemod/codemodCommand'
3
3
  import debugCommand from './debug/debugCommand'
4
4
  import docsCommand from './docs/docsCommand'
5
+ import devfunctionsCommand from './functions/devFunctionsCommand'
6
+ import functionsGroup from './functions/functionsGroup'
7
+ import logsfunctionsCommand from './functions/logsFunctionsCommand'
8
+ import testfunctionsCommand from './functions/testFunctionsCommand'
5
9
  import helpCommand from './help/helpCommand'
6
10
  import initCommand from './init/initCommand'
7
11
  import installCommand from './install/installCommand'
@@ -39,4 +43,8 @@ export const baseCommands: (CliCommandDefinition | CliCommandGroupDefinition)[]
39
43
  telemetryStatusCommand,
40
44
  generateTypegenCommand,
41
45
  typegenGroup,
46
+ functionsGroup,
47
+ devfunctionsCommand,
48
+ logsfunctionsCommand,
49
+ testfunctionsCommand,
42
50
  ]
@@ -101,7 +101,6 @@ export const initCommand: CliCommandDefinition<InitFlags> = {
101
101
  description: 'Initializes a new Sanity Studio and/or project',
102
102
  helpText,
103
103
  action: async (args, context) => {
104
- const {output, chalk} = context
105
104
  const [type] = args.argsWithoutOptions
106
105
 
107
106
  // `sanity init whatever`