@sanity/cli 8.22.0 → 8.23.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 (89) hide show
  1. package/dist/actions/context/watchJob.js +9 -3
  2. package/dist/actions/context/watchJob.js.map +1 -1
  3. package/dist/actions/deploy/blueprintManaged.js +59 -0
  4. package/dist/actions/deploy/blueprintManaged.js.map +1 -0
  5. package/dist/actions/deploy/deployApp.js +117 -1
  6. package/dist/actions/deploy/deployApp.js.map +1 -1
  7. package/dist/actions/deploy/deployRunner.js.map +1 -1
  8. package/dist/actions/deploy/deployStudio.js +6 -31
  9. package/dist/actions/deploy/deployStudio.js.map +1 -1
  10. package/dist/actions/deploy/deployStudioSchemasAndManifestsWorker.js +6 -15
  11. package/dist/actions/deploy/deployStudioSchemasAndManifestsWorker.js.map +1 -1
  12. package/dist/actions/exec/runScript.worker.js +6 -2
  13. package/dist/actions/exec/runScript.worker.js.map +1 -1
  14. package/dist/actions/init/bootstrapLocalTemplate.js +35 -11
  15. package/dist/actions/init/bootstrapLocalTemplate.js.map +1 -1
  16. package/dist/actions/init/createMediaLibraryCliConfig.js +12 -0
  17. package/dist/actions/init/createMediaLibraryCliConfig.js.map +1 -0
  18. package/dist/actions/init/determineMediaLibraryTemplate.js +14 -0
  19. package/dist/actions/init/determineMediaLibraryTemplate.js.map +1 -0
  20. package/dist/actions/init/initAction.js +35 -6
  21. package/dist/actions/init/initAction.js.map +1 -1
  22. package/dist/actions/init/initApp.js +81 -6
  23. package/dist/actions/init/initApp.js.map +1 -1
  24. package/dist/actions/init/offerAspectPull.js +81 -0
  25. package/dist/actions/init/offerAspectPull.js.map +1 -0
  26. package/dist/actions/init/project/getProjectDetails.js +25 -2
  27. package/dist/actions/init/project/getProjectDetails.js.map +1 -1
  28. package/dist/actions/init/templates/index.js +3 -0
  29. package/dist/actions/init/templates/index.js.map +1 -1
  30. package/dist/actions/init/templates/mediaLibraryConfig.js +51 -0
  31. package/dist/actions/init/templates/mediaLibraryConfig.js.map +1 -0
  32. package/dist/actions/media/aspectName.js +8 -0
  33. package/dist/actions/media/aspectName.js.map +1 -0
  34. package/dist/actions/media/aspectSync/contract.js +163 -0
  35. package/dist/actions/media/aspectSync/contract.js.map +1 -0
  36. package/dist/actions/media/aspectSync/syncAspects.js +115 -0
  37. package/dist/actions/media/aspectSync/syncAspects.js.map +1 -0
  38. package/dist/actions/media/cliConfigFields.js +192 -0
  39. package/dist/actions/media/cliConfigFields.js.map +1 -0
  40. package/dist/actions/media/pullAspects.js +225 -0
  41. package/dist/actions/media/pullAspects.js.map +1 -0
  42. package/dist/actions/media/resolveMediaLibraryId.js +83 -0
  43. package/dist/actions/media/resolveMediaLibraryId.js.map +1 -0
  44. package/dist/actions/schema/deploySchemas.js.map +1 -1
  45. package/dist/actions/schema/extractSchemaWatcher.js +69 -14
  46. package/dist/actions/schema/extractSchemaWatcher.js.map +1 -1
  47. package/dist/actions/schema/updateWorkspaceSchema.js.map +1 -1
  48. package/dist/commands/context/build.js +2 -2
  49. package/dist/commands/context/build.js.map +1 -1
  50. package/dist/commands/context/entries/get.js +83 -0
  51. package/dist/commands/context/entries/get.js.map +1 -0
  52. package/dist/commands/context/entries/list.js +82 -0
  53. package/dist/commands/context/entries/list.js.map +1 -0
  54. package/dist/commands/context/entries/rebuild.js +95 -0
  55. package/dist/commands/context/entries/rebuild.js.map +1 -0
  56. package/dist/commands/context/issues/apply.js +86 -0
  57. package/dist/commands/context/issues/apply.js.map +1 -0
  58. package/dist/commands/context/issues/dismiss.js +52 -0
  59. package/dist/commands/context/issues/dismiss.js.map +1 -0
  60. package/dist/commands/context/issues/get.js +105 -0
  61. package/dist/commands/context/issues/get.js.map +1 -0
  62. package/dist/commands/context/issues/list.js +128 -0
  63. package/dist/commands/context/issues/list.js.map +1 -0
  64. package/dist/commands/context/issues/reopen.js +53 -0
  65. package/dist/commands/context/issues/reopen.js.map +1 -0
  66. package/dist/commands/context/issues/resolve.js +137 -0
  67. package/dist/commands/context/issues/resolve.js.map +1 -0
  68. package/dist/commands/context/jobs/get.js +2 -2
  69. package/dist/commands/context/jobs/get.js.map +1 -1
  70. package/dist/commands/context/query.js +92 -0
  71. package/dist/commands/context/query.js.map +1 -0
  72. package/dist/commands/deploy.js +13 -0
  73. package/dist/commands/deploy.js.map +1 -1
  74. package/dist/commands/media/prune-aspect-data.js +170 -0
  75. package/dist/commands/media/prune-aspect-data.js.map +1 -0
  76. package/dist/commands/media/pull-aspects.js +309 -0
  77. package/dist/commands/media/pull-aspects.js.map +1 -0
  78. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +17 -0
  79. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
  80. package/dist/services/context.js +96 -3
  81. package/dist/services/context.js.map +1 -1
  82. package/dist/services/mediaLibraries.js +175 -8
  83. package/dist/services/mediaLibraries.js.map +1 -1
  84. package/oclif.manifest.json +1050 -361
  85. package/package.json +6 -5
  86. package/templates/media-library-config/README.md +52 -0
  87. package/templates/media-library-config/eslint.config.txt +52 -0
  88. package/templates/media-library-config/src/fields/caption.tsx +29 -0
  89. package/templates/media-library-config/src/fields/notes.tsx +54 -0
@@ -0,0 +1,170 @@
1
+ import { styleText } from 'node:util';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { exitCodes, ProjectRootNotFoundError, SanityCommand, subdebug } from '@sanity/cli-core';
4
+ import { getErrorMessage } from '@sanity/cli-core/errors';
5
+ import { logSymbols, spinner } from '@sanity/cli-core/ux';
6
+ import { resolveWorkbenchConfig } from '@sanity/workbench-cli/deploy';
7
+ import { VALID_ASPECT_NAME } from '../../actions/media/aspectName.js';
8
+ import { resolveMediaLibraryId } from '../../actions/media/resolveMediaLibraryId.js';
9
+ import { getAspectDataCarrierIds, getStoredAspectIds, unsetAspectData } from '../../services/mediaLibraries.js';
10
+ import { pluralize } from '../../util/pluralize.js';
11
+ const pruneAspectDataDebug = subdebug('media:prune-aspect-data');
12
+ const NOT_A_CONFIG_APP = 'This command must be run inside a Media Library config app — a project whose ' + '`sanity.cli.ts` declares `unstable_defineMediaLibrary(...)`. Scaffold one with ' + '`sanity init --template media-library-config`.';
13
+ /** Unset-patch batch size per mutate request. */ const BATCH_SIZE = 100;
14
+ /** How many carrier ids the report lists before truncating. */ const SAMPLE_SIZE = 10;
15
+ /**
16
+ * The deliberate, explicitly-invoked counterpart to the deploy-time aspect
17
+ * sync: deploying a config only mirrors aspect *definitions* — `aspects.<name>`
18
+ * values on assets are never touched and resurface if a matching field comes
19
+ * back. This command removes that data. Irreversible fan-out mutation, so it
20
+ * never asks for confirmation: without `--yes` it only reports what would change.
21
+ */ export class MediaPruneAspectDataCommand extends SanityCommand {
22
+ static args = {
23
+ aspectName: Args.string({
24
+ description: 'Name of the aspect whose data to remove from all assets',
25
+ required: true
26
+ })
27
+ };
28
+ static description = "Irreversibly remove an aspect's data (aspects.<name> values) from every asset of a media library";
29
+ static examples = [
30
+ {
31
+ command: '<%= config.bin %> <%= command.id %> legacyAspect',
32
+ description: 'Report which assets carry aspects.legacyAspect (changes nothing)'
33
+ },
34
+ {
35
+ command: '<%= config.bin %> <%= command.id %> legacyAspect --yes',
36
+ description: 'Remove aspects.legacyAspect from every asset that carries it'
37
+ }
38
+ ];
39
+ static flags = {
40
+ 'media-library-id': Flags.string({
41
+ description: 'The id of the media library to prune (defaults to the single active library of the configured organization)'
42
+ }),
43
+ yes: Flags.boolean({
44
+ char: 'y',
45
+ description: 'Execute the prune. Without this flag the command only reports what would change'
46
+ })
47
+ };
48
+ // Part of the beta config-app flow: the command requires a config app to
49
+ // resolve its organization, so listing it in the general help would only
50
+ // route legacy-flow users into the beta paradigm. The prune itself is
51
+ // paradigm-neutral (it only unsets asset data) — when there is demand from
52
+ // the legacy flow, ungate it from the config app and unhide it.
53
+ static hidden = true;
54
+ async run() {
55
+ const { aspectName } = this.args;
56
+ const { 'media-library-id': mediaLibraryIdFlag, yes = false } = this.flags;
57
+ // The name rule shared with the sync contract; also makes the
58
+ // interpolation into the carrier GROQ filter injection-safe.
59
+ if (!VALID_ASPECT_NAME.test(aspectName)) {
60
+ this.error(`"${aspectName}" is not a valid aspect name (${VALID_ASPECT_NAME})`, {
61
+ exit: exitCodes.USAGE_ERROR
62
+ });
63
+ }
64
+ let cliConfig;
65
+ try {
66
+ cliConfig = await this.getCliConfig();
67
+ } catch (err) {
68
+ if (err instanceof ProjectRootNotFoundError) {
69
+ this.error(NOT_A_CONFIG_APP, {
70
+ exit: exitCodes.RUNTIME_ERROR
71
+ });
72
+ }
73
+ throw err;
74
+ }
75
+ const config = resolveWorkbenchConfig(cliConfig);
76
+ if (!config) {
77
+ this.error(NOT_A_CONFIG_APP, {
78
+ exit: exitCodes.RUNTIME_ERROR
79
+ });
80
+ }
81
+ try {
82
+ // Not `isUnattended()`: that treats `--yes` as unattended, but here
83
+ // `--yes` consents to the prune, not to skipping prompts — the report
84
+ // path literally instructs "Re-run with --yes", and that run must still
85
+ // offer the library picker in a multi-library organization. Only a
86
+ // genuinely non-interactive run (CI, piped stdin) must not prompt.
87
+ const mediaLibraryId = await resolveMediaLibraryId({
88
+ mediaLibraryIdFlag,
89
+ organizationId: config.organizationId,
90
+ unattended: !this.resolveIsInteractive()
91
+ });
92
+ const spin = spinner('Finding assets that carry the aspect').start();
93
+ let storedIds;
94
+ let carriers;
95
+ try {
96
+ storedIds = await getStoredAspectIds(mediaLibraryId, 'prune-aspect-data');
97
+ carriers = await getAspectDataCarrierIds(mediaLibraryId, aspectName);
98
+ } finally{
99
+ // Also on failure — a spinner left running garbles the error output.
100
+ spin.stop();
101
+ }
102
+ // Guardrails, not gates: pruning while either is true can be deliberate
103
+ // (e.g. clearing data before re-shaping a field), so warn and continue.
104
+ if (config.fields.some((field)=>field.name === aspectName)) {
105
+ this.warn(`"${aspectName}" is still declared in sanity.cli.ts — its data is likely in active use and will grow back.`);
106
+ }
107
+ if (storedIds.includes(aspectName)) {
108
+ this.warn(`Aspect definition "${aspectName}" still exists in the library — this only removes data, not the definition.`);
109
+ }
110
+ if (carriers.length === 0) {
111
+ this.log(`No assets in media library ${styleText('bold', mediaLibraryId)} carry ${styleText('bold', `aspects.${aspectName}`)} — nothing to prune.`);
112
+ return;
113
+ }
114
+ const sample = carriers.slice(0, SAMPLE_SIZE);
115
+ // "documents", not "assets": under the raw perspective drafts and
116
+ // release versions are separate ids, so an asset count would overstate.
117
+ this.log(`${styleText('bold', String(carriers.length))} ${pluralize('document', carriers.length)} in media library ${styleText('bold', mediaLibraryId)} ${carriers.length === 1 ? 'carries' : 'carry'} ${styleText('bold', `aspects.${aspectName}`)}:`);
118
+ for (const id of sample){
119
+ this.log(` - ${id}`);
120
+ }
121
+ if (carriers.length > sample.length) {
122
+ this.log(styleText('dim', ` … and ${carriers.length - sample.length} more`));
123
+ }
124
+ if (!yes) {
125
+ this.log('');
126
+ this.log(`This would ${styleText('bold', 'irreversibly')} unset aspects.${aspectName} on all of them. Re-run with ${styleText('bold', '--yes')} to execute.`);
127
+ return;
128
+ }
129
+ let pruned = 0;
130
+ for(let offset = 0; offset < carriers.length; offset += BATCH_SIZE){
131
+ const batch = carriers.slice(offset, offset + BATCH_SIZE);
132
+ try {
133
+ await unsetAspectData({
134
+ aspectName,
135
+ assetIds: batch,
136
+ mediaLibraryId
137
+ });
138
+ } catch (batchError) {
139
+ // The progress made so far must survive into the error: re-running
140
+ // is safe (pruned assets no longer match the carrier query), so the
141
+ // message says how far it got and that a retry finishes the rest.
142
+ throw new Error(`Removed aspects.${aspectName} from ${pruned} of ${carriers.length} ${pluralize('document', carriers.length)} before a batch failed: ${getErrorMessage(batchError)}\n` + `Re-running the command with --yes is safe — already-pruned documents no longer ` + `match and only the remaining ${carriers.length - pruned} will be mutated.`, {
143
+ cause: batchError
144
+ });
145
+ }
146
+ pruned += batch.length;
147
+ this.log(styleText('dim', ` pruned ${pruned}/${carriers.length}`));
148
+ }
149
+ this.log('');
150
+ this.log(`${logSymbols.success} aspects.${aspectName} removed from ${pruned} ${pluralize('document', pruned)}.`);
151
+ } catch (error) {
152
+ const err = error;
153
+ // Ctrl+C in the library prompt and errors that already carry their exit
154
+ // story (`this.error`, CLIError from the resolver) pass through to
155
+ // `SanityCommand.catch` instead of being re-wrapped here.
156
+ if (err.name === 'ExitPromptError' || 'oclif' in err) {
157
+ throw err;
158
+ }
159
+ pruneAspectDataDebug('Failed to prune aspect data', {
160
+ aspectName,
161
+ error: err
162
+ });
163
+ this.error(styleText('bold', 'Failed to prune aspect data') + `\n\n${styleText('red', err.message)}`, {
164
+ exit: exitCodes.RUNTIME_ERROR
165
+ });
166
+ }
167
+ }
168
+ }
169
+
170
+ //# sourceMappingURL=prune-aspect-data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/commands/media/prune-aspect-data.ts"],"sourcesContent":["import {styleText} from 'node:util'\n\nimport {Args, Flags} from '@oclif/core'\nimport {\n type CliConfig,\n exitCodes,\n ProjectRootNotFoundError,\n SanityCommand,\n subdebug,\n} from '@sanity/cli-core'\nimport {getErrorMessage} from '@sanity/cli-core/errors'\nimport {logSymbols, spinner} from '@sanity/cli-core/ux'\nimport {resolveWorkbenchConfig} from '@sanity/workbench-cli/deploy'\n\nimport {VALID_ASPECT_NAME} from '../../actions/media/aspectName.js'\nimport {resolveMediaLibraryId} from '../../actions/media/resolveMediaLibraryId.js'\nimport {\n getAspectDataCarrierIds,\n getStoredAspectIds,\n unsetAspectData,\n} from '../../services/mediaLibraries.js'\nimport {pluralize} from '../../util/pluralize.js'\n\nconst pruneAspectDataDebug = subdebug('media:prune-aspect-data')\n\nconst NOT_A_CONFIG_APP =\n 'This command must be run inside a Media Library config app — a project whose ' +\n '`sanity.cli.ts` declares `unstable_defineMediaLibrary(...)`. Scaffold one with ' +\n '`sanity init --template media-library-config`.'\n\n/** Unset-patch batch size per mutate request. */\nconst BATCH_SIZE = 100\n\n/** How many carrier ids the report lists before truncating. */\nconst SAMPLE_SIZE = 10\n\n/**\n * The deliberate, explicitly-invoked counterpart to the deploy-time aspect\n * sync: deploying a config only mirrors aspect *definitions* — `aspects.<name>`\n * values on assets are never touched and resurface if a matching field comes\n * back. This command removes that data. Irreversible fan-out mutation, so it\n * never asks for confirmation: without `--yes` it only reports what would change.\n */\nexport class MediaPruneAspectDataCommand extends SanityCommand<typeof MediaPruneAspectDataCommand> {\n static override args = {\n aspectName: Args.string({\n description: 'Name of the aspect whose data to remove from all assets',\n required: true,\n }),\n }\n\n static override description =\n \"Irreversibly remove an aspect's data (aspects.<name> values) from every asset of a media library\"\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %> legacyAspect',\n description: 'Report which assets carry aspects.legacyAspect (changes nothing)',\n },\n {\n command: '<%= config.bin %> <%= command.id %> legacyAspect --yes',\n description: 'Remove aspects.legacyAspect from every asset that carries it',\n },\n ]\n\n static override flags = {\n 'media-library-id': Flags.string({\n description:\n 'The id of the media library to prune (defaults to the single active library of the configured organization)',\n }),\n yes: Flags.boolean({\n char: 'y',\n description:\n 'Execute the prune. Without this flag the command only reports what would change',\n }),\n }\n\n // Part of the beta config-app flow: the command requires a config app to\n // resolve its organization, so listing it in the general help would only\n // route legacy-flow users into the beta paradigm. The prune itself is\n // paradigm-neutral (it only unsets asset data) — when there is demand from\n // the legacy flow, ungate it from the config app and unhide it.\n static override hidden = true\n\n public async run(): Promise<void> {\n const {aspectName} = this.args\n const {'media-library-id': mediaLibraryIdFlag, yes = false} = this.flags\n\n // The name rule shared with the sync contract; also makes the\n // interpolation into the carrier GROQ filter injection-safe.\n if (!VALID_ASPECT_NAME.test(aspectName)) {\n this.error(`\"${aspectName}\" is not a valid aspect name (${VALID_ASPECT_NAME})`, {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n\n let cliConfig: CliConfig\n try {\n cliConfig = await this.getCliConfig()\n } catch (err) {\n if (err instanceof ProjectRootNotFoundError) {\n this.error(NOT_A_CONFIG_APP, {exit: exitCodes.RUNTIME_ERROR})\n }\n throw err\n }\n\n const config = resolveWorkbenchConfig(cliConfig)\n if (!config) {\n this.error(NOT_A_CONFIG_APP, {exit: exitCodes.RUNTIME_ERROR})\n }\n\n try {\n // Not `isUnattended()`: that treats `--yes` as unattended, but here\n // `--yes` consents to the prune, not to skipping prompts — the report\n // path literally instructs \"Re-run with --yes\", and that run must still\n // offer the library picker in a multi-library organization. Only a\n // genuinely non-interactive run (CI, piped stdin) must not prompt.\n const mediaLibraryId = await resolveMediaLibraryId({\n mediaLibraryIdFlag,\n organizationId: config.organizationId,\n unattended: !this.resolveIsInteractive(),\n })\n\n const spin = spinner('Finding assets that carry the aspect').start()\n let storedIds: string[]\n let carriers: string[]\n try {\n storedIds = await getStoredAspectIds(mediaLibraryId, 'prune-aspect-data')\n carriers = await getAspectDataCarrierIds(mediaLibraryId, aspectName)\n } finally {\n // Also on failure — a spinner left running garbles the error output.\n spin.stop()\n }\n\n // Guardrails, not gates: pruning while either is true can be deliberate\n // (e.g. clearing data before re-shaping a field), so warn and continue.\n if (config.fields.some((field) => field.name === aspectName)) {\n this.warn(\n `\"${aspectName}\" is still declared in sanity.cli.ts — its data is likely in active use and will grow back.`,\n )\n }\n if (storedIds.includes(aspectName)) {\n this.warn(\n `Aspect definition \"${aspectName}\" still exists in the library — this only removes data, not the definition.`,\n )\n }\n\n if (carriers.length === 0) {\n this.log(\n `No assets in media library ${styleText('bold', mediaLibraryId)} carry ${styleText(\n 'bold',\n `aspects.${aspectName}`,\n )} — nothing to prune.`,\n )\n return\n }\n\n const sample = carriers.slice(0, SAMPLE_SIZE)\n // \"documents\", not \"assets\": under the raw perspective drafts and\n // release versions are separate ids, so an asset count would overstate.\n this.log(\n `${styleText('bold', String(carriers.length))} ${pluralize(\n 'document',\n carriers.length,\n )} in media library ${styleText('bold', mediaLibraryId)} ${\n carriers.length === 1 ? 'carries' : 'carry'\n } ${styleText('bold', `aspects.${aspectName}`)}:`,\n )\n for (const id of sample) {\n this.log(` - ${id}`)\n }\n if (carriers.length > sample.length) {\n this.log(styleText('dim', ` … and ${carriers.length - sample.length} more`))\n }\n\n if (!yes) {\n this.log('')\n this.log(\n `This would ${styleText(\n 'bold',\n 'irreversibly',\n )} unset aspects.${aspectName} on all of them. Re-run with ${styleText(\n 'bold',\n '--yes',\n )} to execute.`,\n )\n return\n }\n\n let pruned = 0\n for (let offset = 0; offset < carriers.length; offset += BATCH_SIZE) {\n const batch = carriers.slice(offset, offset + BATCH_SIZE)\n try {\n await unsetAspectData({aspectName, assetIds: batch, mediaLibraryId})\n } catch (batchError) {\n // The progress made so far must survive into the error: re-running\n // is safe (pruned assets no longer match the carrier query), so the\n // message says how far it got and that a retry finishes the rest.\n throw new Error(\n `Removed aspects.${aspectName} from ${pruned} of ${carriers.length} ${pluralize(\n 'document',\n carriers.length,\n )} before a batch failed: ${getErrorMessage(batchError)}\\n` +\n `Re-running the command with --yes is safe — already-pruned documents no longer ` +\n `match and only the remaining ${carriers.length - pruned} will be mutated.`,\n {cause: batchError},\n )\n }\n pruned += batch.length\n this.log(styleText('dim', ` pruned ${pruned}/${carriers.length}`))\n }\n\n this.log('')\n this.log(\n `${logSymbols.success} aspects.${aspectName} removed from ${pruned} ${pluralize(\n 'document',\n pruned,\n )}.`,\n )\n } catch (error) {\n const err = error as Error\n // Ctrl+C in the library prompt and errors that already carry their exit\n // story (`this.error`, CLIError from the resolver) pass through to\n // `SanityCommand.catch` instead of being re-wrapped here.\n if (err.name === 'ExitPromptError' || 'oclif' in err) {\n throw err\n }\n pruneAspectDataDebug('Failed to prune aspect data', {\n aspectName,\n error: err,\n })\n this.error(\n styleText('bold', 'Failed to prune aspect data') + `\\n\\n${styleText('red', err.message)}`,\n {exit: exitCodes.RUNTIME_ERROR},\n )\n }\n }\n}\n"],"names":["styleText","Args","Flags","exitCodes","ProjectRootNotFoundError","SanityCommand","subdebug","getErrorMessage","logSymbols","spinner","resolveWorkbenchConfig","VALID_ASPECT_NAME","resolveMediaLibraryId","getAspectDataCarrierIds","getStoredAspectIds","unsetAspectData","pluralize","pruneAspectDataDebug","NOT_A_CONFIG_APP","BATCH_SIZE","SAMPLE_SIZE","MediaPruneAspectDataCommand","args","aspectName","string","description","required","examples","command","flags","yes","boolean","char","hidden","run","mediaLibraryIdFlag","test","error","exit","USAGE_ERROR","cliConfig","getCliConfig","err","RUNTIME_ERROR","config","mediaLibraryId","organizationId","unattended","resolveIsInteractive","spin","start","storedIds","carriers","stop","fields","some","field","name","warn","includes","length","log","sample","slice","String","id","pruned","offset","batch","assetIds","batchError","Error","cause","success","message"],"mappings":"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"}
@@ -0,0 +1,309 @@
1
+ import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { styleText } from 'node:util';
4
+ import { Flags } from '@oclif/core';
5
+ import { exitCodes, ProjectRootNotFoundError, SanityCommand, subdebug } from '@sanity/cli-core';
6
+ import { confirm, logSymbols, spinner } from '@sanity/cli-core/ux';
7
+ import { resolveWorkbenchConfig } from '@sanity/workbench-cli/deploy';
8
+ import { appendFieldEntries } from '../../actions/media/cliConfigFields.js';
9
+ import { convertAspectDocuments, formatFieldEntries } from '../../actions/media/pullAspects.js';
10
+ import { resolveMediaLibraryId } from '../../actions/media/resolveMediaLibraryId.js';
11
+ import { getAspects } from '../../services/mediaLibraries.js';
12
+ import { pluralize } from '../../util/pluralize.js';
13
+ import { defineCommandTelemetry } from '../../util/telemetry/commandTelemetry.js';
14
+ const pullAspectsDebug = subdebug('media:pull-aspects');
15
+ const NOT_A_CONFIG_APP = 'This command must be run inside a Media Library config app — a project whose ' + '`sanity.cli.ts` declares `unstable_defineMediaLibrary(...)`. Scaffold one with ' + '`sanity init --template media-library-config`.';
16
+ const flags = {
17
+ 'media-library-id': Flags.string({
18
+ description: 'The id of the media library to pull from (defaults to the single active library of the configured organization)'
19
+ }),
20
+ 'out-dir': Flags.string({
21
+ default: 'src/fields',
22
+ description: 'Directory to write the generated field modules to, relative to the project root'
23
+ }),
24
+ overwrite: Flags.boolean({
25
+ description: 'Overwrite existing field modules and re-generate fields already declared in the config'
26
+ }),
27
+ 'update-config': Flags.boolean({
28
+ allowNo: true,
29
+ description: 'Declare the generated fields in sanity.cli.ts (--no-update-config to skip). Prompted when omitted; unattended runs only print the entries'
30
+ })
31
+ };
32
+ export class MediaPullAspectsCommand extends SanityCommand {
33
+ static description = "Generate field modules from a media library's stored aspect documents";
34
+ static examples = [
35
+ {
36
+ command: '<%= config.bin %> <%= command.id %>',
37
+ description: 'Generate src/fields/<name>.tsx modules from the stored aspects'
38
+ },
39
+ {
40
+ command: '<%= config.bin %> <%= command.id %> --media-library-id ml123 --overwrite',
41
+ description: 'Pull from a specific media library, overwriting existing modules'
42
+ }
43
+ ];
44
+ static flags = flags;
45
+ // Part of the beta config-app flow: only usable inside a config app (like
46
+ // the unlisted media-library-config init template, which points to it), and
47
+ // listing it next to the legacy aspect commands would mix the two
48
+ // paradigms. Unhide when the config app leaves beta.
49
+ static hidden = true;
50
+ // --out-dir is a local filesystem path.
51
+ static telemetry = defineCommandTelemetry(flags, {
52
+ redact: [
53
+ 'out-dir'
54
+ ]
55
+ });
56
+ async run() {
57
+ const { 'media-library-id': mediaLibraryIdFlag, 'out-dir': outDir, overwrite = false } = this.flags;
58
+ let cliConfig;
59
+ try {
60
+ cliConfig = await this.getCliConfig();
61
+ } catch (err) {
62
+ if (err instanceof ProjectRootNotFoundError) {
63
+ this.error(NOT_A_CONFIG_APP, {
64
+ exit: exitCodes.RUNTIME_ERROR
65
+ });
66
+ }
67
+ throw err;
68
+ }
69
+ const config = resolveWorkbenchConfig(cliConfig);
70
+ if (!config) {
71
+ this.error(NOT_A_CONFIG_APP, {
72
+ exit: exitCodes.RUNTIME_ERROR
73
+ });
74
+ }
75
+ const root = await this.getProjectRoot();
76
+ // The generated `src:` entries and the ownership story both assume the
77
+ // modules live inside the project; a path escaping the root is a mistake.
78
+ // `outDirRelative` (not the raw flag) feeds the report so an absolute
79
+ // --out-dir inside the root still prints proper relative `src:` entries.
80
+ const outDirAbsolute = path.resolve(root.directory, outDir);
81
+ const outDirRelative = path.relative(root.directory, outDirAbsolute);
82
+ if (path.isAbsolute(outDirRelative) || outDirRelative === '..' || outDirRelative.startsWith(`..${path.sep}`)) {
83
+ this.error(`--out-dir must stay inside the project root (got "${outDir}")`, {
84
+ exit: exitCodes.USAGE_ERROR
85
+ });
86
+ }
87
+ try {
88
+ const mediaLibraryId = await resolveMediaLibraryId({
89
+ mediaLibraryIdFlag,
90
+ organizationId: config.organizationId,
91
+ unattended: this.isUnattended()
92
+ });
93
+ const spin = spinner('Fetching stored aspect documents').start();
94
+ let documents;
95
+ try {
96
+ documents = await getAspects(mediaLibraryId);
97
+ } finally{
98
+ // Also on failure — a spinner left running garbles the error output.
99
+ spin.stop();
100
+ }
101
+ if (documents.length === 0) {
102
+ this.log(`Media library ${styleText('bold', mediaLibraryId)} has no stored aspects — nothing to pull.`);
103
+ return;
104
+ }
105
+ // Names already declared in the config are owned by existing field
106
+ // modules; pulling the stored mirror back over them would regress local
107
+ // edits, so they are skipped unless --overwrite says otherwise.
108
+ const configuredNames = new Set(config.fields.map((field)=>field.name));
109
+ const configuredSrcByName = new Map(config.fields.map((field)=>[
110
+ field.name,
111
+ field.src
112
+ ]));
113
+ // One module per field name; name collisions and invalid documents are skipped.
114
+ const { modules: winners, skipped } = convertAspectDocuments(documents);
115
+ const generated = [];
116
+ // Module name -> written path relative to the project root, so the
117
+ // report shows where each module actually landed.
118
+ const writtenPaths = new Map();
119
+ // Module name -> path of a module skipped because it already exists on
120
+ // disk. Those fields may still be undeclared and need registration.
121
+ const existingPaths = new Map();
122
+ for (const module of winners){
123
+ if (!overwrite && configuredNames.has(module.name)) {
124
+ skipped.push({
125
+ id: module.name,
126
+ reasons: [
127
+ 'Already declared in sanity.cli.ts (pass --overwrite to re-generate)'
128
+ ],
129
+ status: 'skipped',
130
+ validationErrors: []
131
+ });
132
+ continue;
133
+ }
134
+ const defaultDestination = path.join(outDirAbsolute, module.filename);
135
+ // The pull generates `.tsx`, but a hand-written module for the same
136
+ // field may be plain `.ts` — treat both as taken.
137
+ const existing = await firstExisting([
138
+ defaultDestination,
139
+ defaultDestination.replace(/\.tsx$/, '.ts')
140
+ ]);
141
+ if (!overwrite && existing) {
142
+ // The file is left alone, but the field is undeclared (declared
143
+ // names were skipped above) — so it still joins the registration
144
+ // offer below. An on-disk module without a config entry is exactly
145
+ // the state the next deploy punishes, however the file got there.
146
+ existingPaths.set(module.name, path.relative(root.directory, existing));
147
+ skipped.push({
148
+ id: module.name,
149
+ reasons: [
150
+ `A file already exists at ${path.relative(root.directory, existing)} (pass --overwrite to replace it)`
151
+ ],
152
+ status: 'skipped',
153
+ validationErrors: []
154
+ });
155
+ continue;
156
+ }
157
+ // When overwriting a field the config already declares, the module
158
+ // must land at the declared `src` path — writing <out-dir>/<name>.tsx
159
+ // instead would orphan the new file while deploy keeps evaluating the
160
+ // old one. Failing that, replace the module found on disk (which may
161
+ // be the .ts variant), and only then fall back to the default.
162
+ const declaredSrc = overwrite ? configuredSrcByName.get(module.name) : undefined;
163
+ const destination = declaredSrc ? await resolveDeclaredSrc(path.resolve(root.directory, declaredSrc), module.filename) : existing ?? defaultDestination;
164
+ await mkdir(path.dirname(destination), {
165
+ recursive: true
166
+ });
167
+ await writeFile(destination, module.source);
168
+ writtenPaths.set(module.name, path.relative(root.directory, destination));
169
+ generated.push(module);
170
+ }
171
+ // Fields present locally — written this run or already on disk — but not
172
+ // declared get offered for registration: the deploy mirrors config to
173
+ // stored documents, so an undeclared stored aspect is deleted by the
174
+ // next deploy — leaving the entries unpasted is a data hazard, not just
175
+ // friction. Existing files count because a prior pull may have been
176
+ // declined (or edited by hand): the file being there does not make the
177
+ // field declared.
178
+ const entryPaths = new Map([
179
+ ...writtenPaths,
180
+ ...existingPaths
181
+ ]);
182
+ const newEntries = winners.filter((module)=>!configuredNames.has(module.name) && entryPaths.has(module.name));
183
+ const entryLines = newEntries.length > 0 ? formatFieldEntries(newEntries, outDirRelative, entryPaths).split('\n') : [];
184
+ const registration = newEntries.length > 0 ? await this.maybeRegisterFields({
185
+ configPath: root.path,
186
+ entryLines
187
+ }) : null;
188
+ this.report({
189
+ configuredNames,
190
+ entryLines,
191
+ generated,
192
+ newEntries,
193
+ outDir: outDirRelative,
194
+ registration,
195
+ skipped,
196
+ writtenPaths
197
+ });
198
+ } catch (error) {
199
+ // Ctrl+C on the library prompt and errors that already carry their exit
200
+ // code (`this.error` inside helpers) pass through to SanityCommand.catch.
201
+ if (error instanceof Error && error.name === 'ExitPromptError') throw error;
202
+ if (error && typeof error === 'object' && 'oclif' in error) throw error;
203
+ const err = error;
204
+ pullAspectsDebug('Failed to pull aspects', {
205
+ error: err,
206
+ mediaLibraryId: mediaLibraryIdFlag
207
+ });
208
+ this.error(styleText('bold', 'Failed to pull aspects') + `\n\n${styleText('red', err.message)}`, {
209
+ exit: exitCodes.RUNTIME_ERROR
210
+ });
211
+ }
212
+ }
213
+ /**
214
+ * Offer to declare the generated fields in `sanity.cli.ts`. `--update-config`
215
+ * / `--no-update-config` decide outright; otherwise an interactive run
216
+ * prompts (default yes) and an unattended run leaves the config alone.
217
+ */ async maybeRegisterFields({ configPath, entryLines }) {
218
+ const flag = this.flags['update-config'];
219
+ const shouldUpdate = flag ?? (this.isUnattended() ? false : await confirm({
220
+ default: true,
221
+ message: `Declare the ${entryLines.length} pulled ${pluralize('field', entryLines.length)} in sanity.cli.ts?`
222
+ }));
223
+ if (!shouldUpdate) return 'declined';
224
+ const source = await readFile(configPath, 'utf8');
225
+ const result = appendFieldEntries(source, entryLines);
226
+ if (!result.ok) return {
227
+ reason: result.reason
228
+ };
229
+ await writeFile(configPath, result.updated);
230
+ return 'added';
231
+ }
232
+ report({ configuredNames, entryLines, generated, newEntries, outDir, registration, skipped, writtenPaths }) {
233
+ if (skipped.length > 0) {
234
+ this.logToStderr('');
235
+ this.warn(styleText('bold', `Skipped ${skipped.length} ${pluralize('aspect', skipped.length)}`));
236
+ for (const aspect of skipped){
237
+ const errors = aspect.validationErrors.flatMap((group)=>group.map(({ message })=>message));
238
+ const reasons = [
239
+ ...aspect.reasons,
240
+ ...errors
241
+ ];
242
+ this.logToStderr(` - ${aspect.id} ${styleText('dim', reasons.join('; '))}`);
243
+ }
244
+ }
245
+ // No modules written and nothing to declare: done. Skipped-but-undeclared
246
+ // fields keep the report alive — their registration outcome still matters.
247
+ if (generated.length === 0 && newEntries.length === 0) {
248
+ this.log('');
249
+ this.log('No field modules generated.');
250
+ return;
251
+ }
252
+ if (generated.length === 0) {
253
+ this.log('');
254
+ this.log('No field modules generated.');
255
+ } else {
256
+ this.log('');
257
+ this.log(`${logSymbols.success} ${styleText('bold', `Generated ${generated.length} field ${pluralize('module', generated.length)}`)}`);
258
+ for (const module of generated){
259
+ const location = writtenPaths.get(module.name) ?? path.join(outDir, module.filename);
260
+ this.log(` - ${module.name} ${styleText('dim', location)}`);
261
+ for (const warning of module.warnings){
262
+ this.log(` ${logSymbols.warning} ${warning}`);
263
+ }
264
+ }
265
+ }
266
+ if (newEntries.length > 0 && registration === 'added') {
267
+ this.log('');
268
+ this.log(`${logSymbols.success} Declared ${newEntries.length} ${pluralize('field', newEntries.length)} in ${styleText('bold', 'sanity.cli.ts')}`);
269
+ } else if (newEntries.length > 0) {
270
+ if (registration !== null && typeof registration === 'object') {
271
+ this.warn(`Could not update sanity.cli.ts automatically: ${registration.reason}`);
272
+ }
273
+ this.log('');
274
+ this.log(configuredNames.size > 0 ? `Add the pulled ${pluralize('field', newEntries.length)} to the \`fields\` array in ${styleText('bold', 'sanity.cli.ts')}:` : `Declare the pulled ${pluralize('field', newEntries.length)} in ${styleText('bold', 'sanity.cli.ts')}:`);
275
+ this.log('');
276
+ this.log(entryLines.map((line)=>` ${line}`).join('\n'));
277
+ // The deploy mirrors config into stored documents; a pulled-but-undeclared
278
+ // field reads as stale there, so leaving the entries out is destructive.
279
+ this.warn('`sanity deploy` deletes the stored definition of any field not declared in sanity.cli.ts.');
280
+ }
281
+ this.log('');
282
+ this.log('Once declared in sanity.cli.ts, the config app owns these definitions — deploying the');
283
+ this.log('config overwrites the stored documents this pull read from. Stored definitions');
284
+ this.log('not declared in sanity.cli.ts are deleted on deploy.');
285
+ }
286
+ }
287
+ /**
288
+ * The write target for a declared `src` path. A declared path may omit the
289
+ * extension — the bundler and jiti both resolve `./src/fields/notes` to
290
+ * `notes.ts(x)` — so writing it literally would create an extensionless file
291
+ * next to the module deploy actually evaluates. Resolve to the existing
292
+ * module file; if none exists, append the generated module's extension.
293
+ */ async function resolveDeclaredSrc(declaredPath, moduleFilename) {
294
+ if (path.extname(declaredPath) !== '') return declaredPath;
295
+ const existing = await firstExisting([
296
+ `${declaredPath}.ts`,
297
+ `${declaredPath}.tsx`
298
+ ]);
299
+ return existing ?? `${declaredPath}${path.extname(moduleFilename)}`;
300
+ }
301
+ /** The first path in `paths` that exists, or `undefined`. */ async function firstExisting(paths) {
302
+ for (const filePath of paths){
303
+ const exists = await access(filePath).then(()=>true, ()=>false);
304
+ if (exists) return filePath;
305
+ }
306
+ return undefined;
307
+ }
308
+
309
+ //# sourceMappingURL=pull-aspects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/commands/media/pull-aspects.ts"],"sourcesContent":["import {access, mkdir, readFile, writeFile} from 'node:fs/promises'\nimport path from 'node:path'\nimport {styleText} from 'node:util'\n\nimport {Flags} from '@oclif/core'\nimport {\n type CliConfig,\n exitCodes,\n ProjectRootNotFoundError,\n SanityCommand,\n subdebug,\n} from '@sanity/cli-core'\nimport {confirm, logSymbols, spinner} from '@sanity/cli-core/ux'\nimport {resolveWorkbenchConfig} from '@sanity/workbench-cli/deploy'\n\nimport {appendFieldEntries} from '../../actions/media/cliConfigFields.js'\nimport {\n convertAspectDocuments,\n formatFieldEntries,\n type GeneratedFieldModule,\n type SkippedAspect,\n} from '../../actions/media/pullAspects.js'\nimport {resolveMediaLibraryId} from '../../actions/media/resolveMediaLibraryId.js'\nimport {getAspects} from '../../services/mediaLibraries.js'\nimport {pluralize} from '../../util/pluralize.js'\nimport {defineCommandTelemetry} from '../../util/telemetry/commandTelemetry.js'\n\nconst pullAspectsDebug = subdebug('media:pull-aspects')\n\nconst NOT_A_CONFIG_APP =\n 'This command must be run inside a Media Library config app — a project whose ' +\n '`sanity.cli.ts` declares `unstable_defineMediaLibrary(...)`. Scaffold one with ' +\n '`sanity init --template media-library-config`.'\n\nconst flags = {\n 'media-library-id': Flags.string({\n description:\n 'The id of the media library to pull from (defaults to the single active library of the configured organization)',\n }),\n 'out-dir': Flags.string({\n default: 'src/fields',\n description: 'Directory to write the generated field modules to, relative to the project root',\n }),\n overwrite: Flags.boolean({\n description:\n 'Overwrite existing field modules and re-generate fields already declared in the config',\n }),\n 'update-config': Flags.boolean({\n allowNo: true,\n description:\n 'Declare the generated fields in sanity.cli.ts (--no-update-config to skip). Prompted when omitted; unattended runs only print the entries',\n }),\n}\n\n/**\n * The outcome of offering to declare the generated fields in `sanity.cli.ts`:\n * written, turned down (flag, prompt, or unattended), or not writable (the\n * config shape could not be edited safely — the `reason` says why).\n */\ntype Registration = 'added' | 'declined' | {reason: string}\n\nexport class MediaPullAspectsCommand extends SanityCommand<typeof MediaPullAspectsCommand> {\n static override description =\n \"Generate field modules from a media library's stored aspect documents\"\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Generate src/fields/<name>.tsx modules from the stored aspects',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --media-library-id ml123 --overwrite',\n description: 'Pull from a specific media library, overwriting existing modules',\n },\n ]\n\n static override flags = flags\n\n // Part of the beta config-app flow: only usable inside a config app (like\n // the unlisted media-library-config init template, which points to it), and\n // listing it next to the legacy aspect commands would mix the two\n // paradigms. Unhide when the config app leaves beta.\n static override hidden = true\n\n // --out-dir is a local filesystem path.\n static telemetry = defineCommandTelemetry(flags, {redact: ['out-dir']})\n\n public async run(): Promise<void> {\n const {\n 'media-library-id': mediaLibraryIdFlag,\n 'out-dir': outDir,\n overwrite = false,\n } = this.flags\n\n let cliConfig: CliConfig\n try {\n cliConfig = await this.getCliConfig()\n } catch (err) {\n if (err instanceof ProjectRootNotFoundError) {\n this.error(NOT_A_CONFIG_APP, {exit: exitCodes.RUNTIME_ERROR})\n }\n throw err\n }\n\n const config = resolveWorkbenchConfig(cliConfig)\n if (!config) {\n this.error(NOT_A_CONFIG_APP, {exit: exitCodes.RUNTIME_ERROR})\n }\n\n const root = await this.getProjectRoot()\n // The generated `src:` entries and the ownership story both assume the\n // modules live inside the project; a path escaping the root is a mistake.\n // `outDirRelative` (not the raw flag) feeds the report so an absolute\n // --out-dir inside the root still prints proper relative `src:` entries.\n const outDirAbsolute = path.resolve(root.directory, outDir)\n const outDirRelative = path.relative(root.directory, outDirAbsolute)\n if (\n path.isAbsolute(outDirRelative) ||\n outDirRelative === '..' ||\n outDirRelative.startsWith(`..${path.sep}`)\n ) {\n this.error(`--out-dir must stay inside the project root (got \"${outDir}\")`, {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n\n try {\n const mediaLibraryId = await resolveMediaLibraryId({\n mediaLibraryIdFlag,\n organizationId: config.organizationId,\n unattended: this.isUnattended(),\n })\n\n const spin = spinner('Fetching stored aspect documents').start()\n let documents: Awaited<ReturnType<typeof getAspects>>\n try {\n documents = await getAspects(mediaLibraryId)\n } finally {\n // Also on failure — a spinner left running garbles the error output.\n spin.stop()\n }\n\n if (documents.length === 0) {\n this.log(\n `Media library ${styleText('bold', mediaLibraryId)} has no stored aspects — nothing to pull.`,\n )\n return\n }\n\n // Names already declared in the config are owned by existing field\n // modules; pulling the stored mirror back over them would regress local\n // edits, so they are skipped unless --overwrite says otherwise.\n const configuredNames = new Set(config.fields.map((field) => field.name))\n const configuredSrcByName = new Map(config.fields.map((field) => [field.name, field.src]))\n\n // One module per field name; name collisions and invalid documents are skipped.\n const {modules: winners, skipped} = convertAspectDocuments(documents)\n\n const generated: GeneratedFieldModule[] = []\n // Module name -> written path relative to the project root, so the\n // report shows where each module actually landed.\n const writtenPaths = new Map<string, string>()\n // Module name -> path of a module skipped because it already exists on\n // disk. Those fields may still be undeclared and need registration.\n const existingPaths = new Map<string, string>()\n for (const module of winners) {\n if (!overwrite && configuredNames.has(module.name)) {\n skipped.push({\n id: module.name,\n reasons: ['Already declared in sanity.cli.ts (pass --overwrite to re-generate)'],\n status: 'skipped',\n validationErrors: [],\n })\n continue\n }\n\n const defaultDestination = path.join(outDirAbsolute, module.filename)\n // The pull generates `.tsx`, but a hand-written module for the same\n // field may be plain `.ts` — treat both as taken.\n const existing = await firstExisting([\n defaultDestination,\n defaultDestination.replace(/\\.tsx$/, '.ts'),\n ])\n if (!overwrite && existing) {\n // The file is left alone, but the field is undeclared (declared\n // names were skipped above) — so it still joins the registration\n // offer below. An on-disk module without a config entry is exactly\n // the state the next deploy punishes, however the file got there.\n existingPaths.set(module.name, path.relative(root.directory, existing))\n skipped.push({\n id: module.name,\n reasons: [\n `A file already exists at ${path.relative(root.directory, existing)} (pass --overwrite to replace it)`,\n ],\n status: 'skipped',\n validationErrors: [],\n })\n continue\n }\n\n // When overwriting a field the config already declares, the module\n // must land at the declared `src` path — writing <out-dir>/<name>.tsx\n // instead would orphan the new file while deploy keeps evaluating the\n // old one. Failing that, replace the module found on disk (which may\n // be the .ts variant), and only then fall back to the default.\n const declaredSrc = overwrite ? configuredSrcByName.get(module.name) : undefined\n const destination = declaredSrc\n ? await resolveDeclaredSrc(path.resolve(root.directory, declaredSrc), module.filename)\n : (existing ?? defaultDestination)\n\n await mkdir(path.dirname(destination), {recursive: true})\n await writeFile(destination, module.source)\n writtenPaths.set(module.name, path.relative(root.directory, destination))\n generated.push(module)\n }\n\n // Fields present locally — written this run or already on disk — but not\n // declared get offered for registration: the deploy mirrors config to\n // stored documents, so an undeclared stored aspect is deleted by the\n // next deploy — leaving the entries unpasted is a data hazard, not just\n // friction. Existing files count because a prior pull may have been\n // declined (or edited by hand): the file being there does not make the\n // field declared.\n const entryPaths = new Map([...writtenPaths, ...existingPaths])\n const newEntries = winners.filter(\n (module) => !configuredNames.has(module.name) && entryPaths.has(module.name),\n )\n const entryLines =\n newEntries.length > 0\n ? formatFieldEntries(newEntries, outDirRelative, entryPaths).split('\\n')\n : []\n const registration =\n newEntries.length > 0\n ? await this.maybeRegisterFields({configPath: root.path, entryLines})\n : null\n\n this.report({\n configuredNames,\n entryLines,\n generated,\n newEntries,\n outDir: outDirRelative,\n registration,\n skipped,\n writtenPaths,\n })\n } catch (error) {\n // Ctrl+C on the library prompt and errors that already carry their exit\n // code (`this.error` inside helpers) pass through to SanityCommand.catch.\n if (error instanceof Error && error.name === 'ExitPromptError') throw error\n if (error && typeof error === 'object' && 'oclif' in error) throw error\n const err = error as Error\n pullAspectsDebug('Failed to pull aspects', {error: err, mediaLibraryId: mediaLibraryIdFlag})\n this.error(\n styleText('bold', 'Failed to pull aspects') + `\\n\\n${styleText('red', err.message)}`,\n {exit: exitCodes.RUNTIME_ERROR},\n )\n }\n }\n\n /**\n * Offer to declare the generated fields in `sanity.cli.ts`. `--update-config`\n * / `--no-update-config` decide outright; otherwise an interactive run\n * prompts (default yes) and an unattended run leaves the config alone.\n */\n private async maybeRegisterFields({\n configPath,\n entryLines,\n }: {\n configPath: string\n entryLines: string[]\n }): Promise<Registration> {\n const flag = this.flags['update-config']\n const shouldUpdate =\n flag ??\n (this.isUnattended()\n ? false\n : await confirm({\n default: true,\n message: `Declare the ${entryLines.length} pulled ${pluralize('field', entryLines.length)} in sanity.cli.ts?`,\n }))\n if (!shouldUpdate) return 'declined'\n\n const source = await readFile(configPath, 'utf8')\n const result = appendFieldEntries(source, entryLines)\n if (!result.ok) return {reason: result.reason}\n await writeFile(configPath, result.updated)\n return 'added'\n }\n\n private report({\n configuredNames,\n entryLines,\n generated,\n newEntries,\n outDir,\n registration,\n skipped,\n writtenPaths,\n }: {\n configuredNames: Set<string>\n entryLines: string[]\n generated: GeneratedFieldModule[]\n newEntries: GeneratedFieldModule[]\n outDir: string\n registration: Registration | null\n skipped: SkippedAspect[]\n writtenPaths: Map<string, string>\n }): void {\n if (skipped.length > 0) {\n this.logToStderr('')\n this.warn(\n styleText('bold', `Skipped ${skipped.length} ${pluralize('aspect', skipped.length)}`),\n )\n for (const aspect of skipped) {\n const errors = aspect.validationErrors.flatMap((group) => group.map(({message}) => message))\n const reasons = [...aspect.reasons, ...errors]\n this.logToStderr(` - ${aspect.id} ${styleText('dim', reasons.join('; '))}`)\n }\n }\n\n // No modules written and nothing to declare: done. Skipped-but-undeclared\n // fields keep the report alive — their registration outcome still matters.\n if (generated.length === 0 && newEntries.length === 0) {\n this.log('')\n this.log('No field modules generated.')\n return\n }\n\n if (generated.length === 0) {\n this.log('')\n this.log('No field modules generated.')\n } else {\n this.log('')\n this.log(\n `${logSymbols.success} ${styleText('bold', `Generated ${generated.length} field ${pluralize('module', generated.length)}`)}`,\n )\n for (const module of generated) {\n const location = writtenPaths.get(module.name) ?? path.join(outDir, module.filename)\n this.log(` - ${module.name} ${styleText('dim', location)}`)\n for (const warning of module.warnings) {\n this.log(` ${logSymbols.warning} ${warning}`)\n }\n }\n }\n\n if (newEntries.length > 0 && registration === 'added') {\n this.log('')\n this.log(\n `${logSymbols.success} Declared ${newEntries.length} ${pluralize('field', newEntries.length)} in ${styleText('bold', 'sanity.cli.ts')}`,\n )\n } else if (newEntries.length > 0) {\n if (registration !== null && typeof registration === 'object') {\n this.warn(`Could not update sanity.cli.ts automatically: ${registration.reason}`)\n }\n this.log('')\n this.log(\n configuredNames.size > 0\n ? `Add the pulled ${pluralize('field', newEntries.length)} to the \\`fields\\` array in ${styleText('bold', 'sanity.cli.ts')}:`\n : `Declare the pulled ${pluralize('field', newEntries.length)} in ${styleText('bold', 'sanity.cli.ts')}:`,\n )\n this.log('')\n this.log(entryLines.map((line) => ` ${line}`).join('\\n'))\n // The deploy mirrors config into stored documents; a pulled-but-undeclared\n // field reads as stale there, so leaving the entries out is destructive.\n this.warn(\n '`sanity deploy` deletes the stored definition of any field not declared in sanity.cli.ts.',\n )\n }\n\n this.log('')\n this.log(\n 'Once declared in sanity.cli.ts, the config app owns these definitions — deploying the',\n )\n this.log('config overwrites the stored documents this pull read from. Stored definitions')\n this.log('not declared in sanity.cli.ts are deleted on deploy.')\n }\n}\n\n/**\n * The write target for a declared `src` path. A declared path may omit the\n * extension — the bundler and jiti both resolve `./src/fields/notes` to\n * `notes.ts(x)` — so writing it literally would create an extensionless file\n * next to the module deploy actually evaluates. Resolve to the existing\n * module file; if none exists, append the generated module's extension.\n */\nasync function resolveDeclaredSrc(declaredPath: string, moduleFilename: string): Promise<string> {\n if (path.extname(declaredPath) !== '') return declaredPath\n const existing = await firstExisting([`${declaredPath}.ts`, `${declaredPath}.tsx`])\n return existing ?? `${declaredPath}${path.extname(moduleFilename)}`\n}\n\n/** The first path in `paths` that exists, or `undefined`. */\nasync function firstExisting(paths: string[]): Promise<string | undefined> {\n for (const filePath of paths) {\n const exists = await access(filePath).then(\n () => true,\n () => false,\n )\n if (exists) return filePath\n }\n return undefined\n}\n"],"names":["access","mkdir","readFile","writeFile","path","styleText","Flags","exitCodes","ProjectRootNotFoundError","SanityCommand","subdebug","confirm","logSymbols","spinner","resolveWorkbenchConfig","appendFieldEntries","convertAspectDocuments","formatFieldEntries","resolveMediaLibraryId","getAspects","pluralize","defineCommandTelemetry","pullAspectsDebug","NOT_A_CONFIG_APP","flags","string","description","default","overwrite","boolean","allowNo","MediaPullAspectsCommand","examples","command","hidden","telemetry","redact","run","mediaLibraryIdFlag","outDir","cliConfig","getCliConfig","err","error","exit","RUNTIME_ERROR","config","root","getProjectRoot","outDirAbsolute","resolve","directory","outDirRelative","relative","isAbsolute","startsWith","sep","USAGE_ERROR","mediaLibraryId","organizationId","unattended","isUnattended","spin","start","documents","stop","length","log","configuredNames","Set","fields","map","field","name","configuredSrcByName","Map","src","modules","winners","skipped","generated","writtenPaths","existingPaths","module","has","push","id","reasons","status","validationErrors","defaultDestination","join","filename","existing","firstExisting","replace","set","declaredSrc","get","undefined","destination","resolveDeclaredSrc","dirname","recursive","source","entryPaths","newEntries","filter","entryLines","split","registration","maybeRegisterFields","configPath","report","Error","message","flag","shouldUpdate","result","ok","reason","updated","logToStderr","warn","aspect","errors","flatMap","group","success","location","warning","warnings","size","line","declaredPath","moduleFilename","extname","paths","filePath","exists","then"],"mappings":"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"}
@@ -71,6 +71,10 @@ function apiValidator({ args, flags }) {
71
71
  'context:build': conditionalDenyFlags('watch'),
72
72
  'context:create': allow,
73
73
  'context:delete': allow,
74
+ 'context:entries:get': allow,
75
+ 'context:entries:list': allow,
76
+ // See context:build — --watch has no deadline and would hang the MCP tool call.
77
+ 'context:entries:rebuild': conditionalDenyFlags('watch'),
74
78
  'context:get': allow,
75
79
  // --file reads the import payload from the local filesystem; text, URL and
76
80
  // dataset imports are remote-only.
@@ -80,8 +84,17 @@ function apiValidator({ args, flags }) {
80
84
  'context:imports:get': allow,
81
85
  'context:imports:list': allow,
82
86
  // See context:build — --watch has no deadline and would hang the MCP tool call.
87
+ 'context:issues:apply': conditionalDenyFlags('watch'),
88
+ 'context:issues:dismiss': allow,
89
+ 'context:issues:get': allow,
90
+ 'context:issues:list': allow,
91
+ 'context:issues:reopen': allow,
92
+ // See context:build — --watch has no deadline and would hang the MCP tool call.
93
+ 'context:issues:resolve': conditionalDenyFlags('watch'),
94
+ // See context:build — --watch has no deadline and would hang the MCP tool call.
83
95
  'context:jobs:get': conditionalDenyFlags('watch'),
84
96
  'context:list': allow,
97
+ 'context:query': allow,
85
98
  'context:refresh': allow,
86
99
  'context:update': allow,
87
100
  'cors:add': allow,
@@ -170,6 +183,10 @@ function apiValidator({ args, flags }) {
170
183
  ],
171
184
  validate: ({ args })=>typeof args.source === 'string' && isRemoteAssetSource(args.source)
172
185
  }),
186
+ // media:pull-aspects and media:prune-aspect-data are hidden while the
187
+ // config app is in beta, so they take the deny-by-default path like any
188
+ // unlisted command. The exhaustiveness test forces explicit entries when
189
+ // they are unhidden.
173
190
  // Creates migration source files in the local project.
174
191
  'migrations:create': deny,
175
192
  // Reads and loads migration definitions from the local project.