@sanity/cli 8.11.0 → 8.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +69 -26
  2. package/dist/actions/assets/ingestAssetFromUrlWithProgress.js +21 -0
  3. package/dist/actions/assets/ingestAssetFromUrlWithProgress.js.map +1 -0
  4. package/dist/actions/assets/withUrlIngestProgress.js +45 -0
  5. package/dist/actions/assets/withUrlIngestProgress.js.map +1 -0
  6. package/dist/actions/auth/authServer.js +5 -8
  7. package/dist/actions/auth/authServer.js.map +1 -1
  8. package/dist/actions/deploy/deployApp.js +5 -2
  9. package/dist/actions/deploy/deployApp.js.map +1 -1
  10. package/dist/actions/deploy/deployChecks.js +3 -2
  11. package/dist/actions/deploy/deployChecks.js.map +1 -1
  12. package/dist/actions/deploy/findUserApplication.js +3 -2
  13. package/dist/actions/deploy/findUserApplication.js.map +1 -1
  14. package/dist/actions/deploy/resolveDeployTarget.js +6 -1
  15. package/dist/actions/deploy/resolveDeployTarget.js.map +1 -1
  16. package/dist/actions/documents/validateDocuments.worker.js +1 -4
  17. package/dist/actions/documents/validateDocuments.worker.js.map +1 -1
  18. package/dist/actions/media/importMedia.js.map +1 -1
  19. package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js +19 -0
  20. package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js.map +1 -0
  21. package/dist/commands/assets/upload.js +78 -52
  22. package/dist/commands/assets/upload.js.map +1 -1
  23. package/dist/commands/deploy.js +33 -1
  24. package/dist/commands/deploy.js.map +1 -1
  25. package/dist/commands/media/import.js +149 -13
  26. package/dist/commands/media/import.js.map +1 -1
  27. package/dist/exports/invokeSanityCli/commandPolicies/index.js +1 -4
  28. package/dist/exports/invokeSanityCli/commandPolicies/index.js.map +1 -1
  29. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +26 -6
  30. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
  31. package/dist/exports/invokeSanityCli/commandPolicies/resolve.js +112 -0
  32. package/dist/exports/invokeSanityCli/commandPolicies/resolve.js.map +1 -0
  33. package/dist/exports/invokeSanityCli/help.js +8 -1
  34. package/dist/exports/invokeSanityCli/help.js.map +1 -1
  35. package/dist/exports/invokeSanityCli/index.d.ts +1 -2
  36. package/dist/exports/invokeSanityCli/index.js +37 -15
  37. package/dist/exports/invokeSanityCli/index.js.map +1 -1
  38. package/dist/generated/apiRoutes.js +2 -0
  39. package/dist/generated/apiRoutes.js.map +1 -1
  40. package/dist/services/assets.js +46 -1
  41. package/dist/services/assets.js.map +1 -1
  42. package/dist/services/mediaLibraries.js +39 -1
  43. package/dist/services/mediaLibraries.js.map +1 -1
  44. package/dist/util/assetSourceValidation.js +56 -0
  45. package/dist/util/assetSourceValidation.js.map +1 -0
  46. package/dist/util/assetUploadErrors.js +64 -0
  47. package/dist/util/assetUploadErrors.js.map +1 -0
  48. package/dist/util/isRemoteAssetSource.js +19 -0
  49. package/dist/util/isRemoteAssetSource.js.map +1 -0
  50. package/dist/util/parseAspectFlags.js +34 -0
  51. package/dist/util/parseAspectFlags.js.map +1 -0
  52. package/oclif.config.js +0 -1
  53. package/oclif.manifest.json +3071 -2992
  54. package/package.json +18 -18
  55. package/templates/app-quickstart/AGENTS.md +18 -0
  56. package/templates/app-sanity-ui/AGENTS.md +18 -0
  57. package/templates/shopify/.gitignore +26 -0
  58. package/dist/exports/invokeSanityCli/commandPolicies/policy.js +0 -46
  59. package/dist/exports/invokeSanityCli/commandPolicies/policy.js.map +0 -1
@@ -1,46 +1,15 @@
1
1
  import { basename, resolve } from 'node:path';
2
2
  import { Flags } from '@oclif/core';
3
3
  import { exitCodes, SanityCommand, subdebug } from '@sanity/cli-core';
4
- import { getErrorMessage } from '@sanity/cli-core/errors';
5
- import { isHttpError } from '@sanity/client';
6
4
  import { AssetFileError } from '../../actions/assets/assetFileError.js';
5
+ import { ingestAssetFromUrlWithProgress } from '../../actions/assets/ingestAssetFromUrlWithProgress.js';
7
6
  import { uploadAssetWithProgress } from '../../actions/assets/uploadAssetWithProgress.js';
8
7
  import { promptForProject } from '../../prompts/promptForProject.js';
8
+ import { getAssetFilenameError, getIngestUrlError } from '../../util/assetSourceValidation.js';
9
+ import { getAssetUploadErrorMessage } from '../../util/assetUploadErrors.js';
9
10
  import { getDatasetFlag, getProjectIdFlag } from '../../util/sharedFlags.js';
10
11
  import { defineCommandTelemetry } from '../../util/telemetry/commandTelemetry.js';
11
12
  const uploadAssetDebug = subdebug('assets:upload');
12
- const DATASET_ASSET_LIMITS_URL = 'https://www.sanity.io/docs/content-lake/technical-limits#k2c53dc30e24b';
13
- function isProjectUserNotFoundError(body) {
14
- const responseError = body.error;
15
- return typeof responseError === 'object' && responseError !== null && 'type' in responseError && responseError.type === 'projectUserNotFoundError';
16
- }
17
- function getAssetUploadErrorMessage(error) {
18
- if (!isHttpError(error)) {
19
- return `Asset upload failed: ${getErrorMessage(error)}`;
20
- }
21
- const body = typeof error.response.body === 'object' && error.response.body !== null && !Array.isArray(error.response.body) ? error.response.body : {};
22
- const responseError = typeof body.error === 'string' ? body.error : error.response.statusMessage || 'HTTP error';
23
- const statusCode = typeof body.statusCode === 'number' || typeof body.statusCode === 'string' ? body.statusCode : error.statusCode;
24
- const projectUserNotFound = isProjectUserNotFoundError(body);
25
- const responseMessage = projectUserNotFound ? error.message : getErrorMessage(error);
26
- const message = /[.!?]$/.test(responseMessage) ? responseMessage : `${responseMessage}.`;
27
- const details = typeof body.details === 'string' ? `\n\nDetails:\n${body.details}` : '';
28
- const response = `Asset upload failed: HTTP ${statusCode} - ${responseError}\n${message}${details}`;
29
- if (error.statusCode === 401 && !projectUserNotFound) {
30
- return `${response}\n\nRun \`sanity login\` to authenticate, then try again.`;
31
- }
32
- if (error.statusCode === 403) {
33
- return `${response}\n\nCheck that your account has write access to this dataset, then try again.`;
34
- }
35
- if ([
36
- 400,
37
- 413,
38
- 422
39
- ].includes(error.statusCode)) {
40
- return `${response}\n\nCheck the asset requirements and current technical limits, then try again: ${DATASET_ASSET_LIMITS_URL}`;
41
- }
42
- return `${response}\n\nTry again.`;
43
- }
44
13
  const flags = {
45
14
  ...getProjectIdFlag({
46
15
  description: 'Project ID to upload the asset to',
@@ -51,18 +20,29 @@ const flags = {
51
20
  semantics: 'override'
52
21
  }),
53
22
  'content-type': Flags.string({
54
- description: 'MIME type of the asset, such as image/png or application/pdf',
23
+ description: 'MIME type of the asset, such as image/png or application/pdf. Only applies to --file',
55
24
  helpValue: '<mime-type>'
56
25
  }),
57
26
  file: Flags.string({
58
27
  description: 'Path to the local file to upload',
59
- helpValue: '<path>',
60
- required: true
28
+ exactlyOne: [
29
+ 'file',
30
+ 'from-url'
31
+ ],
32
+ helpValue: '<path>'
61
33
  }),
62
34
  filename: Flags.string({
63
- description: 'Original filename stored on the asset document. Defaults to the local filename',
35
+ description: 'Original filename stored on the asset document. Defaults to the local filename when using --file',
64
36
  helpValue: '<filename>'
65
37
  }),
38
+ 'from-url': Flags.string({
39
+ description: 'https URL for Sanity to fetch the asset from, instead of uploading a local file. Must be reachable from the public internet without authentication',
40
+ exactlyOne: [
41
+ 'file',
42
+ 'from-url'
43
+ ],
44
+ helpValue: '<url>'
45
+ }),
66
46
  type: Flags.custom({
67
47
  default: 'image',
68
48
  description: 'Asset type to create',
@@ -73,7 +53,7 @@ const flags = {
73
53
  })()
74
54
  };
75
55
  export class UploadAssetCommand extends SanityCommand {
76
- static description = 'Upload one local image or file to a Sanity dataset and print the asset document as JSON';
56
+ static description = 'Upload one image or file to a Sanity dataset, from a local path or a URL, and print the asset document as JSON';
77
57
  static examples = [
78
58
  {
79
59
  command: '<%= config.bin %> <%= command.id %> --file ./hero.png --type image --dataset production',
@@ -82,6 +62,10 @@ export class UploadAssetCommand extends SanityCommand {
82
62
  {
83
63
  command: '<%= config.bin %> <%= command.id %> --file ./brief.pdf --type file --content-type application/pdf --project-id abc123 --dataset production',
84
64
  description: 'Upload a file with explicit project, dataset, and MIME type'
65
+ },
66
+ {
67
+ command: "<%= config.bin %> <%= command.id %> --from-url 'https://example.com/hero.png' --type image --dataset production",
68
+ description: 'Have Sanity fetch an image from a public URL'
85
69
  }
86
70
  ];
87
71
  static flags = flags;
@@ -91,12 +75,30 @@ export class UploadAssetCommand extends SanityCommand {
91
75
  static telemetry = defineCommandTelemetry(flags, {
92
76
  redact: [
93
77
  'file',
94
- 'filename'
78
+ 'filename',
79
+ 'from-url'
95
80
  ]
96
81
  });
97
82
  async run() {
98
83
  const { flags } = await this.parse(UploadAssetCommand);
99
- const filePath = resolve(flags.file);
84
+ const sourceUrl = flags['from-url'];
85
+ if (sourceUrl && flags['content-type']) {
86
+ this.error('Asset upload failed: --content-type cannot be combined with --from-url. Sanity derives the MIME type automatically.', {
87
+ exit: exitCodes.USAGE_ERROR
88
+ });
89
+ }
90
+ const sourceUrlError = sourceUrl === undefined ? undefined : getIngestUrlError(sourceUrl);
91
+ if (sourceUrlError) {
92
+ this.error(`Asset upload failed: ${sourceUrlError}`, {
93
+ exit: exitCodes.USAGE_ERROR
94
+ });
95
+ }
96
+ const filenameError = flags.filename === undefined ? undefined : getAssetFilenameError(flags.filename);
97
+ if (filenameError) {
98
+ this.error(`Asset upload failed: ${filenameError}`, {
99
+ exit: exitCodes.USAGE_ERROR
100
+ });
101
+ }
100
102
  const cliConfig = await this.tryGetCliConfig();
101
103
  const projectId = await this.getProjectId({
102
104
  fallback: ()=>promptForProject({})
@@ -107,17 +109,38 @@ export class UploadAssetCommand extends SanityCommand {
107
109
  exit: exitCodes.USAGE_ERROR
108
110
  });
109
111
  }
112
+ const isInteractive = this.resolveIsInteractive();
110
113
  try {
111
- const asset = await uploadAssetWithProgress({
112
- assetType: flags.type,
113
- contentType: flags['content-type'],
114
- dataset,
115
- filename: flags.filename ?? basename(filePath),
116
- filePath,
117
- isInteractive: this.resolveIsInteractive(),
118
- logToStderr: (message)=>this.logToStderr(message),
119
- projectId
120
- });
114
+ let asset;
115
+ if (sourceUrl === undefined) {
116
+ // `exactlyOne` on the flags guarantees a source, but not to the compiler.
117
+ if (flags.file === undefined) {
118
+ this.error('Asset upload failed: Pass either --file <path> or --from-url <url>.', {
119
+ exit: exitCodes.USAGE_ERROR
120
+ });
121
+ }
122
+ const filePath = resolve(flags.file);
123
+ asset = await uploadAssetWithProgress({
124
+ assetType: flags.type,
125
+ contentType: flags['content-type'],
126
+ dataset,
127
+ filename: flags.filename ?? basename(filePath),
128
+ filePath,
129
+ isInteractive,
130
+ logToStderr: (message)=>this.logToStderr(message),
131
+ projectId
132
+ });
133
+ } else {
134
+ asset = await ingestAssetFromUrlWithProgress({
135
+ assetType: flags.type,
136
+ dataset,
137
+ filename: flags.filename,
138
+ isInteractive,
139
+ logToStderr: (message)=>this.logToStderr(message),
140
+ projectId,
141
+ url: sourceUrl
142
+ });
143
+ }
121
144
  const fieldType = flags.type === 'image' ? 'image' : 'file';
122
145
  this.log(JSON.stringify({
123
146
  asset: {
@@ -150,7 +173,10 @@ export class UploadAssetCommand extends SanityCommand {
150
173
  });
151
174
  }
152
175
  uploadAssetDebug('Asset upload failed', error);
153
- this.error(getAssetUploadErrorMessage(error), {
176
+ this.error(getAssetUploadErrorMessage(error, {
177
+ fromUrl: sourceUrl !== undefined,
178
+ target: 'dataset'
179
+ }), {
154
180
  exit: exitCodes.RUNTIME_ERROR
155
181
  });
156
182
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/commands/assets/upload.ts"],"sourcesContent":["import {basename, resolve} from 'node:path'\n\nimport {Flags} from '@oclif/core'\nimport {type FlagInput} from '@oclif/core/interfaces'\nimport {exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'\nimport {getErrorMessage} from '@sanity/cli-core/errors'\nimport {isHttpError} from '@sanity/client'\n\nimport {AssetFileError} from '../../actions/assets/assetFileError.js'\nimport {uploadAssetWithProgress} from '../../actions/assets/uploadAssetWithProgress.js'\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {type AssetType} from '../../services/assets.js'\nimport {getDatasetFlag, getProjectIdFlag} from '../../util/sharedFlags.js'\nimport {defineCommandTelemetry} from '../../util/telemetry/commandTelemetry.js'\n\nconst uploadAssetDebug = subdebug('assets:upload')\nconst DATASET_ASSET_LIMITS_URL =\n 'https://www.sanity.io/docs/content-lake/technical-limits#k2c53dc30e24b'\n\nfunction isProjectUserNotFoundError(body: Record<string, unknown>): boolean {\n const responseError = body.error\n return (\n typeof responseError === 'object' &&\n responseError !== null &&\n 'type' in responseError &&\n responseError.type === 'projectUserNotFoundError'\n )\n}\n\nfunction getAssetUploadErrorMessage(error: unknown): string {\n if (!isHttpError(error)) {\n return `Asset upload failed: ${getErrorMessage(error)}`\n }\n\n const body =\n typeof error.response.body === 'object' &&\n error.response.body !== null &&\n !Array.isArray(error.response.body)\n ? (error.response.body as Record<string, unknown>)\n : {}\n const responseError =\n typeof body.error === 'string' ? body.error : error.response.statusMessage || 'HTTP error'\n const statusCode =\n typeof body.statusCode === 'number' || typeof body.statusCode === 'string'\n ? body.statusCode\n : error.statusCode\n const projectUserNotFound = isProjectUserNotFoundError(body)\n const responseMessage = projectUserNotFound ? error.message : getErrorMessage(error)\n const message = /[.!?]$/.test(responseMessage) ? responseMessage : `${responseMessage}.`\n const details = typeof body.details === 'string' ? `\\n\\nDetails:\\n${body.details}` : ''\n const response = `Asset upload failed: HTTP ${statusCode} - ${responseError}\\n${message}${details}`\n\n if (error.statusCode === 401 && !projectUserNotFound) {\n return `${response}\\n\\nRun \\`sanity login\\` to authenticate, then try again.`\n }\n if (error.statusCode === 403) {\n return `${response}\\n\\nCheck that your account has write access to this dataset, then try again.`\n }\n if ([400, 413, 422].includes(error.statusCode)) {\n return `${response}\\n\\nCheck the asset requirements and current technical limits, then try again: ${DATASET_ASSET_LIMITS_URL}`\n }\n return `${response}\\n\\nTry again.`\n}\n\nconst flags = {\n ...getProjectIdFlag({\n description: 'Project ID to upload the asset to',\n semantics: 'override',\n }),\n ...getDatasetFlag({description: 'Dataset to upload the asset to', semantics: 'override'}),\n 'content-type': Flags.string({\n description: 'MIME type of the asset, such as image/png or application/pdf',\n helpValue: '<mime-type>',\n }),\n file: Flags.string({\n description: 'Path to the local file to upload',\n helpValue: '<path>',\n required: true,\n }),\n filename: Flags.string({\n description: 'Original filename stored on the asset document. Defaults to the local filename',\n helpValue: '<filename>',\n }),\n type: Flags.custom<AssetType>({\n default: 'image',\n description: 'Asset type to create',\n options: ['image', 'file'],\n })(),\n} satisfies FlagInput\n\nexport class UploadAssetCommand extends SanityCommand<typeof UploadAssetCommand> {\n static override description =\n 'Upload one local image or file to a Sanity dataset and print the asset document as JSON'\n\n static override examples = [\n {\n command:\n '<%= config.bin %> <%= command.id %> --file ./hero.png --type image --dataset production',\n description: 'Upload an image using the configured project',\n },\n {\n command:\n '<%= config.bin %> <%= command.id %> --file ./brief.pdf --type file --content-type application/pdf --project-id abc123 --dataset production',\n description: 'Upload a file with explicit project, dataset, and MIME type',\n },\n ]\n\n static override flags = flags\n\n static override hiddenAliases: string[] = ['asset:upload']\n\n static telemetry = defineCommandTelemetry(flags, {\n redact: ['file', 'filename'],\n })\n\n public async run(): Promise<void> {\n const {flags} = await this.parse(UploadAssetCommand)\n const filePath = resolve(flags.file)\n\n const cliConfig = await this.tryGetCliConfig()\n const projectId = await this.getProjectId({fallback: () => promptForProject({})})\n const dataset = flags.dataset ?? cliConfig.api?.dataset\n if (!dataset) {\n this.error(\n 'Asset upload failed: Dataset is required. Pass --dataset <name> or configure a dataset in sanity.cli.ts.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n try {\n const asset = await uploadAssetWithProgress({\n assetType: flags.type,\n contentType: flags['content-type'],\n dataset,\n filename: flags.filename ?? basename(filePath),\n filePath,\n isInteractive: this.resolveIsInteractive(),\n logToStderr: (message) => this.logToStderr(message),\n projectId,\n })\n const fieldType = flags.type === 'image' ? 'image' : 'file'\n\n this.log(\n JSON.stringify(\n {\n asset: {\n _id: asset._id,\n _type: asset._type,\n extension: asset.extension,\n mimeType: asset.mimeType,\n originalFilename: asset.originalFilename,\n size: asset.size,\n url: asset.url,\n },\n reference: {\n _type: fieldType,\n asset: {_ref: asset._id, _type: 'reference'},\n },\n },\n null,\n 2,\n ),\n )\n } catch (error) {\n if (error instanceof Error && error.message === 'SIGINT') throw error\n if (error instanceof AssetFileError) {\n if (error.reason === 'not-file') {\n this.error('Asset upload failed: --file must point to a file, not a directory.', {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n this.error(\n 'Asset upload failed: Cannot read the local file. Check that --file points to a readable file, then retry.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n uploadAssetDebug('Asset upload failed', error)\n this.error(getAssetUploadErrorMessage(error), {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n}\n"],"names":["basename","resolve","Flags","exitCodes","SanityCommand","subdebug","getErrorMessage","isHttpError","AssetFileError","uploadAssetWithProgress","promptForProject","getDatasetFlag","getProjectIdFlag","defineCommandTelemetry","uploadAssetDebug","DATASET_ASSET_LIMITS_URL","isProjectUserNotFoundError","body","responseError","error","type","getAssetUploadErrorMessage","response","Array","isArray","statusMessage","statusCode","projectUserNotFound","responseMessage","message","test","details","includes","flags","description","semantics","string","helpValue","file","required","filename","custom","default","options","UploadAssetCommand","examples","command","hiddenAliases","telemetry","redact","run","parse","filePath","cliConfig","tryGetCliConfig","projectId","getProjectId","fallback","dataset","api","exit","USAGE_ERROR","asset","assetType","contentType","isInteractive","resolveIsInteractive","logToStderr","fieldType","log","JSON","stringify","_id","_type","extension","mimeType","originalFilename","size","url","reference","_ref","Error","reason","RUNTIME_ERROR"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/commands/assets/upload.ts"],"sourcesContent":["import {basename, resolve} from 'node:path'\n\nimport {Flags} from '@oclif/core'\nimport {type FlagInput} from '@oclif/core/interfaces'\nimport {exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'\n\nimport {AssetFileError} from '../../actions/assets/assetFileError.js'\nimport {ingestAssetFromUrlWithProgress} from '../../actions/assets/ingestAssetFromUrlWithProgress.js'\nimport {uploadAssetWithProgress} from '../../actions/assets/uploadAssetWithProgress.js'\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {type AssetType} from '../../services/assets.js'\nimport {getAssetFilenameError, getIngestUrlError} from '../../util/assetSourceValidation.js'\nimport {getAssetUploadErrorMessage} from '../../util/assetUploadErrors.js'\nimport {getDatasetFlag, getProjectIdFlag} from '../../util/sharedFlags.js'\nimport {defineCommandTelemetry} from '../../util/telemetry/commandTelemetry.js'\n\nconst uploadAssetDebug = subdebug('assets:upload')\n\nconst flags = {\n ...getProjectIdFlag({\n description: 'Project ID to upload the asset to',\n semantics: 'override',\n }),\n ...getDatasetFlag({description: 'Dataset to upload the asset to', semantics: 'override'}),\n 'content-type': Flags.string({\n description:\n 'MIME type of the asset, such as image/png or application/pdf. Only applies to --file',\n helpValue: '<mime-type>',\n }),\n file: Flags.string({\n description: 'Path to the local file to upload',\n exactlyOne: ['file', 'from-url'],\n helpValue: '<path>',\n }),\n filename: Flags.string({\n description:\n 'Original filename stored on the asset document. Defaults to the local filename when using --file',\n helpValue: '<filename>',\n }),\n 'from-url': Flags.string({\n description:\n 'https URL for Sanity to fetch the asset from, instead of uploading a local file. Must be reachable from the public internet without authentication',\n exactlyOne: ['file', 'from-url'],\n helpValue: '<url>',\n }),\n type: Flags.custom<AssetType>({\n default: 'image',\n description: 'Asset type to create',\n options: ['image', 'file'],\n })(),\n} satisfies FlagInput\n\nexport class UploadAssetCommand extends SanityCommand<typeof UploadAssetCommand> {\n static override description =\n 'Upload one image or file to a Sanity dataset, from a local path or a URL, and print the asset document as JSON'\n\n static override examples = [\n {\n command:\n '<%= config.bin %> <%= command.id %> --file ./hero.png --type image --dataset production',\n description: 'Upload an image using the configured project',\n },\n {\n command:\n '<%= config.bin %> <%= command.id %> --file ./brief.pdf --type file --content-type application/pdf --project-id abc123 --dataset production',\n description: 'Upload a file with explicit project, dataset, and MIME type',\n },\n {\n command:\n \"<%= config.bin %> <%= command.id %> --from-url 'https://example.com/hero.png' --type image --dataset production\",\n description: 'Have Sanity fetch an image from a public URL',\n },\n ]\n\n static override flags = flags\n\n static override hiddenAliases: string[] = ['asset:upload']\n\n static telemetry = defineCommandTelemetry(flags, {\n redact: ['file', 'filename', 'from-url'],\n })\n\n public async run(): Promise<void> {\n const {flags} = await this.parse(UploadAssetCommand)\n const sourceUrl = flags['from-url']\n\n if (sourceUrl && flags['content-type']) {\n this.error(\n 'Asset upload failed: --content-type cannot be combined with --from-url. Sanity derives the MIME type automatically.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n const sourceUrlError = sourceUrl === undefined ? undefined : getIngestUrlError(sourceUrl)\n if (sourceUrlError) {\n this.error(`Asset upload failed: ${sourceUrlError}`, {exit: exitCodes.USAGE_ERROR})\n }\n\n const filenameError =\n flags.filename === undefined ? undefined : getAssetFilenameError(flags.filename)\n if (filenameError) {\n this.error(`Asset upload failed: ${filenameError}`, {exit: exitCodes.USAGE_ERROR})\n }\n\n const cliConfig = await this.tryGetCliConfig()\n const projectId = await this.getProjectId({fallback: () => promptForProject({})})\n const dataset = flags.dataset ?? cliConfig.api?.dataset\n if (!dataset) {\n this.error(\n 'Asset upload failed: Dataset is required. Pass --dataset <name> or configure a dataset in sanity.cli.ts.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n const isInteractive = this.resolveIsInteractive()\n\n try {\n let asset\n if (sourceUrl === undefined) {\n // `exactlyOne` on the flags guarantees a source, but not to the compiler.\n if (flags.file === undefined) {\n this.error('Asset upload failed: Pass either --file <path> or --from-url <url>.', {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n const filePath = resolve(flags.file)\n asset = await uploadAssetWithProgress({\n assetType: flags.type,\n contentType: flags['content-type'],\n dataset,\n filename: flags.filename ?? basename(filePath),\n filePath,\n isInteractive,\n logToStderr: (message) => this.logToStderr(message),\n projectId,\n })\n } else {\n asset = await ingestAssetFromUrlWithProgress({\n assetType: flags.type,\n dataset,\n filename: flags.filename,\n isInteractive,\n logToStderr: (message) => this.logToStderr(message),\n projectId,\n url: sourceUrl,\n })\n }\n const fieldType = flags.type === 'image' ? 'image' : 'file'\n\n this.log(\n JSON.stringify(\n {\n asset: {\n _id: asset._id,\n _type: asset._type,\n extension: asset.extension,\n mimeType: asset.mimeType,\n originalFilename: asset.originalFilename,\n size: asset.size,\n url: asset.url,\n },\n reference: {\n _type: fieldType,\n asset: {_ref: asset._id, _type: 'reference'},\n },\n },\n null,\n 2,\n ),\n )\n } catch (error) {\n if (error instanceof Error && error.message === 'SIGINT') throw error\n if (error instanceof AssetFileError) {\n if (error.reason === 'not-file') {\n this.error('Asset upload failed: --file must point to a file, not a directory.', {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n this.error(\n 'Asset upload failed: Cannot read the local file. Check that --file points to a readable file, then retry.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n uploadAssetDebug('Asset upload failed', error)\n this.error(\n getAssetUploadErrorMessage(error, {\n fromUrl: sourceUrl !== undefined,\n target: 'dataset',\n }),\n {exit: exitCodes.RUNTIME_ERROR},\n )\n }\n }\n}\n"],"names":["basename","resolve","Flags","exitCodes","SanityCommand","subdebug","AssetFileError","ingestAssetFromUrlWithProgress","uploadAssetWithProgress","promptForProject","getAssetFilenameError","getIngestUrlError","getAssetUploadErrorMessage","getDatasetFlag","getProjectIdFlag","defineCommandTelemetry","uploadAssetDebug","flags","description","semantics","string","helpValue","file","exactlyOne","filename","type","custom","default","options","UploadAssetCommand","examples","command","hiddenAliases","telemetry","redact","run","parse","sourceUrl","error","exit","USAGE_ERROR","sourceUrlError","undefined","filenameError","cliConfig","tryGetCliConfig","projectId","getProjectId","fallback","dataset","api","isInteractive","resolveIsInteractive","asset","filePath","assetType","contentType","logToStderr","message","url","fieldType","log","JSON","stringify","_id","_type","extension","mimeType","originalFilename","size","reference","_ref","Error","reason","fromUrl","target","RUNTIME_ERROR"],"mappings":"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"}
@@ -2,9 +2,11 @@ import path from 'node:path';
2
2
  import { Args, Flags } from '@oclif/core';
3
3
  import { exitCodes, SanityCommand } from '@sanity/cli-core';
4
4
  import { confirm } from '@sanity/cli-core/ux';
5
+ import { getWorkbench, resolveWorkbenchConfig } from '@sanity/workbench-cli/deploy';
5
6
  import { deployApp } from '../actions/deploy/deployApp.js';
6
7
  import { deployDebug } from '../actions/deploy/deployDebug.js';
7
8
  import { deployStudio } from '../actions/deploy/deployStudio.js';
9
+ import { getAppId } from '../util/appId.js';
8
10
  import { determineIsApp } from '../util/determineIsApp.js';
9
11
  import { dirIsEmptyOrNonExistent } from '../util/dirIsEmptyOrNonExistent.js';
10
12
  export class DeployCommand extends SanityCommand {
@@ -30,6 +32,18 @@ export class DeployCommand extends SanityCommand {
30
32
  {
31
33
  command: '<%= config.bin %> <%= command.id %> --external',
32
34
  description: 'Register an externally hosted studio (studioHost contains full URL)'
35
+ },
36
+ {
37
+ command: '<%= config.bin %> <%= command.id %> --create --title "My App" --yes --json',
38
+ description: 'Create and deploy an App SDK app without prompts. Save application.id as deployment.appId, then omit --create on later deploys'
39
+ },
40
+ {
41
+ command: '<%= config.bin %> <%= command.id %> --create --title "My App" --dry-run --json',
42
+ description: 'Preview a new App SDK deployment without creating or uploading anything'
43
+ },
44
+ {
45
+ command: '<%= config.bin %> <%= command.id %> --url my-studio --yes',
46
+ description: 'Deploy a studio without prompts using its hostname'
33
47
  }
34
48
  ];
35
49
  static flags = {
@@ -43,6 +57,13 @@ export class DeployCommand extends SanityCommand {
43
57
  default: true,
44
58
  description: 'Build the studio before deploying (use --no-build to deploy existing `dist/` output)'
45
59
  }),
60
+ create: Flags.boolean({
61
+ description: 'Create a new App SDK app, even if the organization has other apps',
62
+ exclusive: [
63
+ 'external',
64
+ 'url'
65
+ ]
66
+ }),
46
67
  'dry-run': Flags.boolean({
47
68
  default: false,
48
69
  description: 'Report what would be deployed without uploading or creating anything'
@@ -75,7 +96,7 @@ export class DeployCommand extends SanityCommand {
75
96
  description: 'Enable source maps for built bundles (increases size of bundle)'
76
97
  }),
77
98
  title: Flags.string({
78
- description: 'Title for a newly created application or studio. For apps it also skips the interactive title prompt, enabling unattended creation'
99
+ description: 'Title for a newly created application or studio. For apps, use --create to select creation when the organization already has apps'
79
100
  }),
80
101
  url: Flags.string({
81
102
  description: 'Studio URL for deployment. For external studios, the full URL. For hosted studios, the hostname (e.g. "my-studio" or "my-studio.sanity.studio")'
@@ -95,6 +116,17 @@ export class DeployCommand extends SanityCommand {
95
116
  const cliConfig = await this.getCliConfig();
96
117
  const projectRoot = await this.getProjectRoot();
97
118
  const isApp = determineIsApp(cliConfig);
119
+ if (flags.create) {
120
+ if (!isApp) {
121
+ return this.output.error('--create is only supported for App SDK apps. For a studio, use --url <hostname>.');
122
+ }
123
+ if (getAppId(cliConfig)) {
124
+ return this.output.error('--create cannot be used with a configured app ID. Omit --create to deploy to that app, or remove deployment.appId and app.id to create a new one.');
125
+ }
126
+ if (getWorkbench(cliConfig) || resolveWorkbenchConfig(cliConfig)) {
127
+ return this.output.error('--create is not supported for this app configuration. Omit --create and run sanity deploy.');
128
+ }
129
+ }
98
130
  const defaultOutputDir = path.resolve(path.join(projectRoot.directory, 'dist'));
99
131
  const sourceDir = path.resolve(process.cwd(), this.args.sourceDir || defaultOutputDir);
100
132
  // Skip the directory check if the studio is externally hosted
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/commands/deploy.ts"],"sourcesContent":["import path from 'node:path'\n\nimport {Args, Flags} from '@oclif/core'\nimport {exitCodes, SanityCommand} from '@sanity/cli-core'\nimport {confirm} from '@sanity/cli-core/ux'\n\nimport {deployApp} from '../actions/deploy/deployApp.js'\nimport {deployDebug} from '../actions/deploy/deployDebug.js'\nimport {deployStudio} from '../actions/deploy/deployStudio.js'\nimport {determineIsApp} from '../util/determineIsApp.js'\nimport {dirIsEmptyOrNonExistent} from '../util/dirIsEmptyOrNonExistent.js'\n\nexport class DeployCommand extends SanityCommand<typeof DeployCommand> {\n static override args = {\n sourceDir: Args.directory({\n description: 'Source directory',\n }),\n }\n\n static override description = 'Builds and deploys Sanity Studio or application to Sanity hosting'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Build and deploy the studio to Sanity hosting',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --no-minify --source-maps',\n description: 'Deploys non-minified build with source maps',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --schema-required',\n description:\n 'Fail fast on schema store fails - for when other services rely on the stored schema',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --external',\n description: 'Register an externally hosted studio (studioHost contains full URL)',\n },\n ]\n\n static override flags = {\n 'auto-updates': Flags.boolean({\n allowNo: true,\n deprecated: true,\n description: 'Automatically update the studio to the latest version',\n }),\n build: Flags.boolean({\n allowNo: true,\n default: true,\n description:\n 'Build the studio before deploying (use --no-build to deploy existing `dist/` output)',\n }),\n 'dry-run': Flags.boolean({\n default: false,\n description: 'Report what would be deployed without uploading or creating anything',\n }),\n external: Flags.boolean({\n default: false,\n description: 'Register an externally hosted studio',\n exclusive: ['source-maps', 'minify', 'build'],\n }),\n json: Flags.boolean({\n char: 'j',\n default: false,\n description: 'Output the result as JSON',\n }),\n minify: Flags.boolean({\n allowNo: true,\n default: true,\n description: 'Minify built JavaScript (use --no-minify to skip for faster builds)',\n }),\n 'schema-required': Flags.boolean({\n default: false,\n description: 'Fail if schema deployment fails',\n }),\n 'source-maps': Flags.boolean({\n default: false,\n description: 'Enable source maps for built bundles (increases size of bundle)',\n }),\n title: Flags.string({\n description:\n 'Title for a newly created application or studio. For apps it also skips the interactive title prompt, enabling unattended creation',\n }),\n url: Flags.string({\n description:\n 'Studio URL for deployment. For external studios, the full URL. For hosted studios, the hostname (e.g. \"my-studio\" or \"my-studio.sanity.studio\")',\n }),\n verbose: Flags.boolean({\n default: false,\n description: 'Enable verbose logging',\n }),\n yes: Flags.boolean({\n char: 'y',\n default: false,\n description:\n 'Unattended mode, answers \"yes\" to any \"yes/no\" prompt and otherwise uses defaults',\n }),\n }\n\n public async run(): Promise<void> {\n const {flags} = await this.parse(DeployCommand)\n\n const cliConfig = await this.getCliConfig()\n const projectRoot = await this.getProjectRoot()\n\n const isApp = determineIsApp(cliConfig)\n\n const defaultOutputDir = path.resolve(path.join(projectRoot.directory, 'dist'))\n const sourceDir = path.resolve(process.cwd(), this.args.sourceDir || defaultOutputDir)\n\n // Skip the directory check if the studio is externally hosted\n if (this.args.sourceDir && this.args.sourceDir !== 'dist' && !flags.external) {\n let relativeOutput = path.relative(process.cwd(), sourceDir)\n if (relativeOutput[0] !== '.') {\n relativeOutput = `./${relativeOutput}`\n }\n\n if (!this.isUnattended() && !flags['dry-run']) {\n const isEmpty = await dirIsEmptyOrNonExistent(sourceDir)\n const shouldProceed =\n isEmpty ||\n (await confirm({\n default: false,\n message: `\"${relativeOutput}\" is not empty, do you want to proceed?`,\n }))\n\n if (!shouldProceed) {\n this.output.error('Cancelled.', {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n\n // Keep --json's stdout clean for the payload\n if (!flags.json) this.output.log(`Building to ${relativeOutput}\\n`)\n }\n\n // Force yes downstream: build/app resolution otherwise prompts for prerelease/version choices\n const deployFlags = this.isUnattended() || flags['dry-run'] ? {...flags, yes: true} : flags\n\n if (isApp) {\n deployDebug('Deploying app')\n await deployApp({\n cliConfig,\n flags: deployFlags,\n output: this.output,\n projectRoot,\n sourceDir,\n })\n } else {\n deployDebug('Deploying studio')\n await deployStudio({\n cliConfig,\n flags: deployFlags,\n output: this.output,\n projectRoot,\n sourceDir,\n })\n }\n }\n}\n"],"names":["path","Args","Flags","exitCodes","SanityCommand","confirm","deployApp","deployDebug","deployStudio","determineIsApp","dirIsEmptyOrNonExistent","DeployCommand","args","sourceDir","directory","description","examples","command","flags","boolean","allowNo","deprecated","build","default","external","exclusive","json","char","minify","title","string","url","verbose","yes","run","parse","cliConfig","getCliConfig","projectRoot","getProjectRoot","isApp","defaultOutputDir","resolve","join","process","cwd","relativeOutput","relative","isUnattended","isEmpty","shouldProceed","message","output","error","exit","RUNTIME_ERROR","log","deployFlags"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/commands/deploy.ts"],"sourcesContent":["import path from 'node:path'\n\nimport {Args, Flags} from '@oclif/core'\nimport {exitCodes, SanityCommand} from '@sanity/cli-core'\nimport {confirm} from '@sanity/cli-core/ux'\nimport {getWorkbench, resolveWorkbenchConfig} from '@sanity/workbench-cli/deploy'\n\nimport {deployApp} from '../actions/deploy/deployApp.js'\nimport {deployDebug} from '../actions/deploy/deployDebug.js'\nimport {deployStudio} from '../actions/deploy/deployStudio.js'\nimport {getAppId} from '../util/appId.js'\nimport {determineIsApp} from '../util/determineIsApp.js'\nimport {dirIsEmptyOrNonExistent} from '../util/dirIsEmptyOrNonExistent.js'\n\nexport class DeployCommand extends SanityCommand<typeof DeployCommand> {\n static override args = {\n sourceDir: Args.directory({\n description: 'Source directory',\n }),\n }\n\n static override description = 'Builds and deploys Sanity Studio or application to Sanity hosting'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Build and deploy the studio to Sanity hosting',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --no-minify --source-maps',\n description: 'Deploys non-minified build with source maps',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --schema-required',\n description:\n 'Fail fast on schema store fails - for when other services rely on the stored schema',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --external',\n description: 'Register an externally hosted studio (studioHost contains full URL)',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --create --title \"My App\" --yes --json',\n description:\n 'Create and deploy an App SDK app without prompts. Save application.id as deployment.appId, then omit --create on later deploys',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --create --title \"My App\" --dry-run --json',\n description: 'Preview a new App SDK deployment without creating or uploading anything',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --url my-studio --yes',\n description: 'Deploy a studio without prompts using its hostname',\n },\n ]\n\n static override flags = {\n 'auto-updates': Flags.boolean({\n allowNo: true,\n deprecated: true,\n description: 'Automatically update the studio to the latest version',\n }),\n build: Flags.boolean({\n allowNo: true,\n default: true,\n description:\n 'Build the studio before deploying (use --no-build to deploy existing `dist/` output)',\n }),\n create: Flags.boolean({\n description: 'Create a new App SDK app, even if the organization has other apps',\n exclusive: ['external', 'url'],\n }),\n 'dry-run': Flags.boolean({\n default: false,\n description: 'Report what would be deployed without uploading or creating anything',\n }),\n external: Flags.boolean({\n default: false,\n description: 'Register an externally hosted studio',\n exclusive: ['source-maps', 'minify', 'build'],\n }),\n json: Flags.boolean({\n char: 'j',\n default: false,\n description: 'Output the result as JSON',\n }),\n minify: Flags.boolean({\n allowNo: true,\n default: true,\n description: 'Minify built JavaScript (use --no-minify to skip for faster builds)',\n }),\n 'schema-required': Flags.boolean({\n default: false,\n description: 'Fail if schema deployment fails',\n }),\n 'source-maps': Flags.boolean({\n default: false,\n description: 'Enable source maps for built bundles (increases size of bundle)',\n }),\n title: Flags.string({\n description:\n 'Title for a newly created application or studio. For apps, use --create to select creation when the organization already has apps',\n }),\n url: Flags.string({\n description:\n 'Studio URL for deployment. For external studios, the full URL. For hosted studios, the hostname (e.g. \"my-studio\" or \"my-studio.sanity.studio\")',\n }),\n verbose: Flags.boolean({\n default: false,\n description: 'Enable verbose logging',\n }),\n yes: Flags.boolean({\n char: 'y',\n default: false,\n description:\n 'Unattended mode, answers \"yes\" to any \"yes/no\" prompt and otherwise uses defaults',\n }),\n }\n\n public async run(): Promise<void> {\n const {flags} = await this.parse(DeployCommand)\n\n const cliConfig = await this.getCliConfig()\n const projectRoot = await this.getProjectRoot()\n\n const isApp = determineIsApp(cliConfig)\n\n if (flags.create) {\n if (!isApp) {\n return this.output.error(\n '--create is only supported for App SDK apps. For a studio, use --url <hostname>.',\n )\n }\n if (getAppId(cliConfig)) {\n return this.output.error(\n '--create cannot be used with a configured app ID. Omit --create to deploy to that app, or remove deployment.appId and app.id to create a new one.',\n )\n }\n if (getWorkbench(cliConfig) || resolveWorkbenchConfig(cliConfig)) {\n return this.output.error(\n '--create is not supported for this app configuration. Omit --create and run sanity deploy.',\n )\n }\n }\n\n const defaultOutputDir = path.resolve(path.join(projectRoot.directory, 'dist'))\n const sourceDir = path.resolve(process.cwd(), this.args.sourceDir || defaultOutputDir)\n\n // Skip the directory check if the studio is externally hosted\n if (this.args.sourceDir && this.args.sourceDir !== 'dist' && !flags.external) {\n let relativeOutput = path.relative(process.cwd(), sourceDir)\n if (relativeOutput[0] !== '.') {\n relativeOutput = `./${relativeOutput}`\n }\n\n if (!this.isUnattended() && !flags['dry-run']) {\n const isEmpty = await dirIsEmptyOrNonExistent(sourceDir)\n const shouldProceed =\n isEmpty ||\n (await confirm({\n default: false,\n message: `\"${relativeOutput}\" is not empty, do you want to proceed?`,\n }))\n\n if (!shouldProceed) {\n this.output.error('Cancelled.', {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n\n // Keep --json's stdout clean for the payload\n if (!flags.json) this.output.log(`Building to ${relativeOutput}\\n`)\n }\n\n // Force yes downstream: build/app resolution otherwise prompts for prerelease/version choices\n const deployFlags = this.isUnattended() || flags['dry-run'] ? {...flags, yes: true} : flags\n\n if (isApp) {\n deployDebug('Deploying app')\n await deployApp({\n cliConfig,\n flags: deployFlags,\n output: this.output,\n projectRoot,\n sourceDir,\n })\n } else {\n deployDebug('Deploying studio')\n await deployStudio({\n cliConfig,\n flags: deployFlags,\n output: this.output,\n projectRoot,\n sourceDir,\n })\n }\n }\n}\n"],"names":["path","Args","Flags","exitCodes","SanityCommand","confirm","getWorkbench","resolveWorkbenchConfig","deployApp","deployDebug","deployStudio","getAppId","determineIsApp","dirIsEmptyOrNonExistent","DeployCommand","args","sourceDir","directory","description","examples","command","flags","boolean","allowNo","deprecated","build","default","create","exclusive","external","json","char","minify","title","string","url","verbose","yes","run","parse","cliConfig","getCliConfig","projectRoot","getProjectRoot","isApp","output","error","defaultOutputDir","resolve","join","process","cwd","relativeOutput","relative","isUnattended","isEmpty","shouldProceed","message","exit","RUNTIME_ERROR","log","deployFlags"],"mappings":"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"}
@@ -6,14 +6,41 @@ import { boxen, spinner } from '@sanity/cli-core/ux';
6
6
  import { pipe, scan, tap } from 'rxjs';
7
7
  import { importer } from '../../actions/media/importMedia.js';
8
8
  import { importMediaDebug } from '../../actions/media/importMediaDebug.js';
9
+ import { ingestMediaAssetFromUrlWithProgress } from '../../actions/media/ingestMediaAssetFromUrlWithProgress.js';
9
10
  import { promptForMediaLibrary } from '../../prompts/promptForMediaLibrary.js';
10
11
  import { promptForProject } from '../../prompts/promptForProject.js';
11
12
  import { getMediaLibraries } from '../../services/mediaLibraries.js';
13
+ import { getAssetFilenameError, getIngestUrlError } from '../../util/assetSourceValidation.js';
14
+ import { getAssetUploadErrorMessage } from '../../util/assetUploadErrors.js';
15
+ import { isRemoteAssetSource } from '../../util/isRemoteAssetSource.js';
16
+ import { parseAspectFlags } from '../../util/parseAspectFlags.js';
12
17
  import { getProjectIdFlag } from '../../util/sharedFlags.js';
18
+ import { defineCommandTelemetry } from '../../util/telemetry/commandTelemetry.js';
19
+ const flags = {
20
+ ...getProjectIdFlag({
21
+ description: 'Project ID to import media to',
22
+ semantics: 'override'
23
+ }),
24
+ aspect: Flags.string({
25
+ description: 'Aspect value to set on the imported asset, as key=value. Repeatable. Only applies when the source is a URL - directory and archive imports read aspects from their data.ndjson',
26
+ helpValue: '<key=value>',
27
+ multiple: true
28
+ }),
29
+ filename: Flags.string({
30
+ description: 'Original filename to store on the asset. Only applies when the source is a URL. Defaults to a name derived from the URL',
31
+ helpValue: '<filename>'
32
+ }),
33
+ 'media-library-id': requiredWhenUnattended(Flags.string({
34
+ description: 'The id of the target media library'
35
+ })),
36
+ 'replace-aspects': Flags.boolean({
37
+ description: 'Replace existing aspect data. All versions will be replaced (e.g. published and draft aspect data). Only applies to directory and archive imports'
38
+ })
39
+ };
13
40
  export class MediaImportCommand extends SanityCommand {
14
41
  static args = {
15
42
  source: Args.string({
16
- description: 'Image file or folder to import from',
43
+ description: 'Directory, archive, or asset URL to import from. An https URL imports one asset that Sanity fetches itself',
17
44
  required: true
18
45
  })
19
46
  };
@@ -30,24 +57,54 @@ export class MediaImportCommand extends SanityCommand {
30
57
  {
31
58
  command: '<%= config.bin %> <%= command.id %> products --replace-aspects',
32
59
  description: 'Import all assets from the "products" directory and replace aspects'
60
+ },
61
+ {
62
+ command: '<%= config.bin %> <%= command.id %> https://example.com/hero.png',
63
+ description: 'Have Sanity fetch a single asset from a public URL'
64
+ },
65
+ {
66
+ command: '<%= config.bin %> <%= command.id %> https://example.com/hero.png --aspect department=Brand',
67
+ description: 'Fetch an asset from a URL and set aspect data on it'
33
68
  }
34
69
  ];
35
- static flags = {
36
- ...getProjectIdFlag({
37
- description: 'Project ID to import media to',
38
- semantics: 'override'
39
- }),
40
- 'media-library-id': requiredWhenUnattended(Flags.string({
41
- description: 'The id of the target media library'
42
- })),
43
- 'replace-aspects': Flags.boolean({
44
- description: 'Replace existing aspect data. All versions will be replaced (e.g. published and draft aspect data)'
45
- })
46
- };
70
+ static flags = flags;
71
+ static telemetry = defineCommandTelemetry(flags, {
72
+ redact: [
73
+ 'aspect',
74
+ 'filename'
75
+ ]
76
+ });
47
77
  async run() {
48
78
  const { args, flags } = await this.parse(MediaImportCommand);
49
79
  const { source } = args;
50
80
  const replaceAspects = flags['replace-aspects'];
81
+ const isUrlSource = isRemoteAssetSource(source);
82
+ const aspects = this.resolveAspects({
83
+ aspectFlags: flags.aspect,
84
+ isUrlSource
85
+ });
86
+ if (!isUrlSource && flags.filename !== undefined) {
87
+ this.error('The --filename flag only applies when importing from a URL. Directory and archive imports keep each asset’s own filename.', {
88
+ exit: exitCodes.USAGE_ERROR
89
+ });
90
+ }
91
+ if (isUrlSource && replaceAspects) {
92
+ this.error('The --replace-aspects flag only applies to directory and archive imports. Aspects passed with --aspect are set when the asset is created.', {
93
+ exit: exitCodes.USAGE_ERROR
94
+ });
95
+ }
96
+ const filenameError = flags.filename === undefined ? undefined : getAssetFilenameError(flags.filename);
97
+ if (filenameError) {
98
+ this.error(filenameError, {
99
+ exit: exitCodes.USAGE_ERROR
100
+ });
101
+ }
102
+ const sourceUrlError = isUrlSource ? getIngestUrlError(source) : undefined;
103
+ if (sourceUrlError) {
104
+ this.error(sourceUrlError, {
105
+ exit: exitCodes.USAGE_ERROR
106
+ });
107
+ }
51
108
  const projectId = await this.getProjectId({
52
109
  fallback: ()=>promptForProject({})
53
110
  });
@@ -85,6 +142,19 @@ export class MediaImportCommand extends SanityCommand {
85
142
  exit: exitCodes.RUNTIME_ERROR
86
143
  });
87
144
  }
145
+ if (isUrlSource) {
146
+ await this.importFromUrl({
147
+ ...aspects ? {
148
+ aspects
149
+ } : {},
150
+ ...flags.filename ? {
151
+ filename: flags.filename
152
+ } : {},
153
+ mediaLibraryId,
154
+ url: source
155
+ });
156
+ return;
157
+ }
88
158
  const projectClient = await getProjectCliClient({
89
159
  apiVersion: 'v2025-02-19',
90
160
  dataset,
@@ -142,6 +212,53 @@ export class MediaImportCommand extends SanityCommand {
142
212
  });
143
213
  });
144
214
  }
215
+ /**
216
+ * Import one asset that Sanity fetches from a URL itself.
217
+ *
218
+ * Nothing but the resulting documents goes to stdout, so the output stays
219
+ * consumable as JSON; the wait is reported on stderr by the spinner.
220
+ */ async importFromUrl(options) {
221
+ const { aspects, filename, mediaLibraryId, url } = options;
222
+ let result;
223
+ try {
224
+ result = await ingestMediaAssetFromUrlWithProgress({
225
+ ...aspects ? {
226
+ aspects
227
+ } : {},
228
+ ...filename ? {
229
+ filename
230
+ } : {},
231
+ isInteractive: this.resolveIsInteractive(),
232
+ logToStderr: (message)=>this.logToStderr(message),
233
+ mediaLibraryId,
234
+ url
235
+ });
236
+ } catch (error) {
237
+ if (error instanceof Error && error.message === 'SIGINT') throw error;
238
+ importMediaDebug('Error importing asset from URL', error);
239
+ this.error(getAssetUploadErrorMessage(error, {
240
+ fromUrl: true,
241
+ target: 'media-library'
242
+ }), {
243
+ exit: exitCodes.RUNTIME_ERROR
244
+ });
245
+ }
246
+ this.log(JSON.stringify({
247
+ asset: {
248
+ _id: result.asset._id,
249
+ _type: result.asset._type,
250
+ assetType: result.asset.assetType
251
+ },
252
+ assetInstance: {
253
+ _id: result.assetInstance._id,
254
+ extension: result.assetInstance.extension,
255
+ mimeType: result.assetInstance.mimeType,
256
+ originalFilename: result.assetInstance.originalFilename,
257
+ size: result.assetInstance.size,
258
+ url: result.assetInstance.url
259
+ }
260
+ }, null, 2));
261
+ }
145
262
  reportResult(spin) {
146
263
  let previousState;
147
264
  return pipe(scan((processedAssetsCount, state)=>[
@@ -158,6 +275,25 @@ export class MediaImportCommand extends SanityCommand {
158
275
  }
159
276
  }));
160
277
  }
278
+ /**
279
+ * Validate `--aspect` against the source kind and shape it for the request.
280
+ * Runs before any network call so a malformed flag fails immediately.
281
+ */ resolveAspects(options) {
282
+ const { aspectFlags, isUrlSource } = options;
283
+ if (!aspectFlags?.length) return undefined;
284
+ if (!isUrlSource) {
285
+ this.error('The --aspect flag only applies when importing from a URL. Directory and archive imports read aspect data from their data.ndjson file.', {
286
+ exit: exitCodes.USAGE_ERROR
287
+ });
288
+ }
289
+ const parsed = parseAspectFlags(aspectFlags);
290
+ if ('error' in parsed) {
291
+ this.error(parsed.error, {
292
+ exit: exitCodes.USAGE_ERROR
293
+ });
294
+ }
295
+ return parsed.aspects;
296
+ }
161
297
  }
162
298
 
163
299
  //# sourceMappingURL=import.js.map