@sanity/cli 8.10.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 (63) 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/auth/login/getProvider.js +10 -14
  9. package/dist/actions/auth/login/getProvider.js.map +1 -1
  10. package/dist/actions/auth/login/login.js +0 -1
  11. package/dist/actions/auth/login/login.js.map +1 -1
  12. package/dist/actions/deploy/deployApp.js +5 -2
  13. package/dist/actions/deploy/deployApp.js.map +1 -1
  14. package/dist/actions/deploy/deployChecks.js +3 -2
  15. package/dist/actions/deploy/deployChecks.js.map +1 -1
  16. package/dist/actions/deploy/findUserApplication.js +3 -2
  17. package/dist/actions/deploy/findUserApplication.js.map +1 -1
  18. package/dist/actions/deploy/resolveDeployTarget.js +6 -1
  19. package/dist/actions/deploy/resolveDeployTarget.js.map +1 -1
  20. package/dist/actions/documents/validateDocuments.worker.js +1 -4
  21. package/dist/actions/documents/validateDocuments.worker.js.map +1 -1
  22. package/dist/actions/media/importMedia.js.map +1 -1
  23. package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js +19 -0
  24. package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js.map +1 -0
  25. package/dist/commands/assets/upload.js +78 -52
  26. package/dist/commands/assets/upload.js.map +1 -1
  27. package/dist/commands/deploy.js +33 -1
  28. package/dist/commands/deploy.js.map +1 -1
  29. package/dist/commands/media/import.js +149 -13
  30. package/dist/commands/media/import.js.map +1 -1
  31. package/dist/exports/invokeSanityCli/commandPolicies/index.js +1 -4
  32. package/dist/exports/invokeSanityCli/commandPolicies/index.js.map +1 -1
  33. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +26 -6
  34. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
  35. package/dist/exports/invokeSanityCli/commandPolicies/resolve.js +112 -0
  36. package/dist/exports/invokeSanityCli/commandPolicies/resolve.js.map +1 -0
  37. package/dist/exports/invokeSanityCli/help.js +8 -1
  38. package/dist/exports/invokeSanityCli/help.js.map +1 -1
  39. package/dist/exports/invokeSanityCli/index.d.ts +1 -2
  40. package/dist/exports/invokeSanityCli/index.js +37 -15
  41. package/dist/exports/invokeSanityCli/index.js.map +1 -1
  42. package/dist/generated/apiRoutes.js +2 -0
  43. package/dist/generated/apiRoutes.js.map +1 -1
  44. package/dist/services/assets.js +46 -1
  45. package/dist/services/assets.js.map +1 -1
  46. package/dist/services/mediaLibraries.js +39 -1
  47. package/dist/services/mediaLibraries.js.map +1 -1
  48. package/dist/util/assetSourceValidation.js +56 -0
  49. package/dist/util/assetSourceValidation.js.map +1 -0
  50. package/dist/util/assetUploadErrors.js +64 -0
  51. package/dist/util/assetUploadErrors.js.map +1 -0
  52. package/dist/util/isRemoteAssetSource.js +19 -0
  53. package/dist/util/isRemoteAssetSource.js.map +1 -0
  54. package/dist/util/parseAspectFlags.js +34 -0
  55. package/dist/util/parseAspectFlags.js.map +1 -0
  56. package/oclif.config.js +0 -1
  57. package/oclif.manifest.json +2134 -2055
  58. package/package.json +18 -18
  59. package/templates/app-quickstart/AGENTS.md +18 -0
  60. package/templates/app-sanity-ui/AGENTS.md +18 -0
  61. package/templates/shopify/.gitignore +26 -0
  62. package/dist/exports/invokeSanityCli/commandPolicies/policy.js +0 -46
  63. package/dist/exports/invokeSanityCli/commandPolicies/policy.js.map +0 -1
@@ -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
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/commands/media/import.ts"],"sourcesContent":["import {styleText} from 'node:util'\n\nimport {Args, Flags} from '@oclif/core'\nimport {exitCodes, getProjectCliClient, SanityCommand} from '@sanity/cli-core'\nimport {requiredWhenUnattended} from '@sanity/cli-core/flags'\nimport {boxen, spinner} from '@sanity/cli-core/ux'\nimport {SanityClient} from '@sanity/client'\nimport {type OperatorFunction, pipe, scan, tap} from 'rxjs'\n\nimport {importer, type State} from '../../actions/media/importMedia.js'\nimport {importMediaDebug} from '../../actions/media/importMediaDebug.js'\nimport {promptForMediaLibrary} from '../../prompts/promptForMediaLibrary.js'\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {getMediaLibraries} from '../../services/mediaLibraries.js'\nimport {getProjectIdFlag} from '../../util/sharedFlags.js'\n\nexport class MediaImportCommand extends SanityCommand<typeof MediaImportCommand> {\n static override args = {\n source: Args.string({\n description: 'Image file or folder to import from',\n required: true,\n }),\n }\n\n static override description = 'Import a set of assets to the target media library.'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %> products',\n description: 'Import all assets from the \"products\" directory',\n },\n {\n command: '<%= config.bin %> <%= command.id %> gallery.tar.gz',\n description: 'Import all assets from \"gallery\" archive',\n },\n {\n command: '<%= config.bin %> <%= command.id %> products --replace-aspects',\n description: 'Import all assets from the \"products\" directory and replace aspects',\n },\n ]\n\n static override flags = {\n ...getProjectIdFlag({\n description: 'Project ID to import media to',\n semantics: 'override',\n }),\n 'media-library-id': requiredWhenUnattended(\n Flags.string({\n description: 'The id of the target media library',\n }),\n ),\n 'replace-aspects': Flags.boolean({\n description:\n 'Replace existing aspect data. All versions will be replaced (e.g. published and draft aspect data)',\n }),\n }\n\n public async run(): Promise<void> {\n const {args, flags} = await this.parse(MediaImportCommand)\n const {source} = args\n const replaceAspects = flags['replace-aspects']\n\n const projectId = await this.getProjectId({fallback: () => promptForProject({})})\n\n const cliConfig = await this.tryGetCliConfig()\n const dataset = cliConfig.api?.dataset\n\n let mediaLibraries\n try {\n mediaLibraries = await getMediaLibraries(projectId)\n } catch (error) {\n importMediaDebug('Error listing media libraries', error)\n this.error(\n `Failed to list media libraries:\\n${error instanceof Error ? error.message : error}`,\n {\n exit: exitCodes.RUNTIME_ERROR,\n },\n )\n }\n\n if (mediaLibraries.length === 0) {\n this.error('No active media libraries found in this project', {exit: exitCodes.RUNTIME_ERROR})\n }\n\n let mediaLibraryId = flags['media-library-id']\n if (!mediaLibraryId) {\n try {\n mediaLibraryId = await promptForMediaLibrary({mediaLibraries})\n } catch (error) {\n importMediaDebug('Error selecting media library', error)\n this.error(\n `Failed to select media library:\\n${error instanceof Error ? error.message : error}`,\n {\n exit: exitCodes.RUNTIME_ERROR,\n },\n )\n }\n }\n\n if (!mediaLibraries.some((library) => library.id === mediaLibraryId)) {\n this.error(`Media library with id \"${mediaLibraryId}\" not found`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n const projectClient = await getProjectCliClient({\n apiVersion: 'v2025-02-19',\n dataset,\n perspective: 'drafts',\n projectId,\n requestTagPrefix: 'sanity.mediaLibraryCli.import',\n requireUser: true,\n '~experimental_resource': {\n id: mediaLibraryId,\n type: 'media-library',\n },\n })\n\n this.log(\n boxen(\n `\n Importing to media library: ${mediaLibraryId.padEnd(37)}\n Importing from path: ${source}\n `,\n {\n borderColor: 'yellow',\n borderStyle: 'round',\n },\n ),\n )\n\n const spin = spinner('Beginning import…').start()\n\n await this.importAssets({projectClient, replaceAspects, source, spin})\n }\n\n private async importAssets(options: {\n projectClient: SanityClient\n replaceAspects: boolean\n source: string\n spin: ReturnType<typeof spinner>\n }): Promise<void> {\n const {projectClient, replaceAspects, source, spin} = options\n\n return new Promise<void>((resolve, reject) => {\n const subscription = importer({\n client: projectClient,\n replaceAspects,\n sourcePath: source,\n spinner: spin,\n })\n .pipe(this.reportResult(spin))\n .subscribe({\n complete: () => {\n resolve()\n },\n error: (error: unknown) => {\n const message = error instanceof Error ? error.message : String(error)\n spin.stop()\n reject(new Error(message))\n },\n })\n\n // Cleanup on Ctrl+C\n process.once('SIGINT', () => {\n subscription.unsubscribe()\n spin.fail('Import interrupted.')\n process.exit(exitCodes.SIGINT)\n })\n }).catch((error) => {\n this.error(styleText('red', error.message), {exit: exitCodes.RUNTIME_ERROR})\n })\n }\n\n private reportResult(\n spin: ReturnType<typeof spinner>,\n ): OperatorFunction<State, [number, State | undefined]> {\n let previousState: State | undefined\n\n return pipe(\n scan<State, [number, State | undefined]>(\n (processedAssetsCount, state) => [processedAssetsCount[0] + 1, state],\n [0, undefined],\n ),\n tap({\n complete: () => spin.succeed(`Imported ${previousState?.fileCount} assets`),\n next: ([processedAssetsCount, state]) => {\n previousState = state\n spin.text = `${processedAssetsCount} of ${state?.fileCount} assets imported ${styleText('dim', state?.asset.originalFilename ?? '')}`\n },\n }),\n )\n }\n}\n"],"names":["styleText","Args","Flags","exitCodes","getProjectCliClient","SanityCommand","requiredWhenUnattended","boxen","spinner","pipe","scan","tap","importer","importMediaDebug","promptForMediaLibrary","promptForProject","getMediaLibraries","getProjectIdFlag","MediaImportCommand","args","source","string","description","required","examples","command","flags","semantics","boolean","run","parse","replaceAspects","projectId","getProjectId","fallback","cliConfig","tryGetCliConfig","dataset","api","mediaLibraries","error","Error","message","exit","RUNTIME_ERROR","length","mediaLibraryId","some","library","id","projectClient","apiVersion","perspective","requestTagPrefix","requireUser","type","log","padEnd","borderColor","borderStyle","spin","start","importAssets","options","Promise","resolve","reject","subscription","client","sourcePath","reportResult","subscribe","complete","String","stop","process","once","unsubscribe","fail","SIGINT","catch","previousState","processedAssetsCount","state","undefined","succeed","fileCount","next","text","asset","originalFilename"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/commands/media/import.ts"],"sourcesContent":["import {styleText} from 'node:util'\n\nimport {Args, Flags} from '@oclif/core'\nimport {type FlagInput} from '@oclif/core/interfaces'\nimport {exitCodes, getProjectCliClient, SanityCommand} from '@sanity/cli-core'\nimport {requiredWhenUnattended} from '@sanity/cli-core/flags'\nimport {boxen, spinner} from '@sanity/cli-core/ux'\nimport {SanityClient} from '@sanity/client'\nimport {type OperatorFunction, pipe, scan, tap} from 'rxjs'\n\nimport {importer, type State} from '../../actions/media/importMedia.js'\nimport {importMediaDebug} from '../../actions/media/importMediaDebug.js'\nimport {ingestMediaAssetFromUrlWithProgress} from '../../actions/media/ingestMediaAssetFromUrlWithProgress.js'\nimport {promptForMediaLibrary} from '../../prompts/promptForMediaLibrary.js'\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {getMediaLibraries} from '../../services/mediaLibraries.js'\nimport {getAssetFilenameError, getIngestUrlError} from '../../util/assetSourceValidation.js'\nimport {getAssetUploadErrorMessage} from '../../util/assetUploadErrors.js'\nimport {isRemoteAssetSource} from '../../util/isRemoteAssetSource.js'\nimport {parseAspectFlags} from '../../util/parseAspectFlags.js'\nimport {getProjectIdFlag} from '../../util/sharedFlags.js'\nimport {defineCommandTelemetry} from '../../util/telemetry/commandTelemetry.js'\n\nconst flags = {\n ...getProjectIdFlag({\n description: 'Project ID to import media to',\n semantics: 'override',\n }),\n aspect: Flags.string({\n description:\n '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',\n helpValue: '<key=value>',\n multiple: true,\n }),\n filename: Flags.string({\n description:\n 'Original filename to store on the asset. Only applies when the source is a URL. Defaults to a name derived from the URL',\n helpValue: '<filename>',\n }),\n 'media-library-id': requiredWhenUnattended(\n Flags.string({\n description: 'The id of the target media library',\n }),\n ),\n 'replace-aspects': Flags.boolean({\n description:\n 'Replace existing aspect data. All versions will be replaced (e.g. published and draft aspect data). Only applies to directory and archive imports',\n }),\n} satisfies FlagInput\n\nexport class MediaImportCommand extends SanityCommand<typeof MediaImportCommand> {\n static override args = {\n source: Args.string({\n description:\n 'Directory, archive, or asset URL to import from. An https URL imports one asset that Sanity fetches itself',\n required: true,\n }),\n }\n\n static override description = 'Import a set of assets to the target media library.'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %> products',\n description: 'Import all assets from the \"products\" directory',\n },\n {\n command: '<%= config.bin %> <%= command.id %> gallery.tar.gz',\n description: 'Import all assets from \"gallery\" archive',\n },\n {\n command: '<%= config.bin %> <%= command.id %> products --replace-aspects',\n description: 'Import all assets from the \"products\" directory and replace aspects',\n },\n {\n command: '<%= config.bin %> <%= command.id %> https://example.com/hero.png',\n description: 'Have Sanity fetch a single asset from a public URL',\n },\n {\n command:\n '<%= config.bin %> <%= command.id %> https://example.com/hero.png --aspect department=Brand',\n description: 'Fetch an asset from a URL and set aspect data on it',\n },\n ]\n\n static override flags = flags\n\n static telemetry = defineCommandTelemetry(flags, {\n redact: ['aspect', 'filename'],\n })\n\n public async run(): Promise<void> {\n const {args, flags} = await this.parse(MediaImportCommand)\n const {source} = args\n const replaceAspects = flags['replace-aspects']\n const isUrlSource = isRemoteAssetSource(source)\n\n const aspects = this.resolveAspects({aspectFlags: flags.aspect, isUrlSource})\n\n if (!isUrlSource && flags.filename !== undefined) {\n this.error(\n 'The --filename flag only applies when importing from a URL. Directory and archive imports keep each asset’s own filename.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n if (isUrlSource && replaceAspects) {\n this.error(\n 'The --replace-aspects flag only applies to directory and archive imports. Aspects passed with --aspect are set when the asset is created.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n const filenameError =\n flags.filename === undefined ? undefined : getAssetFilenameError(flags.filename)\n if (filenameError) {\n this.error(filenameError, {exit: exitCodes.USAGE_ERROR})\n }\n\n const sourceUrlError = isUrlSource ? getIngestUrlError(source) : undefined\n if (sourceUrlError) {\n this.error(sourceUrlError, {exit: exitCodes.USAGE_ERROR})\n }\n\n const projectId = await this.getProjectId({fallback: () => promptForProject({})})\n\n const cliConfig = await this.tryGetCliConfig()\n const dataset = cliConfig.api?.dataset\n\n let mediaLibraries\n try {\n mediaLibraries = await getMediaLibraries(projectId)\n } catch (error) {\n importMediaDebug('Error listing media libraries', error)\n this.error(\n `Failed to list media libraries:\\n${error instanceof Error ? error.message : error}`,\n {\n exit: exitCodes.RUNTIME_ERROR,\n },\n )\n }\n\n if (mediaLibraries.length === 0) {\n this.error('No active media libraries found in this project', {exit: exitCodes.RUNTIME_ERROR})\n }\n\n let mediaLibraryId = flags['media-library-id']\n if (!mediaLibraryId) {\n try {\n mediaLibraryId = await promptForMediaLibrary({mediaLibraries})\n } catch (error) {\n importMediaDebug('Error selecting media library', error)\n this.error(\n `Failed to select media library:\\n${error instanceof Error ? error.message : error}`,\n {\n exit: exitCodes.RUNTIME_ERROR,\n },\n )\n }\n }\n\n if (!mediaLibraries.some((library) => library.id === mediaLibraryId)) {\n this.error(`Media library with id \"${mediaLibraryId}\" not found`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n if (isUrlSource) {\n await this.importFromUrl({\n ...(aspects ? {aspects} : {}),\n ...(flags.filename ? {filename: flags.filename} : {}),\n mediaLibraryId,\n url: source,\n })\n return\n }\n\n const projectClient = await getProjectCliClient({\n apiVersion: 'v2025-02-19',\n dataset,\n perspective: 'drafts',\n projectId,\n requestTagPrefix: 'sanity.mediaLibraryCli.import',\n requireUser: true,\n '~experimental_resource': {\n id: mediaLibraryId,\n type: 'media-library',\n },\n })\n\n this.log(\n boxen(\n `\n Importing to media library: ${mediaLibraryId.padEnd(37)}\n Importing from path: ${source}\n `,\n {\n borderColor: 'yellow',\n borderStyle: 'round',\n },\n ),\n )\n\n const spin = spinner('Beginning import…').start()\n\n await this.importAssets({projectClient, replaceAspects, source, spin})\n }\n\n private async importAssets(options: {\n projectClient: SanityClient\n replaceAspects: boolean\n source: string\n spin: ReturnType<typeof spinner>\n }): Promise<void> {\n const {projectClient, replaceAspects, source, spin} = options\n\n return new Promise<void>((resolve, reject) => {\n const subscription = importer({\n client: projectClient,\n replaceAspects,\n sourcePath: source,\n spinner: spin,\n })\n .pipe(this.reportResult(spin))\n .subscribe({\n complete: () => {\n resolve()\n },\n error: (error: unknown) => {\n const message = error instanceof Error ? error.message : String(error)\n spin.stop()\n reject(new Error(message))\n },\n })\n\n // Cleanup on Ctrl+C\n process.once('SIGINT', () => {\n subscription.unsubscribe()\n spin.fail('Import interrupted.')\n process.exit(exitCodes.SIGINT)\n })\n }).catch((error) => {\n this.error(styleText('red', error.message), {exit: exitCodes.RUNTIME_ERROR})\n })\n }\n\n /**\n * Import one asset that Sanity fetches from a URL itself.\n *\n * Nothing but the resulting documents goes to stdout, so the output stays\n * consumable as JSON; the wait is reported on stderr by the spinner.\n */\n private async importFromUrl(options: {\n aspects?: Record<string, string>\n filename?: string\n\n mediaLibraryId: string\n url: string\n }): Promise<void> {\n const {aspects, filename, mediaLibraryId, url} = options\n\n let result\n try {\n result = await ingestMediaAssetFromUrlWithProgress({\n ...(aspects ? {aspects} : {}),\n ...(filename ? {filename} : {}),\n isInteractive: this.resolveIsInteractive(),\n logToStderr: (message) => this.logToStderr(message),\n mediaLibraryId,\n url,\n })\n } catch (error) {\n if (error instanceof Error && error.message === 'SIGINT') throw error\n importMediaDebug('Error importing asset from URL', error)\n this.error(getAssetUploadErrorMessage(error, {fromUrl: true, target: 'media-library'}), {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n this.log(\n JSON.stringify(\n {\n asset: {\n _id: result.asset._id,\n _type: result.asset._type,\n assetType: result.asset.assetType,\n },\n assetInstance: {\n _id: result.assetInstance._id,\n extension: result.assetInstance.extension,\n mimeType: result.assetInstance.mimeType,\n originalFilename: result.assetInstance.originalFilename,\n size: result.assetInstance.size,\n url: result.assetInstance.url,\n },\n },\n null,\n 2,\n ),\n )\n }\n\n private reportResult(\n spin: ReturnType<typeof spinner>,\n ): OperatorFunction<State, [number, State | undefined]> {\n let previousState: State | undefined\n\n return pipe(\n scan<State, [number, State | undefined]>(\n (processedAssetsCount, state) => [processedAssetsCount[0] + 1, state],\n [0, undefined],\n ),\n tap({\n complete: () => spin.succeed(`Imported ${previousState?.fileCount} assets`),\n next: ([processedAssetsCount, state]) => {\n previousState = state\n spin.text = `${processedAssetsCount} of ${state?.fileCount} assets imported ${styleText('dim', state?.asset.originalFilename ?? '')}`\n },\n }),\n )\n }\n\n /**\n * Validate `--aspect` against the source kind and shape it for the request.\n * Runs before any network call so a malformed flag fails immediately.\n */\n private resolveAspects(options: {\n aspectFlags?: string[]\n\n isUrlSource: boolean\n }): Record<string, string> | undefined {\n const {aspectFlags, isUrlSource} = options\n if (!aspectFlags?.length) return undefined\n\n if (!isUrlSource) {\n this.error(\n 'The --aspect flag only applies when importing from a URL. Directory and archive imports read aspect data from their data.ndjson file.',\n {exit: exitCodes.USAGE_ERROR},\n )\n }\n\n const parsed = parseAspectFlags(aspectFlags)\n if ('error' in parsed) {\n this.error(parsed.error, {exit: exitCodes.USAGE_ERROR})\n }\n\n return parsed.aspects\n }\n}\n"],"names":["styleText","Args","Flags","exitCodes","getProjectCliClient","SanityCommand","requiredWhenUnattended","boxen","spinner","pipe","scan","tap","importer","importMediaDebug","ingestMediaAssetFromUrlWithProgress","promptForMediaLibrary","promptForProject","getMediaLibraries","getAssetFilenameError","getIngestUrlError","getAssetUploadErrorMessage","isRemoteAssetSource","parseAspectFlags","getProjectIdFlag","defineCommandTelemetry","flags","description","semantics","aspect","string","helpValue","multiple","filename","boolean","MediaImportCommand","args","source","required","examples","command","telemetry","redact","run","parse","replaceAspects","isUrlSource","aspects","resolveAspects","aspectFlags","undefined","error","exit","USAGE_ERROR","filenameError","sourceUrlError","projectId","getProjectId","fallback","cliConfig","tryGetCliConfig","dataset","api","mediaLibraries","Error","message","RUNTIME_ERROR","length","mediaLibraryId","some","library","id","importFromUrl","url","projectClient","apiVersion","perspective","requestTagPrefix","requireUser","type","log","padEnd","borderColor","borderStyle","spin","start","importAssets","options","Promise","resolve","reject","subscription","client","sourcePath","reportResult","subscribe","complete","String","stop","process","once","unsubscribe","fail","SIGINT","catch","result","isInteractive","resolveIsInteractive","logToStderr","fromUrl","target","JSON","stringify","asset","_id","_type","assetType","assetInstance","extension","mimeType","originalFilename","size","previousState","processedAssetsCount","state","succeed","fileCount","next","text","parsed"],"mappings":"AAAA,SAAQA,SAAS,QAAO,YAAW;AAEnC,SAAQC,IAAI,EAAEC,KAAK,QAAO,cAAa;AAEvC,SAAQC,SAAS,EAAEC,mBAAmB,EAAEC,aAAa,QAAO,mBAAkB;AAC9E,SAAQC,sBAAsB,QAAO,yBAAwB;AAC7D,SAAQC,KAAK,EAAEC,OAAO,QAAO,sBAAqB;AAElD,SAA+BC,IAAI,EAAEC,IAAI,EAAEC,GAAG,QAAO,OAAM;AAE3D,SAAQC,QAAQ,QAAmB,qCAAoC;AACvE,SAAQC,gBAAgB,QAAO,0CAAyC;AACxE,SAAQC,mCAAmC,QAAO,6DAA4D;AAC9G,SAAQC,qBAAqB,QAAO,yCAAwC;AAC5E,SAAQC,gBAAgB,QAAO,oCAAmC;AAClE,SAAQC,iBAAiB,QAAO,mCAAkC;AAClE,SAAQC,qBAAqB,EAAEC,iBAAiB,QAAO,sCAAqC;AAC5F,SAAQC,0BAA0B,QAAO,kCAAiC;AAC1E,SAAQC,mBAAmB,QAAO,oCAAmC;AACrE,SAAQC,gBAAgB,QAAO,iCAAgC;AAC/D,SAAQC,gBAAgB,QAAO,4BAA2B;AAC1D,SAAQC,sBAAsB,QAAO,2CAA0C;AAE/E,MAAMC,QAAQ;IACZ,GAAGF,iBAAiB;QAClBG,aAAa;QACbC,WAAW;IACb,EAAE;IACFC,QAAQ1B,MAAM2B,MAAM,CAAC;QACnBH,aACE;QACFI,WAAW;QACXC,UAAU;IACZ;IACAC,UAAU9B,MAAM2B,MAAM,CAAC;QACrBH,aACE;QACFI,WAAW;IACb;IACA,oBAAoBxB,uBAClBJ,MAAM2B,MAAM,CAAC;QACXH,aAAa;IACf;IAEF,mBAAmBxB,MAAM+B,OAAO,CAAC;QAC/BP,aACE;IACJ;AACF;AAEA,OAAO,MAAMQ,2BAA2B7B;IACtC,OAAgB8B,OAAO;QACrBC,QAAQnC,KAAK4B,MAAM,CAAC;YAClBH,aACE;YACFW,UAAU;QACZ;IACF,EAAC;IAED,OAAgBX,cAAc,sDAAqD;IAEnF,OAAgBY,WAAW;QACzB;YACEC,SAAS;YACTb,aAAa;QACf;QACA;YACEa,SAAS;YACTb,aAAa;QACf;QACA;YACEa,SAAS;YACTb,aAAa;QACf;QACA;YACEa,SAAS;YACTb,aAAa;QACf;QACA;YACEa,SACE;YACFb,aAAa;QACf;KACD,CAAA;IAED,OAAgBD,QAAQA,MAAK;IAE7B,OAAOe,YAAYhB,uBAAuBC,OAAO;QAC/CgB,QAAQ;YAAC;YAAU;SAAW;IAChC,GAAE;IAEF,MAAaC,MAAqB;QAChC,MAAM,EAACP,IAAI,EAAEV,KAAK,EAAC,GAAG,MAAM,IAAI,CAACkB,KAAK,CAACT;QACvC,MAAM,EAACE,MAAM,EAAC,GAAGD;QACjB,MAAMS,iBAAiBnB,KAAK,CAAC,kBAAkB;QAC/C,MAAMoB,cAAcxB,oBAAoBe;QAExC,MAAMU,UAAU,IAAI,CAACC,cAAc,CAAC;YAACC,aAAavB,MAAMG,MAAM;YAAEiB;QAAW;QAE3E,IAAI,CAACA,eAAepB,MAAMO,QAAQ,KAAKiB,WAAW;YAChD,IAAI,CAACC,KAAK,CACR,6HACA;gBAACC,MAAMhD,UAAUiD,WAAW;YAAA;QAEhC;QAEA,IAAIP,eAAeD,gBAAgB;YACjC,IAAI,CAACM,KAAK,CACR,6IACA;gBAACC,MAAMhD,UAAUiD,WAAW;YAAA;QAEhC;QAEA,MAAMC,gBACJ5B,MAAMO,QAAQ,KAAKiB,YAAYA,YAAY/B,sBAAsBO,MAAMO,QAAQ;QACjF,IAAIqB,eAAe;YACjB,IAAI,CAACH,KAAK,CAACG,eAAe;gBAACF,MAAMhD,UAAUiD,WAAW;YAAA;QACxD;QAEA,MAAME,iBAAiBT,cAAc1B,kBAAkBiB,UAAUa;QACjE,IAAIK,gBAAgB;YAClB,IAAI,CAACJ,KAAK,CAACI,gBAAgB;gBAACH,MAAMhD,UAAUiD,WAAW;YAAA;QACzD;QAEA,MAAMG,YAAY,MAAM,IAAI,CAACC,YAAY,CAAC;YAACC,UAAU,IAAMzC,iBAAiB,CAAC;QAAE;QAE/E,MAAM0C,YAAY,MAAM,IAAI,CAACC,eAAe;QAC5C,MAAMC,UAAUF,UAAUG,GAAG,EAAED;QAE/B,IAAIE;QACJ,IAAI;YACFA,iBAAiB,MAAM7C,kBAAkBsC;QAC3C,EAAE,OAAOL,OAAO;YACdrC,iBAAiB,iCAAiCqC;YAClD,IAAI,CAACA,KAAK,CACR,CAAC,iCAAiC,EAAEA,iBAAiBa,QAAQb,MAAMc,OAAO,GAAGd,OAAO,EACpF;gBACEC,MAAMhD,UAAU8D,aAAa;YAC/B;QAEJ;QAEA,IAAIH,eAAeI,MAAM,KAAK,GAAG;YAC/B,IAAI,CAAChB,KAAK,CAAC,mDAAmD;gBAACC,MAAMhD,UAAU8D,aAAa;YAAA;QAC9F;QAEA,IAAIE,iBAAiB1C,KAAK,CAAC,mBAAmB;QAC9C,IAAI,CAAC0C,gBAAgB;YACnB,IAAI;gBACFA,iBAAiB,MAAMpD,sBAAsB;oBAAC+C;gBAAc;YAC9D,EAAE,OAAOZ,OAAO;gBACdrC,iBAAiB,iCAAiCqC;gBAClD,IAAI,CAACA,KAAK,CACR,CAAC,iCAAiC,EAAEA,iBAAiBa,QAAQb,MAAMc,OAAO,GAAGd,OAAO,EACpF;oBACEC,MAAMhD,UAAU8D,aAAa;gBAC/B;YAEJ;QACF;QAEA,IAAI,CAACH,eAAeM,IAAI,CAAC,CAACC,UAAYA,QAAQC,EAAE,KAAKH,iBAAiB;YACpE,IAAI,CAACjB,KAAK,CAAC,CAAC,uBAAuB,EAAEiB,eAAe,WAAW,CAAC,EAAE;gBAChEhB,MAAMhD,UAAU8D,aAAa;YAC/B;QACF;QAEA,IAAIpB,aAAa;YACf,MAAM,IAAI,CAAC0B,aAAa,CAAC;gBACvB,GAAIzB,UAAU;oBAACA;gBAAO,IAAI,CAAC,CAAC;gBAC5B,GAAIrB,MAAMO,QAAQ,GAAG;oBAACA,UAAUP,MAAMO,QAAQ;gBAAA,IAAI,CAAC,CAAC;gBACpDmC;gBACAK,KAAKpC;YACP;YACA;QACF;QAEA,MAAMqC,gBAAgB,MAAMrE,oBAAoB;YAC9CsE,YAAY;YACZd;YACAe,aAAa;YACbpB;YACAqB,kBAAkB;YAClBC,aAAa;YACb,0BAA0B;gBACxBP,IAAIH;gBACJW,MAAM;YACR;QACF;QAEA,IAAI,CAACC,GAAG,CACNxE,MACE,CAAC;sCAC6B,EAAE4D,eAAea,MAAM,CAAC,IAAI;+BACnC,EAAE5C,OAAO;QAChC,CAAC,EACD;YACE6C,aAAa;YACbC,aAAa;QACf;QAIJ,MAAMC,OAAO3E,QAAQ,qBAAqB4E,KAAK;QAE/C,MAAM,IAAI,CAACC,YAAY,CAAC;YAACZ;YAAe7B;YAAgBR;YAAQ+C;QAAI;IACtE;IAEA,MAAcE,aAAaC,OAK1B,EAAiB;QAChB,MAAM,EAACb,aAAa,EAAE7B,cAAc,EAAER,MAAM,EAAE+C,IAAI,EAAC,GAAGG;QAEtD,OAAO,IAAIC,QAAc,CAACC,SAASC;YACjC,MAAMC,eAAe9E,SAAS;gBAC5B+E,QAAQlB;gBACR7B;gBACAgD,YAAYxD;gBACZ5B,SAAS2E;YACX,GACG1E,IAAI,CAAC,IAAI,CAACoF,YAAY,CAACV,OACvBW,SAAS,CAAC;gBACTC,UAAU;oBACRP;gBACF;gBACAtC,OAAO,CAACA;oBACN,MAAMc,UAAUd,iBAAiBa,QAAQb,MAAMc,OAAO,GAAGgC,OAAO9C;oBAChEiC,KAAKc,IAAI;oBACTR,OAAO,IAAI1B,MAAMC;gBACnB;YACF;YAEF,oBAAoB;YACpBkC,QAAQC,IAAI,CAAC,UAAU;gBACrBT,aAAaU,WAAW;gBACxBjB,KAAKkB,IAAI,CAAC;gBACVH,QAAQ/C,IAAI,CAAChD,UAAUmG,MAAM;YAC/B;QACF,GAAGC,KAAK,CAAC,CAACrD;YACR,IAAI,CAACA,KAAK,CAAClD,UAAU,OAAOkD,MAAMc,OAAO,GAAG;gBAACb,MAAMhD,UAAU8D,aAAa;YAAA;QAC5E;IACF;IAEA;;;;;GAKC,GACD,MAAcM,cAAce,OAM3B,EAAiB;QAChB,MAAM,EAACxC,OAAO,EAAEd,QAAQ,EAAEmC,cAAc,EAAEK,GAAG,EAAC,GAAGc;QAEjD,IAAIkB;QACJ,IAAI;YACFA,SAAS,MAAM1F,oCAAoC;gBACjD,GAAIgC,UAAU;oBAACA;gBAAO,IAAI,CAAC,CAAC;gBAC5B,GAAId,WAAW;oBAACA;gBAAQ,IAAI,CAAC,CAAC;gBAC9ByE,eAAe,IAAI,CAACC,oBAAoB;gBACxCC,aAAa,CAAC3C,UAAY,IAAI,CAAC2C,WAAW,CAAC3C;gBAC3CG;gBACAK;YACF;QACF,EAAE,OAAOtB,OAAO;YACd,IAAIA,iBAAiBa,SAASb,MAAMc,OAAO,KAAK,UAAU,MAAMd;YAChErC,iBAAiB,kCAAkCqC;YACnD,IAAI,CAACA,KAAK,CAAC9B,2BAA2B8B,OAAO;gBAAC0D,SAAS;gBAAMC,QAAQ;YAAe,IAAI;gBACtF1D,MAAMhD,UAAU8D,aAAa;YAC/B;QACF;QAEA,IAAI,CAACc,GAAG,CACN+B,KAAKC,SAAS,CACZ;YACEC,OAAO;gBACLC,KAAKT,OAAOQ,KAAK,CAACC,GAAG;gBACrBC,OAAOV,OAAOQ,KAAK,CAACE,KAAK;gBACzBC,WAAWX,OAAOQ,KAAK,CAACG,SAAS;YACnC;YACAC,eAAe;gBACbH,KAAKT,OAAOY,aAAa,CAACH,GAAG;gBAC7BI,WAAWb,OAAOY,aAAa,CAACC,SAAS;gBACzCC,UAAUd,OAAOY,aAAa,CAACE,QAAQ;gBACvCC,kBAAkBf,OAAOY,aAAa,CAACG,gBAAgB;gBACvDC,MAAMhB,OAAOY,aAAa,CAACI,IAAI;gBAC/BhD,KAAKgC,OAAOY,aAAa,CAAC5C,GAAG;YAC/B;QACF,GACA,MACA;IAGN;IAEQqB,aACNV,IAAgC,EACsB;QACtD,IAAIsC;QAEJ,OAAOhH,KACLC,KACE,CAACgH,sBAAsBC,QAAU;gBAACD,oBAAoB,CAAC,EAAE,GAAG;gBAAGC;aAAM,EACrE;YAAC;YAAG1E;SAAU,GAEhBtC,IAAI;YACFoF,UAAU,IAAMZ,KAAKyC,OAAO,CAAC,CAAC,SAAS,EAAEH,eAAeI,UAAU,OAAO,CAAC;YAC1EC,MAAM,CAAC,CAACJ,sBAAsBC,MAAM;gBAClCF,gBAAgBE;gBAChBxC,KAAK4C,IAAI,GAAG,GAAGL,qBAAqB,IAAI,EAAEC,OAAOE,UAAU,iBAAiB,EAAE7H,UAAU,OAAO2H,OAAOX,MAAMO,oBAAoB,KAAK;YACvI;QACF;IAEJ;IAEA;;;GAGC,GACD,AAAQxE,eAAeuC,OAItB,EAAsC;QACrC,MAAM,EAACtC,WAAW,EAAEH,WAAW,EAAC,GAAGyC;QACnC,IAAI,CAACtC,aAAakB,QAAQ,OAAOjB;QAEjC,IAAI,CAACJ,aAAa;YAChB,IAAI,CAACK,KAAK,CACR,yIACA;gBAACC,MAAMhD,UAAUiD,WAAW;YAAA;QAEhC;QAEA,MAAM4E,SAAS1G,iBAAiB0B;QAChC,IAAI,WAAWgF,QAAQ;YACrB,IAAI,CAAC9E,KAAK,CAAC8E,OAAO9E,KAAK,EAAE;gBAACC,MAAMhD,UAAUiD,WAAW;YAAA;QACvD;QAEA,OAAO4E,OAAOlF,OAAO;IACvB;AACF"}
@@ -1,6 +1,3 @@
1
- import { mcpPolicy } from './mcpPolicy.js';
2
- export const commandPolicies = {
3
- mcp: mcpPolicy
4
- };
1
+ export { commandPolicies, resolveCommandPolicies } from './resolve.js';
5
2
 
6
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/index.ts"],"sourcesContent":["import {mcpPolicy} from './mcpPolicy.js'\nimport {type CommandPolicySet, type InvocationSource} from './policy.js'\n\nexport const commandPolicies: Record<InvocationSource, CommandPolicySet> = {\n mcp: mcpPolicy,\n}\n"],"names":["mcpPolicy","commandPolicies","mcp"],"mappings":"AAAA,SAAQA,SAAS,QAAO,iBAAgB;AAGxC,OAAO,MAAMC,kBAA8D;IACzEC,KAAKF;AACP,EAAC"}
1
+ {"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/index.ts"],"sourcesContent":["export {commandPolicies, resolveCommandPolicies} from './resolve.js'\n"],"names":["commandPolicies","resolveCommandPolicies"],"mappings":"AAAA,SAAQA,eAAe,EAAEC,sBAAsB,QAAO,eAAc"}
@@ -1,4 +1,5 @@
1
- import { allow, conditionalDenyFlags, conditionalPolicy, deny } from './policy.js';
1
+ import { allow, conditionalDenyFlags, conditionalPolicy, deny } from '@sanity/cli-core/commandPolicy';
2
+ import { isRemoteAssetSource } from '../../../util/isRemoteAssetSource.js';
2
3
  function apiValidator({ args, flags }) {
3
4
  const fields = Array.isArray(flags.field) ? flags.field : [];
4
5
  const fieldReadsFromHost = fields.some((field)=>{
@@ -26,10 +27,14 @@ function apiValidator({ args, flags }) {
26
27
  * a usage error, but cannot cause local filesystem access. Destructive remote
27
28
  * operations are allowed and do not by themselves make a command unsafe.
28
29
  *
29
- * Every manifest command must have exactly one policy here:
30
+ * Every command this package contributes must have exactly one policy here:
30
31
  * - allow: every valid invocation is safe
31
32
  * - conditional: safety depends on parsed arguments or flags
32
33
  * - deny: no invocation is safe
34
+ *
35
+ * Commands contributed by plugins are not listed here. Each plugin declares
36
+ * policies for its own commands (see `PluginInvocationPolicies` in
37
+ * `@sanity/cli-core/commandPolicy`).
33
38
  */ export const mcpPolicy = {
34
39
  // Special exception, this can be very dangerous but is also super useful
35
40
  // to expose. Refuse authentication overrides and host input channels:
@@ -44,8 +49,14 @@ function apiValidator({ args, flags }) {
44
49
  ],
45
50
  validate: apiValidator
46
51
  }),
47
- // Reads a file from the machine running the command and must only run locally.
48
- 'assets:upload': deny,
52
+ // --file reads from the machine running the command and must only run locally.
53
+ 'assets:upload': conditionalPolicy({
54
+ deniedFlags: [
55
+ 'content-type',
56
+ 'file'
57
+ ],
58
+ validate: ({ flags })=>typeof flags['from-url'] === 'string' && flags['from-url'].length > 0
59
+ }),
49
60
  'backups:disable': allow,
50
61
  // Writes a downloaded backup to the local filesystem.
51
62
  'backups:download': deny,
@@ -148,8 +159,17 @@ function apiValidator({ args, flags }) {
148
159
  'media:deploy-aspect': deny,
149
160
  // Writes media assets to the local filesystem.
150
161
  'media:export': deny,
151
- // Reads media assets from the local filesystem.
152
- 'media:import': deny,
162
+ // A directory or archive source reads media assets from the local
163
+ // filesystem. A URL source does not: Sanity fetches the asset itself, so the
164
+ // only local input is the URL. `--replace-aspects` is meaningless for a URL
165
+ // source (the command rejects the combination), so it is hidden here rather
166
+ // than advertised as usable surface.
167
+ 'media:import': conditionalPolicy({
168
+ deniedFlags: [
169
+ 'replace-aspects'
170
+ ],
171
+ validate: ({ args })=>typeof args.source === 'string' && isRemoteAssetSource(args.source)
172
+ }),
153
173
  // Creates migration source files in the local project.
154
174
  'migrations:create': deny,
155
175
  // Reads and loads migration definitions from the local project.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/mcpPolicy.ts"],"sourcesContent":["import {\n allow,\n type CommandPolicySet,\n conditionalDenyFlags,\n conditionalPolicy,\n deny,\n} from './policy.js'\n\nfunction apiValidator({\n args,\n flags,\n}: {\n args: Readonly<Record<string, unknown>>\n flags: Readonly<Record<string, unknown>>\n}): boolean {\n const fields: unknown[] = Array.isArray(flags.field) ? flags.field : []\n\n const fieldReadsFromHost = fields.some((field) => {\n if (typeof field !== 'string') return false\n const separatorIndex = field.indexOf('=')\n return separatorIndex > 0 && field[separatorIndex + 1] === '@'\n })\n if (fieldReadsFromHost) return false\n\n const headers: unknown[] = Array.isArray(flags.header) ? flags.header : []\n\n const headerOverridesAuthentication = headers.some((header) => {\n if (typeof header !== 'string') return false\n const separatorIndex = header.indexOf(':')\n if (separatorIndex <= 0) return false\n\n const name = header.slice(0, separatorIndex).trim().toLowerCase()\n return name === 'authorization' || name === 'cookie'\n })\n\n if (headerOverridesAuthentication) return false\n\n const endpoint = typeof args.endpoint === 'string' ? URL.parse(args.endpoint) : null\n const urlEmbedsCredentials =\n endpoint !== null && (endpoint.username !== '' || endpoint.password !== '')\n\n return !urlEmbedsCredentials\n}\n\n/**\n * MCP programmatic mode disables local project/config discovery (see the CLI\n * execution context). Missing project or dataset values may therefore produce\n * a usage error, but cannot cause local filesystem access. Destructive remote\n * operations are allowed and do not by themselves make a command unsafe.\n *\n * Every manifest command must have exactly one policy here:\n * - allow: every valid invocation is safe\n * - conditional: safety depends on parsed arguments or flags\n * - deny: no invocation is safe\n */\nexport const mcpPolicy: CommandPolicySet = {\n // Special exception, this can be very dangerous but is also super useful\n // to expose. Refuse authentication overrides and host input channels:\n // `--token`, Authorization, and Cookie headers replace the MCP user's authentication,\n // URL-embedded credentials replace it with Basic authentication,\n // `--input` reads the request body from the host's filesystem or stdin, and\n // `-F key=@<file>` / `-F key=@-` field values do the same.\n api: conditionalPolicy({\n deniedFlags: ['input', 'token'],\n validate: apiValidator,\n }),\n\n // Reads a file from the machine running the command and must only run locally.\n 'assets:upload': deny,\n\n 'backups:disable': allow,\n // Writes a downloaded backup to the local filesystem.\n 'backups:download': deny,\n 'backups:enable': allow,\n 'backups:list': allow,\n\n // Requires a local Studio project and writes build output to disk.\n build: deny,\n\n // Reads and rewrites local source code.\n codemod: deny,\n\n // --watch polls a job with no deadline; a stuck job would hang the MCP tool\n // call indefinitely. Non-watch invocations are single remote requests.\n 'context:build': conditionalDenyFlags('watch'),\n 'context:create': allow,\n 'context:delete': allow,\n 'context:get': allow,\n // --file reads the import payload from the local filesystem; text, URL and\n // dataset imports are remote-only.\n 'context:imports:create': conditionalDenyFlags('file'),\n 'context:imports:delete': allow,\n 'context:imports:download': allow,\n 'context:imports:get': allow,\n 'context:imports:list': allow,\n // See context:build — --watch has no deadline and would hang the MCP tool call.\n 'context:jobs:get': conditionalDenyFlags('watch'),\n 'context:list': allow,\n 'context:refresh': allow,\n 'context:update': allow,\n\n 'cors:add': allow,\n 'cors:delete': allow,\n 'cors:list': allow,\n\n 'datasets:alias:create': allow,\n 'datasets:alias:delete': allow,\n 'datasets:alias:link': allow,\n 'datasets:alias:unlink': allow,\n 'datasets:copy': allow,\n 'datasets:create': allow,\n 'datasets:delete': allow,\n 'datasets:embeddings:disable': allow,\n 'datasets:embeddings:enable': allow,\n 'datasets:embeddings:status': allow,\n // Writes dataset contents and assets to the local filesystem.\n 'datasets:export': deny,\n // Reads import data from the local filesystem and may replace documents.\n 'datasets:import': deny,\n 'datasets:list': allow,\n 'datasets:visibility:get': allow,\n 'datasets:visibility:set': allow,\n\n // Inspects local project files and can print authentication secrets.\n debug: deny,\n\n // Reads, builds, and deploys a local Studio project.\n deploy: deny,\n\n // Loads a local Studio project and starts a development server.\n dev: deny,\n\n // Opens a browser on the machine running the MCP server.\n 'docs:browse': deny,\n // --web opens a browser on the machine running the MCP server.\n 'docs:read': conditionalDenyFlags('web'),\n 'docs:search': allow,\n\n // Reads and executes local project configuration for diagnostics.\n doctor: deny,\n\n // Reads document input from disk or launches a local editor.\n 'documents:create': deny,\n 'documents:delete': allow,\n 'documents:get': allow,\n 'documents:query': allow,\n // Loads a local Studio schema to validate documents.\n 'documents:validate': deny,\n\n // Executes arbitrary code in the local Studio context.\n exec: deny,\n\n // Loads a local schema and deploys a GraphQL API.\n 'graphql:deploy': deny,\n 'graphql:list': allow,\n // --api loads GraphQL definitions from the local project; explicit project/dataset flags are safe.\n 'graphql:undeploy': conditionalDenyFlags('api'),\n\n 'hooks:attempt': allow,\n 'hooks:create': allow,\n 'hooks:delete': allow,\n 'hooks:list': allow,\n 'hooks:logs': allow,\n\n // Creates or modifies a local project and may install dependencies.\n init: deny,\n\n // Installs packages into the local project.\n install: deny,\n\n // Opens a browser on the machine running the MCP server.\n learn: deny,\n\n // Performs an authentication flow.\n login: deny,\n\n // Performs an authentication operation and clears local credentials.\n logout: deny,\n\n // Reads local project configuration and opens a browser.\n manage: deny,\n\n // Loads local Studio configuration and writes manifest files.\n 'manifest:extract': deny,\n\n // Reads and writes local MCP client configuration.\n 'mcp:configure': deny,\n\n // Writes an aspect definition to the local filesystem.\n 'media:create-aspect': deny,\n 'media:delete-aspect': allow,\n // Reads a local aspect definition before deploying it.\n 'media:deploy-aspect': deny,\n // Writes media assets to the local filesystem.\n 'media:export': deny,\n // Reads media assets from the local filesystem.\n 'media:import': deny,\n\n // Creates migration source files in the local project.\n 'migrations:create': deny,\n // Reads and loads migration definitions from the local project.\n 'migrations:list': deny,\n // Executes local migration code that may perform arbitrary document mutations.\n 'migrations:run': deny,\n\n // Writes project setup values and scaffolds local applications.\n new: deny,\n\n // --web opens a browser on the machine running the MCP server.\n 'openapi:get': conditionalDenyFlags('web'),\n 'openapi:list': conditionalDenyFlags('web'),\n\n 'organizations:create': allow,\n 'organizations:delete': allow,\n 'organizations:get': allow,\n 'organizations:list': allow,\n 'organizations:update': allow,\n\n // Serves a local production build.\n preview: deny,\n\n 'projects:create': allow,\n 'projects:list': allow,\n // Reads locally stored claim URLs and robot tokens.\n 'projects:unclaimed': deny,\n\n // Loads the local Studio configuration to resolve the datasets containing each schema.\n 'schemas:delete': deny,\n // Loads local schema files before deploying them.\n 'schemas:deploy': deny,\n // Loads local Studio configuration and writes an extracted schema to disk.\n 'schemas:extract': deny,\n // Requires a local project and loads its schema configuration.\n 'schemas:list': deny,\n // Loads and executes a local Studio schema.\n 'schemas:validate': deny,\n\n // Installs skills into local editor configuration directories.\n 'skills:install': deny,\n\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:disable': deny,\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:enable': deny,\n 'telemetry:status': allow,\n\n // Creates authentication credentials.\n 'tokens:create': deny,\n // Deletes authentication credentials.\n 'tokens:delete': deny,\n // Exposes authentication credential metadata.\n 'tokens:list': deny,\n // Replaces authentication credentials and exposes the new secret.\n 'tokens:rotate': deny,\n\n // Reads the local schema and source files and writes generated types to the project.\n 'typegen:generate': deny,\n\n // Loads local CLI and workbench configuration to identify the deployed Studio or application.\n undeploy: deny,\n\n // Grants a user access to a project.\n 'users:invite': deny,\n 'users:list': allow,\n\n // Reads the local project and installed package tree.\n versions: deny,\n}\n"],"names":["allow","conditionalDenyFlags","conditionalPolicy","deny","apiValidator","args","flags","fields","Array","isArray","field","fieldReadsFromHost","some","separatorIndex","indexOf","headers","header","headerOverridesAuthentication","name","slice","trim","toLowerCase","endpoint","URL","parse","urlEmbedsCredentials","username","password","mcpPolicy","api","deniedFlags","validate","build","codemod","debug","deploy","dev","doctor","exec","init","install","learn","login","logout","manage","new","preview","undeploy","versions"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/mcpPolicy.ts"],"sourcesContent":["import {\n allow,\n type CommandPolicySet,\n conditionalDenyFlags,\n conditionalPolicy,\n deny,\n} from '@sanity/cli-core/commandPolicy'\n\nimport {isRemoteAssetSource} from '../../../util/isRemoteAssetSource.js'\n\nfunction apiValidator({\n args,\n flags,\n}: {\n args: Readonly<Record<string, unknown>>\n flags: Readonly<Record<string, unknown>>\n}): boolean {\n const fields: unknown[] = Array.isArray(flags.field) ? flags.field : []\n\n const fieldReadsFromHost = fields.some((field) => {\n if (typeof field !== 'string') return false\n const separatorIndex = field.indexOf('=')\n return separatorIndex > 0 && field[separatorIndex + 1] === '@'\n })\n if (fieldReadsFromHost) return false\n\n const headers: unknown[] = Array.isArray(flags.header) ? flags.header : []\n\n const headerOverridesAuthentication = headers.some((header) => {\n if (typeof header !== 'string') return false\n const separatorIndex = header.indexOf(':')\n if (separatorIndex <= 0) return false\n\n const name = header.slice(0, separatorIndex).trim().toLowerCase()\n return name === 'authorization' || name === 'cookie'\n })\n\n if (headerOverridesAuthentication) return false\n\n const endpoint = typeof args.endpoint === 'string' ? URL.parse(args.endpoint) : null\n const urlEmbedsCredentials =\n endpoint !== null && (endpoint.username !== '' || endpoint.password !== '')\n\n return !urlEmbedsCredentials\n}\n\n/**\n * MCP programmatic mode disables local project/config discovery (see the CLI\n * execution context). Missing project or dataset values may therefore produce\n * a usage error, but cannot cause local filesystem access. Destructive remote\n * operations are allowed and do not by themselves make a command unsafe.\n *\n * Every command this package contributes must have exactly one policy here:\n * - allow: every valid invocation is safe\n * - conditional: safety depends on parsed arguments or flags\n * - deny: no invocation is safe\n *\n * Commands contributed by plugins are not listed here. Each plugin declares\n * policies for its own commands (see `PluginInvocationPolicies` in\n * `@sanity/cli-core/commandPolicy`).\n */\nexport const mcpPolicy: CommandPolicySet = {\n // Special exception, this can be very dangerous but is also super useful\n // to expose. Refuse authentication overrides and host input channels:\n // `--token`, Authorization, and Cookie headers replace the MCP user's authentication,\n // URL-embedded credentials replace it with Basic authentication,\n // `--input` reads the request body from the host's filesystem or stdin, and\n // `-F key=@<file>` / `-F key=@-` field values do the same.\n api: conditionalPolicy({\n deniedFlags: ['input', 'token'],\n validate: apiValidator,\n }),\n\n // --file reads from the machine running the command and must only run locally.\n 'assets:upload': conditionalPolicy({\n deniedFlags: ['content-type', 'file'],\n validate: ({flags}) => typeof flags['from-url'] === 'string' && flags['from-url'].length > 0,\n }),\n\n 'backups:disable': allow,\n // Writes a downloaded backup to the local filesystem.\n 'backups:download': deny,\n 'backups:enable': allow,\n 'backups:list': allow,\n\n // Requires a local Studio project and writes build output to disk.\n build: deny,\n\n // Reads and rewrites local source code.\n codemod: deny,\n\n // --watch polls a job with no deadline; a stuck job would hang the MCP tool\n // call indefinitely. Non-watch invocations are single remote requests.\n 'context:build': conditionalDenyFlags('watch'),\n 'context:create': allow,\n 'context:delete': allow,\n 'context:get': allow,\n // --file reads the import payload from the local filesystem; text, URL and\n // dataset imports are remote-only.\n 'context:imports:create': conditionalDenyFlags('file'),\n 'context:imports:delete': allow,\n 'context:imports:download': allow,\n 'context:imports:get': allow,\n 'context:imports:list': allow,\n // See context:build — --watch has no deadline and would hang the MCP tool call.\n 'context:jobs:get': conditionalDenyFlags('watch'),\n 'context:list': allow,\n 'context:refresh': allow,\n 'context:update': allow,\n\n 'cors:add': allow,\n 'cors:delete': allow,\n 'cors:list': allow,\n\n 'datasets:alias:create': allow,\n 'datasets:alias:delete': allow,\n 'datasets:alias:link': allow,\n 'datasets:alias:unlink': allow,\n 'datasets:copy': allow,\n 'datasets:create': allow,\n 'datasets:delete': allow,\n 'datasets:embeddings:disable': allow,\n 'datasets:embeddings:enable': allow,\n 'datasets:embeddings:status': allow,\n // Writes dataset contents and assets to the local filesystem.\n 'datasets:export': deny,\n // Reads import data from the local filesystem and may replace documents.\n 'datasets:import': deny,\n 'datasets:list': allow,\n 'datasets:visibility:get': allow,\n 'datasets:visibility:set': allow,\n\n // Inspects local project files and can print authentication secrets.\n debug: deny,\n\n // Reads, builds, and deploys a local Studio project.\n deploy: deny,\n\n // Loads a local Studio project and starts a development server.\n dev: deny,\n\n // Opens a browser on the machine running the MCP server.\n 'docs:browse': deny,\n // --web opens a browser on the machine running the MCP server.\n 'docs:read': conditionalDenyFlags('web'),\n 'docs:search': allow,\n\n // Reads and executes local project configuration for diagnostics.\n doctor: deny,\n\n // Reads document input from disk or launches a local editor.\n 'documents:create': deny,\n 'documents:delete': allow,\n 'documents:get': allow,\n 'documents:query': allow,\n // Loads a local Studio schema to validate documents.\n 'documents:validate': deny,\n\n // Executes arbitrary code in the local Studio context.\n exec: deny,\n\n // Loads a local schema and deploys a GraphQL API.\n 'graphql:deploy': deny,\n 'graphql:list': allow,\n // --api loads GraphQL definitions from the local project; explicit project/dataset flags are safe.\n 'graphql:undeploy': conditionalDenyFlags('api'),\n\n 'hooks:attempt': allow,\n 'hooks:create': allow,\n 'hooks:delete': allow,\n 'hooks:list': allow,\n 'hooks:logs': allow,\n\n // Creates or modifies a local project and may install dependencies.\n init: deny,\n\n // Installs packages into the local project.\n install: deny,\n\n // Opens a browser on the machine running the MCP server.\n learn: deny,\n\n // Performs an authentication flow.\n login: deny,\n\n // Performs an authentication operation and clears local credentials.\n logout: deny,\n\n // Reads local project configuration and opens a browser.\n manage: deny,\n\n // Loads local Studio configuration and writes manifest files.\n 'manifest:extract': deny,\n\n // Reads and writes local MCP client configuration.\n 'mcp:configure': deny,\n\n // Writes an aspect definition to the local filesystem.\n 'media:create-aspect': deny,\n 'media:delete-aspect': allow,\n // Reads a local aspect definition before deploying it.\n 'media:deploy-aspect': deny,\n // Writes media assets to the local filesystem.\n 'media:export': deny,\n // A directory or archive source reads media assets from the local\n // filesystem. A URL source does not: Sanity fetches the asset itself, so the\n // only local input is the URL. `--replace-aspects` is meaningless for a URL\n // source (the command rejects the combination), so it is hidden here rather\n // than advertised as usable surface.\n 'media:import': conditionalPolicy({\n deniedFlags: ['replace-aspects'],\n validate: ({args}) => typeof args.source === 'string' && isRemoteAssetSource(args.source),\n }),\n\n // Creates migration source files in the local project.\n 'migrations:create': deny,\n // Reads and loads migration definitions from the local project.\n 'migrations:list': deny,\n // Executes local migration code that may perform arbitrary document mutations.\n 'migrations:run': deny,\n\n // Writes project setup values and scaffolds local applications.\n new: deny,\n\n // --web opens a browser on the machine running the MCP server.\n 'openapi:get': conditionalDenyFlags('web'),\n 'openapi:list': conditionalDenyFlags('web'),\n\n 'organizations:create': allow,\n 'organizations:delete': allow,\n 'organizations:get': allow,\n 'organizations:list': allow,\n 'organizations:update': allow,\n\n // Serves a local production build.\n preview: deny,\n\n 'projects:create': allow,\n 'projects:list': allow,\n // Reads locally stored claim URLs and robot tokens.\n 'projects:unclaimed': deny,\n\n // Loads the local Studio configuration to resolve the datasets containing each schema.\n 'schemas:delete': deny,\n // Loads local schema files before deploying them.\n 'schemas:deploy': deny,\n // Loads local Studio configuration and writes an extracted schema to disk.\n 'schemas:extract': deny,\n // Requires a local project and loads its schema configuration.\n 'schemas:list': deny,\n // Loads and executes a local Studio schema.\n 'schemas:validate': deny,\n\n // Installs skills into local editor configuration directories.\n 'skills:install': deny,\n\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:disable': deny,\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:enable': deny,\n 'telemetry:status': allow,\n\n // Creates authentication credentials.\n 'tokens:create': deny,\n // Deletes authentication credentials.\n 'tokens:delete': deny,\n // Exposes authentication credential metadata.\n 'tokens:list': deny,\n // Replaces authentication credentials and exposes the new secret.\n 'tokens:rotate': deny,\n\n // Reads the local schema and source files and writes generated types to the project.\n 'typegen:generate': deny,\n\n // Loads local CLI and workbench configuration to identify the deployed Studio or application.\n undeploy: deny,\n\n // Grants a user access to a project.\n 'users:invite': deny,\n 'users:list': allow,\n\n // Reads the local project and installed package tree.\n versions: deny,\n}\n"],"names":["allow","conditionalDenyFlags","conditionalPolicy","deny","isRemoteAssetSource","apiValidator","args","flags","fields","Array","isArray","field","fieldReadsFromHost","some","separatorIndex","indexOf","headers","header","headerOverridesAuthentication","name","slice","trim","toLowerCase","endpoint","URL","parse","urlEmbedsCredentials","username","password","mcpPolicy","api","deniedFlags","validate","length","build","codemod","debug","deploy","dev","doctor","exec","init","install","learn","login","logout","manage","source","new","preview","undeploy","versions"],"mappings":"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"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Builds the effective policy table for one invocation source, combining what
3
+ * this CLI declares for its own commands with what each plugin declares for
4
+ * the commands it contributes.
5
+ *
6
+ * The table is deny-by-default in both directions: a command with no entry is
7
+ * denied by the caller (`policySet[id] ?? deny`), and every step here that
8
+ * cannot establish permission simply omits an entry rather than falling back
9
+ * to something permissive.
10
+ */ import { resolve } from 'node:path';
11
+ import { isCommandPolicySet } from '@sanity/cli-core/commandPolicy';
12
+ import { subdebug } from '@sanity/cli-core/debug';
13
+ import { doImport } from '@sanity/cli-core/util';
14
+ import { mcpPolicy } from './mcpPolicy.js';
15
+ const debug = subdebug('commandPolicies');
16
+ /** Policies this CLI declares for its own commands, by invocation source. */ export const commandPolicies = {
17
+ mcp: mcpPolicy
18
+ };
19
+ /**
20
+ * Resolution reads plugin package.json files and imports their policy
21
+ * modules, so it is async and depends on the loaded oclif config. Memoized
22
+ * per config so that work happens once per process rather than per invocation.
23
+ */ const cache = new WeakMap();
24
+ export function resolveCommandPolicies(config, source) {
25
+ let bySource = cache.get(config);
26
+ if (!bySource) {
27
+ bySource = new Map();
28
+ cache.set(config, bySource);
29
+ }
30
+ let resolved = bySource.get(source);
31
+ if (!resolved) {
32
+ resolved = buildCommandPolicies(config, source);
33
+ bySource.set(source, resolved);
34
+ }
35
+ return resolved;
36
+ }
37
+ async function buildCommandPolicies(config, source) {
38
+ const hostName = config.pjson.name;
39
+ const entries = {};
40
+ for (const [id, policy] of Object.entries(commandPolicies[source])){
41
+ entries[id] = {
42
+ ...policy,
43
+ declaredBy: hostName
44
+ };
45
+ }
46
+ for (const plugin of config.plugins.values()){
47
+ if (plugin.name === hostName) continue;
48
+ // Only plugins this CLI declares itself should contribute commands.
49
+ // oclif loads plugins recursively, so a dependency of a plugin also lands here.
50
+ if (plugin.parent) {
51
+ debug('ignoring %s, contributed by %s rather than declared here', plugin.name, plugin.parent.name);
52
+ continue;
53
+ }
54
+ const declared = await loadPluginPolicies(plugin, source);
55
+ if (!declared) continue;
56
+ // Plugin policy can only be set for commands they actually contributed.
57
+ const contributed = new Set(plugin.commands.map((command)=>command.id));
58
+ for (const [id, policy] of Object.entries(declared)){
59
+ if (!contributed.has(id)) {
60
+ debug('%s declared a policy for %s, which it does not contribute', plugin.name, id);
61
+ continue;
62
+ }
63
+ // Ids already claimed keep their existing declaration, mirroring
64
+ // oclif's own first-one-wins command resolution.
65
+ if (entries[id]) continue;
66
+ entries[id] = {
67
+ ...policy,
68
+ declaredBy: plugin.name
69
+ };
70
+ }
71
+ }
72
+ return entries;
73
+ }
74
+ /**
75
+ * Load the policy table a plugin declares for one invocation source, or
76
+ * `undefined` when it declares none, points at a module that cannot be
77
+ * loaded, or exports something that is not a policy table.
78
+ *
79
+ * Every failure is silent and non-fatal by design. A plugin's policy module
80
+ * is third-party code resolved at runtime: it must not be able to take down
81
+ * the whole invocation surface by throwing, and it must not be able to reach
82
+ * a permissive outcome by being malformed. Returning `undefined` leaves the
83
+ * plugin's commands uncategorized, which the caller treats as denied.
84
+ */ async function loadPluginPolicies(plugin, source) {
85
+ const declaredPath = readInvocationPoliciesPath(plugin);
86
+ if (!declaredPath) return undefined;
87
+ try {
88
+ const loaded = await doImport(resolve(plugin.root, declaredPath));
89
+ const policies = loaded.invocationPolicies;
90
+ if (typeof policies !== 'object' || policies === null) {
91
+ debug('%s policy module has no usable `invocationPolicies` export', plugin.name);
92
+ return undefined;
93
+ }
94
+ const forSource = policies[source];
95
+ if (forSource === undefined) return undefined;
96
+ if (!isCommandPolicySet(forSource)) {
97
+ debug('%s declared a malformed policy table for %s', plugin.name, source);
98
+ return undefined;
99
+ }
100
+ return forSource;
101
+ } catch (err) {
102
+ debug('failed to load policy module for %s: %s', plugin.name, err);
103
+ return undefined;
104
+ }
105
+ }
106
+ function readInvocationPoliciesPath(plugin) {
107
+ const { sanity } = plugin.pjson;
108
+ const declared = sanity?.invocationPolicies;
109
+ return typeof declared === 'string' && declared !== '' ? declared : undefined;
110
+ }
111
+
112
+ //# sourceMappingURL=resolve.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/resolve.ts"],"sourcesContent":["/**\n * Builds the effective policy table for one invocation source, combining what\n * this CLI declares for its own commands with what each plugin declares for\n * the commands it contributes.\n *\n * The table is deny-by-default in both directions: a command with no entry is\n * denied by the caller (`policySet[id] ?? deny`), and every step here that\n * cannot establish permission simply omits an entry rather than falling back\n * to something permissive.\n */\nimport {resolve} from 'node:path'\n\nimport {type Config, type Interfaces} from '@oclif/core'\nimport {\n type CommandPolicy,\n type CommandPolicySet,\n type InvocationSource,\n isCommandPolicySet,\n} from '@sanity/cli-core/commandPolicy'\nimport {subdebug} from '@sanity/cli-core/debug'\nimport {doImport} from '@sanity/cli-core/util'\n\nimport {mcpPolicy} from './mcpPolicy.js'\n\nconst debug = subdebug('commandPolicies')\n\n/** Policies this CLI declares for its own commands, by invocation source. */\nexport const commandPolicies: Record<InvocationSource, CommandPolicySet> = {\n mcp: mcpPolicy,\n}\n\n/**\n * Policy entries annotated with the package that declared each one.\n */\nexport type ResolvedCommandPolicySet = Readonly<\n Record<string, CommandPolicy & {declaredBy: string}>\n>\n\ntype ResolvedCommandPolicy = ResolvedCommandPolicySet[string]\n\n/**\n * Resolution reads plugin package.json files and imports their policy\n * modules, so it is async and depends on the loaded oclif config. Memoized\n * per config so that work happens once per process rather than per invocation.\n */\nconst cache = new WeakMap<Config, Map<InvocationSource, Promise<ResolvedCommandPolicySet>>>()\n\nexport function resolveCommandPolicies(\n config: Config,\n source: InvocationSource,\n): Promise<ResolvedCommandPolicySet> {\n let bySource = cache.get(config)\n if (!bySource) {\n bySource = new Map()\n cache.set(config, bySource)\n }\n\n let resolved = bySource.get(source)\n if (!resolved) {\n resolved = buildCommandPolicies(config, source)\n bySource.set(source, resolved)\n }\n return resolved\n}\n\nasync function buildCommandPolicies(\n config: Config,\n source: InvocationSource,\n): Promise<ResolvedCommandPolicySet> {\n const hostName = config.pjson.name\n const entries: Record<string, ResolvedCommandPolicy> = {}\n\n for (const [id, policy] of Object.entries(commandPolicies[source])) {\n entries[id] = {...policy, declaredBy: hostName}\n }\n\n for (const plugin of config.plugins.values()) {\n if (plugin.name === hostName) continue\n\n // Only plugins this CLI declares itself should contribute commands.\n // oclif loads plugins recursively, so a dependency of a plugin also lands here.\n if (plugin.parent) {\n debug(\n 'ignoring %s, contributed by %s rather than declared here',\n plugin.name,\n plugin.parent.name,\n )\n continue\n }\n\n const declared = await loadPluginPolicies(plugin, source)\n if (!declared) continue\n\n // Plugin policy can only be set for commands they actually contributed.\n const contributed = new Set(plugin.commands.map((command) => command.id))\n\n for (const [id, policy] of Object.entries(declared)) {\n if (!contributed.has(id)) {\n debug('%s declared a policy for %s, which it does not contribute', plugin.name, id)\n continue\n }\n // Ids already claimed keep their existing declaration, mirroring\n // oclif's own first-one-wins command resolution.\n if (entries[id]) continue\n\n entries[id] = {...policy, declaredBy: plugin.name}\n }\n }\n\n return entries\n}\n\n/**\n * Load the policy table a plugin declares for one invocation source, or\n * `undefined` when it declares none, points at a module that cannot be\n * loaded, or exports something that is not a policy table.\n *\n * Every failure is silent and non-fatal by design. A plugin's policy module\n * is third-party code resolved at runtime: it must not be able to take down\n * the whole invocation surface by throwing, and it must not be able to reach\n * a permissive outcome by being malformed. Returning `undefined` leaves the\n * plugin's commands uncategorized, which the caller treats as denied.\n */\nasync function loadPluginPolicies(\n plugin: Interfaces.Plugin,\n source: InvocationSource,\n): Promise<CommandPolicySet | undefined> {\n const declaredPath = readInvocationPoliciesPath(plugin)\n if (!declaredPath) return undefined\n\n try {\n const loaded: unknown = await doImport(resolve(plugin.root, declaredPath))\n const policies = (loaded as {invocationPolicies?: unknown}).invocationPolicies\n\n if (typeof policies !== 'object' || policies === null) {\n debug('%s policy module has no usable `invocationPolicies` export', plugin.name)\n return undefined\n }\n\n const forSource = (policies as Record<string, unknown>)[source]\n if (forSource === undefined) return undefined\n\n if (!isCommandPolicySet(forSource)) {\n debug('%s declared a malformed policy table for %s', plugin.name, source)\n return undefined\n }\n\n return forSource\n } catch (err) {\n debug('failed to load policy module for %s: %s', plugin.name, err)\n return undefined\n }\n}\n\nfunction readInvocationPoliciesPath(plugin: Interfaces.Plugin): string | undefined {\n const {sanity} = plugin.pjson as {sanity?: {invocationPolicies?: unknown}}\n const declared = sanity?.invocationPolicies\n return typeof declared === 'string' && declared !== '' ? declared : undefined\n}\n"],"names":["resolve","isCommandPolicySet","subdebug","doImport","mcpPolicy","debug","commandPolicies","mcp","cache","WeakMap","resolveCommandPolicies","config","source","bySource","get","Map","set","resolved","buildCommandPolicies","hostName","pjson","name","entries","id","policy","Object","declaredBy","plugin","plugins","values","parent","declared","loadPluginPolicies","contributed","Set","commands","map","command","has","declaredPath","readInvocationPoliciesPath","undefined","loaded","root","policies","invocationPolicies","forSource","err","sanity"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { Help } from '@oclif/core';
2
2
  import { getHelpFlagAdditions } from '@oclif/core/help';
3
+ import { isConditionalInvocationPolicy } from '@sanity/cli-core/commandPolicy';
3
4
  import { resolveTopicAliasInArgv } from '../../topicAliases.js';
4
- import { isConditionalInvocationPolicy } from './commandPolicies/policy.js';
5
5
  /** Command ids a policy exposes: entries that are not denied. */ function visibleCommandIds(policySet) {
6
6
  return new Set(Object.entries(policySet).filter(([, policy])=>policy.kind !== 'deny').map(([id])=>id));
7
7
  }
@@ -74,6 +74,13 @@ import { isConditionalInvocationPolicy } from './commandPolicies/policy.js';
74
74
  this.commandIds = visibleCommandIds(policySet);
75
75
  this.topicNames = visibleTopicNames(this.commandIds);
76
76
  }
77
+ // Both listings start from oclif's, which already drops `hidden` entries, so
78
+ // help advertises the same commands a CLI user sees, minus the ones the
79
+ // policy denies. Hiding and denying answer different questions — "should this
80
+ // be advertised?" and "may this be invoked?" — and a command that is hidden
81
+ // but allowed stays invocable by id, exactly as it is for a CLI user who
82
+ // knows the name.
83
+ //
77
84
  // The copies below are load-bearing: oclif's formatters rewrite ids/names
78
85
  // in place (`cors:list` → `cors list`). Without copies those writes corrupt
79
86
  // the shared (cached) config, breaking the policy checks on subsequent