@sanity/cli 8.22.1 → 8.24.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 (98) hide show
  1. package/README.md +313 -0
  2. package/dist/actions/codemods/dashboard.js +136 -8
  3. package/dist/actions/codemods/dashboard.js.map +1 -1
  4. package/dist/actions/codemods/types.js.map +1 -1
  5. package/dist/actions/context/watchJob.js +9 -3
  6. package/dist/actions/context/watchJob.js.map +1 -1
  7. package/dist/actions/deploy/blueprintManaged.js +59 -0
  8. package/dist/actions/deploy/blueprintManaged.js.map +1 -0
  9. package/dist/actions/deploy/deployApp.js +117 -1
  10. package/dist/actions/deploy/deployApp.js.map +1 -1
  11. package/dist/actions/deploy/deployRunner.js.map +1 -1
  12. package/dist/actions/deploy/deployStudio.js +6 -31
  13. package/dist/actions/deploy/deployStudio.js.map +1 -1
  14. package/dist/actions/exec/runScript.worker.js +6 -2
  15. package/dist/actions/exec/runScript.worker.js.map +1 -1
  16. package/dist/actions/init/bootstrapLocalTemplate.js +35 -11
  17. package/dist/actions/init/bootstrapLocalTemplate.js.map +1 -1
  18. package/dist/actions/init/createMediaLibraryCliConfig.js +12 -0
  19. package/dist/actions/init/createMediaLibraryCliConfig.js.map +1 -0
  20. package/dist/actions/init/determineMediaLibraryTemplate.js +14 -0
  21. package/dist/actions/init/determineMediaLibraryTemplate.js.map +1 -0
  22. package/dist/actions/init/initAction.js +35 -6
  23. package/dist/actions/init/initAction.js.map +1 -1
  24. package/dist/actions/init/initApp.js +81 -6
  25. package/dist/actions/init/initApp.js.map +1 -1
  26. package/dist/actions/init/offerAspectPull.js +81 -0
  27. package/dist/actions/init/offerAspectPull.js.map +1 -0
  28. package/dist/actions/init/project/getOrCreateDataset.js +8 -5
  29. package/dist/actions/init/project/getOrCreateDataset.js.map +1 -1
  30. package/dist/actions/init/project/getOrCreateProject.js +7 -5
  31. package/dist/actions/init/project/getOrCreateProject.js.map +1 -1
  32. package/dist/actions/init/project/getProjectDetails.js +25 -2
  33. package/dist/actions/init/project/getProjectDetails.js.map +1 -1
  34. package/dist/actions/init/project/promptUserForOrganization.js +6 -4
  35. package/dist/actions/init/project/promptUserForOrganization.js.map +1 -1
  36. package/dist/actions/init/templates/index.js +3 -0
  37. package/dist/actions/init/templates/index.js.map +1 -1
  38. package/dist/actions/init/templates/mediaLibraryConfig.js +51 -0
  39. package/dist/actions/init/templates/mediaLibraryConfig.js.map +1 -0
  40. package/dist/actions/media/aspectName.js +8 -0
  41. package/dist/actions/media/aspectName.js.map +1 -0
  42. package/dist/actions/media/aspectSync/contract.js +163 -0
  43. package/dist/actions/media/aspectSync/contract.js.map +1 -0
  44. package/dist/actions/media/aspectSync/syncAspects.js +115 -0
  45. package/dist/actions/media/aspectSync/syncAspects.js.map +1 -0
  46. package/dist/actions/media/cliConfigFields.js +192 -0
  47. package/dist/actions/media/cliConfigFields.js.map +1 -0
  48. package/dist/actions/media/pullAspects.js +225 -0
  49. package/dist/actions/media/pullAspects.js.map +1 -0
  50. package/dist/actions/media/resolveMediaLibraryId.js +83 -0
  51. package/dist/actions/media/resolveMediaLibraryId.js.map +1 -0
  52. package/dist/commands/codemod.js +1 -0
  53. package/dist/commands/codemod.js.map +1 -1
  54. package/dist/commands/context/build.js +2 -2
  55. package/dist/commands/context/build.js.map +1 -1
  56. package/dist/commands/context/entries/get.js +83 -0
  57. package/dist/commands/context/entries/get.js.map +1 -0
  58. package/dist/commands/context/entries/list.js +82 -0
  59. package/dist/commands/context/entries/list.js.map +1 -0
  60. package/dist/commands/context/entries/rebuild.js +95 -0
  61. package/dist/commands/context/entries/rebuild.js.map +1 -0
  62. package/dist/commands/context/issues/apply.js +86 -0
  63. package/dist/commands/context/issues/apply.js.map +1 -0
  64. package/dist/commands/context/issues/dismiss.js +52 -0
  65. package/dist/commands/context/issues/dismiss.js.map +1 -0
  66. package/dist/commands/context/issues/get.js +105 -0
  67. package/dist/commands/context/issues/get.js.map +1 -0
  68. package/dist/commands/context/issues/list.js +128 -0
  69. package/dist/commands/context/issues/list.js.map +1 -0
  70. package/dist/commands/context/issues/reopen.js +53 -0
  71. package/dist/commands/context/issues/reopen.js.map +1 -0
  72. package/dist/commands/context/issues/resolve.js +137 -0
  73. package/dist/commands/context/issues/resolve.js.map +1 -0
  74. package/dist/commands/context/jobs/get.js +2 -2
  75. package/dist/commands/context/jobs/get.js.map +1 -1
  76. package/dist/commands/context/query.js +92 -0
  77. package/dist/commands/context/query.js.map +1 -0
  78. package/dist/commands/deploy.js +13 -0
  79. package/dist/commands/deploy.js.map +1 -1
  80. package/dist/commands/media/prune-aspect-data.js +170 -0
  81. package/dist/commands/media/prune-aspect-data.js.map +1 -0
  82. package/dist/commands/media/pull-aspects.js +309 -0
  83. package/dist/commands/media/pull-aspects.js.map +1 -0
  84. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +17 -0
  85. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
  86. package/dist/services/context.js +96 -3
  87. package/dist/services/context.js.map +1 -1
  88. package/dist/services/mediaLibraries.js +175 -8
  89. package/dist/services/mediaLibraries.js.map +1 -1
  90. package/dist/util/searchChoices.js +52 -0
  91. package/dist/util/searchChoices.js.map +1 -0
  92. package/oclif.manifest.json +2134 -1445
  93. package/package.json +9 -8
  94. package/templates/media-library-config/README.md +52 -0
  95. package/templates/media-library-config/eslint.config.txt +52 -0
  96. package/templates/media-library-config/src/fields/caption.tsx +29 -0
  97. package/templates/media-library-config/src/fields/notes.tsx +54 -0
  98. package/templates/shopify/.gitignore +0 -26
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/commands/context/query.ts"],"sourcesContent":["import {Args, Flags} from '@oclif/core'\nimport {type FlagInput} from '@oclif/core/interfaces'\nimport {colorizeJson, exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'\nimport {getErrorMessage} from '@sanity/cli-core/errors'\nimport {isRecord} from '@sanity/cli-core/util'\n\nimport {\n MissingOrganizationError,\n resolveOrganizationId,\n} from '../../actions/context/resolveOrganizationId.js'\nimport {queryContext} from '../../services/context.js'\nimport {getOrganizationFlag} from '../../util/sharedFlags.js'\nimport {defineCommandTelemetry} from '../../util/telemetry/commandTelemetry.js'\n\nconst queryContextDebug = subdebug('context:query')\n\nconst flags = {\n ...getOrganizationFlag({\n description: 'Organization to query',\n semantics: 'override',\n }),\n params: Flags.custom<Record<string, unknown>>({\n description: 'Query parameters as a JSON object',\n helpValue: '<json>',\n parse: async (input: string) => {\n let parsed: unknown\n try {\n parsed = JSON.parse(input)\n } catch {\n throw new Error('`--params` must be valid JSON')\n }\n if (!isRecord(parsed)) {\n throw new Error('`--params` must be a JSON object, like \\'{\"type\": \"issue\"}\\'')\n }\n return parsed\n },\n })(),\n} satisfies FlagInput\n\nexport class QueryContextCommand extends SanityCommand<typeof QueryContextCommand> {\n static override args = {\n query: Args.string({\n description: \"GROQ query to run against the organization's Context documents\",\n required: true,\n }),\n }\n\n static override description = \"Query an organization's Context documents with GROQ\"\n\n static override enableJsonFlag = true\n\n static override examples = [\n {\n command:\n '<%= config.bin %> <%= command.id %> \\'*[_type == \"sanity.context.issue\" && status == \"open\"]{_id, knowledgeBaseId}\\'',\n description: 'List open issues across all knowledge bases',\n },\n {\n command:\n '<%= config.bin %> <%= command.id %> \\'*[_type == \"sanity.context.entry\" && knowledgeBaseId == $kb].path\\' --params \\'{\"kb\": \"kb-abc123\"}\\' --json',\n description: 'List the entry paths of one knowledge base as plain JSON',\n },\n ]\n\n static override flags = flags\n\n static telemetry = defineCommandTelemetry(flags, {redact: ['params']})\n\n public async run(): Promise<void> {\n const {query} = this.args\n const {json, organization, params} = this.flags\n\n let organizationId: string\n try {\n organizationId = await resolveOrganizationId({\n configuredOrganizationId: (await this.tryGetCliConfig()).app?.organizationId,\n flagOrganizationId: organization,\n unattended: this.isUnattended(),\n })\n } catch (error) {\n queryContextDebug('Error resolving organization', error)\n this.error(getErrorMessage(error), {\n exit:\n error instanceof MissingOrganizationError\n ? exitCodes.USAGE_ERROR\n : exitCodes.RUNTIME_ERROR,\n })\n }\n\n let result\n try {\n result = await queryContext(organizationId, query, params)\n } catch (error) {\n queryContextDebug('Error running query', error)\n this.error(`Failed to run query: ${getErrorMessage(error)}`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n // GROQ returns null for no match; guard undefined, which colorizeJson cannot tokenize\n const output = result ?? null\n if (json) {\n this.logJson(output)\n return\n }\n\n this.log(colorizeJson(output))\n }\n}\n"],"names":["Args","Flags","colorizeJson","exitCodes","SanityCommand","subdebug","getErrorMessage","isRecord","MissingOrganizationError","resolveOrganizationId","queryContext","getOrganizationFlag","defineCommandTelemetry","queryContextDebug","flags","description","semantics","params","custom","helpValue","parse","input","parsed","JSON","Error","QueryContextCommand","args","query","string","required","enableJsonFlag","examples","command","telemetry","redact","run","json","organization","organizationId","configuredOrganizationId","tryGetCliConfig","app","flagOrganizationId","unattended","isUnattended","error","exit","USAGE_ERROR","RUNTIME_ERROR","result","output","logJson","log"],"mappings":"AAAA,SAAQA,IAAI,EAAEC,KAAK,QAAO,cAAa;AAEvC,SAAQC,YAAY,EAAEC,SAAS,EAAEC,aAAa,EAAEC,QAAQ,QAAO,mBAAkB;AACjF,SAAQC,eAAe,QAAO,0BAAyB;AACvD,SAAQC,QAAQ,QAAO,wBAAuB;AAE9C,SACEC,wBAAwB,EACxBC,qBAAqB,QAChB,iDAAgD;AACvD,SAAQC,YAAY,QAAO,4BAA2B;AACtD,SAAQC,mBAAmB,QAAO,4BAA2B;AAC7D,SAAQC,sBAAsB,QAAO,2CAA0C;AAE/E,MAAMC,oBAAoBR,SAAS;AAEnC,MAAMS,QAAQ;IACZ,GAAGH,oBAAoB;QACrBI,aAAa;QACbC,WAAW;IACb,EAAE;IACFC,QAAQhB,MAAMiB,MAAM,CAA0B;QAC5CH,aAAa;QACbI,WAAW;QACXC,OAAO,OAAOC;YACZ,IAAIC;YACJ,IAAI;gBACFA,SAASC,KAAKH,KAAK,CAACC;YACtB,EAAE,OAAM;gBACN,MAAM,IAAIG,MAAM;YAClB;YACA,IAAI,CAACjB,SAASe,SAAS;gBACrB,MAAM,IAAIE,MAAM;YAClB;YACA,OAAOF;QACT;IACF;AACF;AAEA,OAAO,MAAMG,4BAA4BrB;IACvC,OAAgBsB,OAAO;QACrBC,OAAO3B,KAAK4B,MAAM,CAAC;YACjBb,aAAa;YACbc,UAAU;QACZ;IACF,EAAC;IAED,OAAgBd,cAAc,sDAAqD;IAEnF,OAAgBe,iBAAiB,KAAI;IAErC,OAAgBC,WAAW;QACzB;YACEC,SACE;YACFjB,aAAa;QACf;QACA;YACEiB,SACE;YACFjB,aAAa;QACf;KACD,CAAA;IAED,OAAgBD,QAAQA,MAAK;IAE7B,OAAOmB,YAAYrB,uBAAuBE,OAAO;QAACoB,QAAQ;YAAC;SAAS;IAAA,GAAE;IAEtE,MAAaC,MAAqB;QAChC,MAAM,EAACR,KAAK,EAAC,GAAG,IAAI,CAACD,IAAI;QACzB,MAAM,EAACU,IAAI,EAAEC,YAAY,EAAEpB,MAAM,EAAC,GAAG,IAAI,CAACH,KAAK;QAE/C,IAAIwB;QACJ,IAAI;YACFA,iBAAiB,MAAM7B,sBAAsB;gBAC3C8B,0BAA0B,AAAC,CAAA,MAAM,IAAI,CAACC,eAAe,EAAC,EAAGC,GAAG,EAAEH;gBAC9DI,oBAAoBL;gBACpBM,YAAY,IAAI,CAACC,YAAY;YAC/B;QACF,EAAE,OAAOC,OAAO;YACdhC,kBAAkB,gCAAgCgC;YAClD,IAAI,CAACA,KAAK,CAACvC,gBAAgBuC,QAAQ;gBACjCC,MACED,iBAAiBrC,2BACbL,UAAU4C,WAAW,GACrB5C,UAAU6C,aAAa;YAC/B;QACF;QAEA,IAAIC;QACJ,IAAI;YACFA,SAAS,MAAMvC,aAAa4B,gBAAgBX,OAAOV;QACrD,EAAE,OAAO4B,OAAO;YACdhC,kBAAkB,uBAAuBgC;YACzC,IAAI,CAACA,KAAK,CAAC,CAAC,qBAAqB,EAAEvC,gBAAgBuC,QAAQ,EAAE;gBAC3DC,MAAM3C,UAAU6C,aAAa;YAC/B;QACF;QAEA,sFAAsF;QACtF,MAAME,SAASD,UAAU;QACzB,IAAIb,MAAM;YACR,IAAI,CAACe,OAAO,CAACD;YACb;QACF;QAEA,IAAI,CAACE,GAAG,CAAClD,aAAagD;IACxB;AACF"}
@@ -86,6 +86,12 @@ export class DeployCommand extends SanityCommand {
86
86
  default: false,
87
87
  description: 'Output the result as JSON'
88
88
  }),
89
+ 'media-library-id': Flags.string({
90
+ description: 'For a Media Library config deploy: the media library to sync the aspect definitions to. Defaults to the single active library of the configured organization; required when the organization has several',
91
+ // Part of the beta config-app flow, like the hidden media commands it
92
+ // pairs with. A help-listed flag is hard to take back — unhide at GA.
93
+ hidden: true
94
+ }),
89
95
  minify: Flags.boolean({
90
96
  allowNo: true,
91
97
  default: true,
@@ -120,6 +126,13 @@ export class DeployCommand extends SanityCommand {
120
126
  const cliConfig = await this.getCliConfig();
121
127
  const projectRoot = await this.getProjectRoot();
122
128
  const isApp = determineIsApp(cliConfig);
129
+ // `--media-library-id` only steers the aspect-document sync of a Media
130
+ // Library config deploy (see deployApp); studio and app deploys never read
131
+ // it. Warned rather than silently dropped (same courtesy init gives
132
+ // --dashboard on the media template).
133
+ if (flags['media-library-id'] && !resolveWorkbenchConfig(cliConfig)) {
134
+ this.output.warn('--media-library-id is ignored — it only applies when deploying a Media Library config');
135
+ }
123
136
  if (flags.create) {
124
137
  if (!isApp) {
125
138
  return this.output.error('--create is only supported for App SDK apps. For a studio, use --url <hostname>.');
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/commands/deploy.ts"],"sourcesContent":["import path from 'node:path'\n\nimport {Args, Flags} from '@oclif/core'\nimport {exitCodes, SanityCommand} from '@sanity/cli-core'\nimport {confirm} from '@sanity/cli-core/ux'\nimport {getWorkbench, resolveWorkbenchConfig} from '@sanity/workbench-cli/deploy'\n\nimport {deployApp} from '../actions/deploy/deployApp.js'\nimport {deployDebug} from '../actions/deploy/deployDebug.js'\nimport {deployStudio} from '../actions/deploy/deployStudio.js'\nimport {getAppId} from '../util/appId.js'\nimport {determineIsApp} from '../util/determineIsApp.js'\nimport {dirIsEmptyOrNonExistent} from '../util/dirIsEmptyOrNonExistent.js'\n\nexport class DeployCommand extends SanityCommand<typeof DeployCommand> {\n static override args = {\n sourceDir: Args.directory({\n description: 'Source directory',\n }),\n }\n\n static override description = 'Builds and deploys Sanity Studio or application to Sanity hosting'\n\n static override enableJsonFlag = true\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Build and deploy the studio to Sanity hosting',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --no-minify --source-maps',\n description: 'Deploys non-minified build with source maps',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --schema-required',\n description:\n 'Fail fast on schema store fails - for when other services rely on the stored schema',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --external',\n description: 'Register an externally hosted studio (studioHost contains full URL)',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --external --url https://apps.example.com',\n description: 'Register an externally hosted studio, served from the provided origin',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --create --title \"My App\" --yes --json',\n description:\n 'Create and deploy an App SDK app without prompts. Save application.id as deployment.appId, then omit --create on later deploys',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --create --title \"My App\" --dry-run --json',\n description: 'Preview a new App SDK deployment without creating or uploading anything',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --url my-studio --yes',\n description: 'Deploy a studio without prompts using its hostname',\n },\n ]\n\n static override flags = {\n 'auto-updates': Flags.boolean({\n allowNo: true,\n deprecated: true,\n description: 'Automatically update the studio to the latest version',\n }),\n build: Flags.boolean({\n allowNo: true,\n default: true,\n description:\n 'Build the studio before deploying (use --no-build to deploy existing `dist/` output)',\n }),\n create: Flags.boolean({\n description: 'Create a new App SDK app, even if the organization has other apps',\n exclusive: ['external', 'url'],\n }),\n 'dry-run': Flags.boolean({\n default: false,\n description: 'Report what would be deployed without uploading or creating anything',\n }),\n external: Flags.boolean({\n default: false,\n description: 'Register an externally hosted studio',\n exclusive: ['source-maps', 'minify'],\n }),\n json: Flags.boolean({\n char: 'j',\n default: false,\n description: 'Output the result as JSON',\n }),\n minify: Flags.boolean({\n allowNo: true,\n default: true,\n description: 'Minify built JavaScript (use --no-minify to skip for faster builds)',\n }),\n 'schema-required': Flags.boolean({\n default: false,\n description: 'Fail if schema deployment fails',\n }),\n 'source-maps': Flags.boolean({\n default: false,\n description: 'Enable source maps for built bundles (increases size of bundle)',\n }),\n title: Flags.string({\n description:\n 'Title for a newly created application or studio. For apps, use --create to select creation when the organization already has apps',\n }),\n url: Flags.string({\n description:\n 'Studio URL for deployment. For external deploys, the full URL. For hosted studios, the hostname (e.g. \"my-studio\" or \"my-studio.sanity.studio\")',\n }),\n verbose: Flags.boolean({\n default: false,\n description: 'Enable verbose logging',\n }),\n yes: Flags.boolean({\n char: 'y',\n default: false,\n description:\n 'Unattended mode, answers \"yes\" to any \"yes/no\" prompt and otherwise uses defaults',\n }),\n }\n\n public async run(): Promise<void> {\n const {flags} = await this.parse(DeployCommand)\n\n const cliConfig = await this.getCliConfig()\n const projectRoot = await this.getProjectRoot()\n\n const isApp = determineIsApp(cliConfig)\n\n if (flags.create) {\n if (!isApp) {\n return this.output.error(\n '--create is only supported for App SDK apps. For a studio, use --url <hostname>.',\n )\n }\n if (getAppId(cliConfig)) {\n return this.output.error(\n '--create cannot be used with a configured app ID. Omit --create to deploy to that app, or remove deployment.appId and app.id to create a new one.',\n )\n }\n if (getWorkbench(cliConfig) || resolveWorkbenchConfig(cliConfig)) {\n return this.output.error(\n '--create is not supported for this app configuration. Omit --create and run sanity deploy.',\n )\n }\n }\n\n const defaultOutputDir = path.resolve(path.join(projectRoot.directory, 'dist'))\n const sourceDir = path.resolve(process.cwd(), this.args.sourceDir || defaultOutputDir)\n\n // Skip the directory check if the studio is externally hosted\n if (this.args.sourceDir && this.args.sourceDir !== 'dist' && !flags.external) {\n let relativeOutput = path.relative(process.cwd(), sourceDir)\n if (relativeOutput[0] !== '.') {\n relativeOutput = `./${relativeOutput}`\n }\n\n if (!this.isUnattended() && !flags['dry-run']) {\n const isEmpty = await dirIsEmptyOrNonExistent(sourceDir)\n const shouldProceed =\n isEmpty ||\n (await confirm({\n default: false,\n message: `\"${relativeOutput}\" is not empty, do you want to proceed?`,\n }))\n\n if (!shouldProceed) {\n this.output.error('Cancelled.', {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n\n // Keep --json's stdout clean for the payload\n if (!flags.json) this.output.log(`Building to ${relativeOutput}\\n`)\n }\n\n // Force yes downstream: build/app resolution otherwise prompts for prerelease/version choices\n const deployFlags = this.isUnattended() || flags['dry-run'] ? {...flags, yes: true} : flags\n\n if (isApp) {\n deployDebug('Deploying app')\n await deployApp({\n cliConfig,\n flags: deployFlags,\n output: this.output,\n projectRoot,\n sourceDir,\n })\n } else {\n deployDebug('Deploying studio')\n await deployStudio({\n cliConfig,\n flags: deployFlags,\n output: this.output,\n projectRoot,\n sourceDir,\n })\n }\n }\n}\n"],"names":["path","Args","Flags","exitCodes","SanityCommand","confirm","getWorkbench","resolveWorkbenchConfig","deployApp","deployDebug","deployStudio","getAppId","determineIsApp","dirIsEmptyOrNonExistent","DeployCommand","args","sourceDir","directory","description","enableJsonFlag","examples","command","flags","boolean","allowNo","deprecated","build","default","create","exclusive","external","json","char","minify","title","string","url","verbose","yes","run","parse","cliConfig","getCliConfig","projectRoot","getProjectRoot","isApp","output","error","defaultOutputDir","resolve","join","process","cwd","relativeOutput","relative","isUnattended","isEmpty","shouldProceed","message","exit","RUNTIME_ERROR","log","deployFlags"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/commands/deploy.ts"],"sourcesContent":["import path from 'node:path'\n\nimport {Args, Flags} from '@oclif/core'\nimport {exitCodes, SanityCommand} from '@sanity/cli-core'\nimport {confirm} from '@sanity/cli-core/ux'\nimport {getWorkbench, resolveWorkbenchConfig} from '@sanity/workbench-cli/deploy'\n\nimport {deployApp} from '../actions/deploy/deployApp.js'\nimport {deployDebug} from '../actions/deploy/deployDebug.js'\nimport {deployStudio} from '../actions/deploy/deployStudio.js'\nimport {getAppId} from '../util/appId.js'\nimport {determineIsApp} from '../util/determineIsApp.js'\nimport {dirIsEmptyOrNonExistent} from '../util/dirIsEmptyOrNonExistent.js'\n\nexport class DeployCommand extends SanityCommand<typeof DeployCommand> {\n static override args = {\n sourceDir: Args.directory({\n description: 'Source directory',\n }),\n }\n\n static override description = 'Builds and deploys Sanity Studio or application to Sanity hosting'\n\n static override enableJsonFlag = true\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Build and deploy the studio to Sanity hosting',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --no-minify --source-maps',\n description: 'Deploys non-minified build with source maps',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --schema-required',\n description:\n 'Fail fast on schema store fails - for when other services rely on the stored schema',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --external',\n description: 'Register an externally hosted studio (studioHost contains full URL)',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --external --url https://apps.example.com',\n description: 'Register an externally hosted studio, served from the provided origin',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --create --title \"My App\" --yes --json',\n description:\n 'Create and deploy an App SDK app without prompts. Save application.id as deployment.appId, then omit --create on later deploys',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --create --title \"My App\" --dry-run --json',\n description: 'Preview a new App SDK deployment without creating or uploading anything',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --url my-studio --yes',\n description: 'Deploy a studio without prompts using its hostname',\n },\n ]\n\n static override flags = {\n 'auto-updates': Flags.boolean({\n allowNo: true,\n deprecated: true,\n description: 'Automatically update the studio to the latest version',\n }),\n build: Flags.boolean({\n allowNo: true,\n default: true,\n description:\n 'Build the studio before deploying (use --no-build to deploy existing `dist/` output)',\n }),\n create: Flags.boolean({\n description: 'Create a new App SDK app, even if the organization has other apps',\n exclusive: ['external', 'url'],\n }),\n 'dry-run': Flags.boolean({\n default: false,\n description: 'Report what would be deployed without uploading or creating anything',\n }),\n external: Flags.boolean({\n default: false,\n description: 'Register an externally hosted studio',\n exclusive: ['source-maps', 'minify'],\n }),\n json: Flags.boolean({\n char: 'j',\n default: false,\n description: 'Output the result as JSON',\n }),\n 'media-library-id': Flags.string({\n description:\n 'For a Media Library config deploy: the media library to sync the aspect definitions to. Defaults to the single active library of the configured organization; required when the organization has several',\n // Part of the beta config-app flow, like the hidden media commands it\n // pairs with. A help-listed flag is hard to take back — unhide at GA.\n hidden: true,\n }),\n minify: Flags.boolean({\n allowNo: true,\n default: true,\n description: 'Minify built JavaScript (use --no-minify to skip for faster builds)',\n }),\n 'schema-required': Flags.boolean({\n default: false,\n description: 'Fail if schema deployment fails',\n }),\n 'source-maps': Flags.boolean({\n default: false,\n description: 'Enable source maps for built bundles (increases size of bundle)',\n }),\n title: Flags.string({\n description:\n 'Title for a newly created application or studio. For apps, use --create to select creation when the organization already has apps',\n }),\n url: Flags.string({\n description:\n 'Studio URL for deployment. For external deploys, the full URL. For hosted studios, the hostname (e.g. \"my-studio\" or \"my-studio.sanity.studio\")',\n }),\n verbose: Flags.boolean({\n default: false,\n description: 'Enable verbose logging',\n }),\n yes: Flags.boolean({\n char: 'y',\n default: false,\n description:\n 'Unattended mode, answers \"yes\" to any \"yes/no\" prompt and otherwise uses defaults',\n }),\n }\n\n public async run(): Promise<void> {\n const {flags} = await this.parse(DeployCommand)\n\n const cliConfig = await this.getCliConfig()\n const projectRoot = await this.getProjectRoot()\n\n const isApp = determineIsApp(cliConfig)\n\n // `--media-library-id` only steers the aspect-document sync of a Media\n // Library config deploy (see deployApp); studio and app deploys never read\n // it. Warned rather than silently dropped (same courtesy init gives\n // --dashboard on the media template).\n if (flags['media-library-id'] && !resolveWorkbenchConfig(cliConfig)) {\n this.output.warn(\n '--media-library-id is ignored — it only applies when deploying a Media Library config',\n )\n }\n\n if (flags.create) {\n if (!isApp) {\n return this.output.error(\n '--create is only supported for App SDK apps. For a studio, use --url <hostname>.',\n )\n }\n if (getAppId(cliConfig)) {\n return this.output.error(\n '--create cannot be used with a configured app ID. Omit --create to deploy to that app, or remove deployment.appId and app.id to create a new one.',\n )\n }\n if (getWorkbench(cliConfig) || resolveWorkbenchConfig(cliConfig)) {\n return this.output.error(\n '--create is not supported for this app configuration. Omit --create and run sanity deploy.',\n )\n }\n }\n\n const defaultOutputDir = path.resolve(path.join(projectRoot.directory, 'dist'))\n const sourceDir = path.resolve(process.cwd(), this.args.sourceDir || defaultOutputDir)\n\n // Skip the directory check if the studio is externally hosted\n if (this.args.sourceDir && this.args.sourceDir !== 'dist' && !flags.external) {\n let relativeOutput = path.relative(process.cwd(), sourceDir)\n if (relativeOutput[0] !== '.') {\n relativeOutput = `./${relativeOutput}`\n }\n\n if (!this.isUnattended() && !flags['dry-run']) {\n const isEmpty = await dirIsEmptyOrNonExistent(sourceDir)\n const shouldProceed =\n isEmpty ||\n (await confirm({\n default: false,\n message: `\"${relativeOutput}\" is not empty, do you want to proceed?`,\n }))\n\n if (!shouldProceed) {\n this.output.error('Cancelled.', {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n\n // Keep --json's stdout clean for the payload\n if (!flags.json) this.output.log(`Building to ${relativeOutput}\\n`)\n }\n\n // Force yes downstream: build/app resolution otherwise prompts for prerelease/version choices\n const deployFlags = this.isUnattended() || flags['dry-run'] ? {...flags, yes: true} : flags\n\n if (isApp) {\n deployDebug('Deploying app')\n await deployApp({\n cliConfig,\n flags: deployFlags,\n output: this.output,\n projectRoot,\n sourceDir,\n })\n } else {\n deployDebug('Deploying studio')\n await deployStudio({\n cliConfig,\n flags: deployFlags,\n output: this.output,\n projectRoot,\n sourceDir,\n })\n }\n }\n}\n"],"names":["path","Args","Flags","exitCodes","SanityCommand","confirm","getWorkbench","resolveWorkbenchConfig","deployApp","deployDebug","deployStudio","getAppId","determineIsApp","dirIsEmptyOrNonExistent","DeployCommand","args","sourceDir","directory","description","enableJsonFlag","examples","command","flags","boolean","allowNo","deprecated","build","default","create","exclusive","external","json","char","string","hidden","minify","title","url","verbose","yes","run","parse","cliConfig","getCliConfig","projectRoot","getProjectRoot","isApp","output","warn","error","defaultOutputDir","resolve","join","process","cwd","relativeOutput","relative","isUnattended","isEmpty","shouldProceed","message","exit","RUNTIME_ERROR","log","deployFlags"],"mappings":"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"}
@@ -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