@sanity/cli 7.18.0 → 8.0.1

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 (135) hide show
  1. package/README.md +74 -4
  2. package/dist/SanityHelp.js +1 -35
  3. package/dist/SanityHelp.js.map +1 -1
  4. package/dist/actions/assets/assetFileError.js +9 -0
  5. package/dist/actions/assets/assetFileError.js.map +1 -0
  6. package/dist/actions/assets/uploadAssetFromFile.js +40 -0
  7. package/dist/actions/assets/uploadAssetFromFile.js.map +1 -0
  8. package/dist/actions/assets/uploadAssetWithProgress.js +76 -0
  9. package/dist/actions/assets/uploadAssetWithProgress.js.map +1 -0
  10. package/dist/actions/deploy/deployApp.js +39 -44
  11. package/dist/actions/deploy/deployApp.js.map +1 -1
  12. package/dist/actions/deploy/deployChecks.js +21 -13
  13. package/dist/actions/deploy/deployChecks.js.map +1 -1
  14. package/dist/actions/deploy/deployRunner.js +8 -7
  15. package/dist/actions/deploy/deployRunner.js.map +1 -1
  16. package/dist/actions/deploy/deployStudio.js +55 -35
  17. package/dist/actions/deploy/deployStudio.js.map +1 -1
  18. package/dist/actions/deploy/deploymentPlan.js +17 -29
  19. package/dist/actions/deploy/deploymentPlan.js.map +1 -1
  20. package/dist/actions/deploy/resolveDeployTarget.js +2 -8
  21. package/dist/actions/deploy/resolveDeployTarget.js.map +1 -1
  22. package/dist/actions/dev/servers/getDevServerConfig.js +2 -8
  23. package/dist/actions/dev/servers/getDevServerConfig.js.map +1 -1
  24. package/dist/actions/dev/servers/startStudioDevServer.js +7 -1
  25. package/dist/actions/dev/servers/startStudioDevServer.js.map +1 -1
  26. package/dist/actions/init/bootstrapRemoteTemplate.js +2 -2
  27. package/dist/actions/init/bootstrapRemoteTemplate.js.map +1 -1
  28. package/dist/actions/init/sdkAppDependencies.js +1 -1
  29. package/dist/actions/init/sdkAppDependencies.js.map +1 -1
  30. package/dist/actions/init/studioDependencies.js +1 -1
  31. package/dist/actions/init/studioDependencies.js.map +1 -1
  32. package/dist/actions/schema/listSchemas.js +1 -1
  33. package/dist/actions/schema/listSchemas.js.map +1 -1
  34. package/dist/actions/tokens/constants.js +2 -2
  35. package/dist/actions/tokens/constants.js.map +1 -1
  36. package/dist/actions/tokens/expiry.js +35 -0
  37. package/dist/actions/tokens/expiry.js.map +1 -0
  38. package/dist/actions/tokens/types.js.map +1 -1
  39. package/dist/actions/tokens/validateRole.js +5 -2
  40. package/dist/actions/tokens/validateRole.js.map +1 -1
  41. package/dist/actions/typegen/count.js +6 -0
  42. package/dist/actions/typegen/count.js.map +1 -0
  43. package/dist/actions/typegen/formatPath.js +5 -0
  44. package/dist/actions/typegen/formatPath.js.map +1 -0
  45. package/dist/actions/typegen/percent.js +8 -0
  46. package/dist/actions/typegen/percent.js.map +1 -0
  47. package/dist/actions/typegen/renderTypegenProgress.js +88 -0
  48. package/dist/actions/typegen/renderTypegenProgress.js.map +1 -0
  49. package/dist/actions/undeploy/adapters.js +6 -9
  50. package/dist/actions/undeploy/adapters.js.map +1 -1
  51. package/dist/actions/undeploy/runUndeploy.js +12 -4
  52. package/dist/actions/undeploy/runUndeploy.js.map +1 -1
  53. package/dist/actions/undeploy/undeployPlan.js +6 -14
  54. package/dist/actions/undeploy/undeployPlan.js.map +1 -1
  55. package/dist/commands/assets/upload.js +160 -0
  56. package/dist/commands/assets/upload.js.map +1 -0
  57. package/dist/commands/backups/list.js +1 -1
  58. package/dist/commands/backups/list.js.map +1 -1
  59. package/dist/commands/datasets/copy.js +1 -1
  60. package/dist/commands/datasets/copy.js.map +1 -1
  61. package/dist/commands/login.js +2 -8
  62. package/dist/commands/login.js.map +1 -1
  63. package/dist/commands/mcp/configure.js.map +1 -1
  64. package/dist/commands/migrations/list.js +1 -1
  65. package/dist/commands/migrations/list.js.map +1 -1
  66. package/dist/commands/migrations/run.js +67 -3
  67. package/dist/commands/migrations/run.js.map +1 -1
  68. package/dist/commands/new.js +4 -3
  69. package/dist/commands/new.js.map +1 -1
  70. package/dist/commands/organizations/list.js +1 -1
  71. package/dist/commands/organizations/list.js.map +1 -1
  72. package/dist/commands/projects/list.js +39 -12
  73. package/dist/commands/projects/list.js.map +1 -1
  74. package/dist/commands/projects/unclaimed.js +39 -33
  75. package/dist/commands/projects/unclaimed.js.map +1 -1
  76. package/dist/commands/tokens/create.js +92 -7
  77. package/dist/commands/tokens/create.js.map +1 -1
  78. package/dist/commands/tokens/delete.js +2 -2
  79. package/dist/commands/tokens/delete.js.map +1 -1
  80. package/dist/commands/tokens/list.js +12 -11
  81. package/dist/commands/tokens/list.js.map +1 -1
  82. package/dist/commands/tokens/rotate.js +95 -0
  83. package/dist/commands/tokens/rotate.js.map +1 -0
  84. package/dist/commands/typegen/generate.js +222 -0
  85. package/dist/commands/typegen/generate.js.map +1 -0
  86. package/dist/commands/users/list.js +8 -8
  87. package/dist/commands/users/list.js.map +1 -1
  88. package/dist/exports/_internal.d.ts +29 -0
  89. package/dist/exports/_internal.js +1 -0
  90. package/dist/exports/_internal.js.map +1 -1
  91. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +6 -0
  92. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
  93. package/dist/exports/invokeSanityCli/help.js +4 -0
  94. package/dist/exports/invokeSanityCli/help.js.map +1 -1
  95. package/dist/exports/invokeSanityCli/index.js +2 -0
  96. package/dist/exports/invokeSanityCli/index.js.map +1 -1
  97. package/dist/hooks/commandNotFound/topicAliases.js +13 -40
  98. package/dist/hooks/commandNotFound/topicAliases.js.map +1 -1
  99. package/dist/hooks/prerun/setupTelemetry.js +3 -2
  100. package/dist/hooks/prerun/setupTelemetry.js.map +1 -1
  101. package/dist/server/vite/plugin-typegen.js +4 -16
  102. package/dist/server/vite/plugin-typegen.js.map +1 -1
  103. package/dist/services/assets.js +46 -0
  104. package/dist/services/assets.js.map +1 -0
  105. package/dist/services/tokens.js +81 -31
  106. package/dist/services/tokens.js.map +1 -1
  107. package/dist/topicAliases.js +43 -0
  108. package/dist/topicAliases.js.map +1 -1
  109. package/dist/util/parseArguments.js +7 -2
  110. package/dist/util/parseArguments.js.map +1 -1
  111. package/dist/util/readTokenFromStdin.js +17 -0
  112. package/dist/util/readTokenFromStdin.js.map +1 -0
  113. package/dist/util/responsiveTable.js +97 -0
  114. package/dist/util/responsiveTable.js.map +1 -0
  115. package/dist/util/telemetry/commandTelemetry.js +34 -0
  116. package/dist/util/telemetry/commandTelemetry.js.map +1 -0
  117. package/dist/util/unclaimedProjects.js +10 -0
  118. package/dist/util/unclaimedProjects.js.map +1 -1
  119. package/oclif.config.js +2 -4
  120. package/oclif.manifest.json +688 -476
  121. package/package.json +12 -10
  122. package/templates/shopify/components/inputs/ShopifyMetafield.tsx +20 -0
  123. package/templates/shopify/docs/features.md +24 -0
  124. package/templates/shopify/schemaTypes/index.ts +2 -0
  125. package/templates/shopify/schemaTypes/objects/shopify/shopifyCollectionType.ts +18 -1
  126. package/templates/shopify/schemaTypes/objects/shopify/shopifyMetafieldType.ts +47 -0
  127. package/templates/shopify/schemaTypes/objects/shopify/shopifyProductType.ts +17 -0
  128. package/templates/shopify/utils/formatMetafieldValue.ts +30 -0
  129. package/templates/shopify-online-storefront/components/inputs/ShopifyMetafield.tsx +22 -0
  130. package/templates/shopify-online-storefront/schemaTypes/index.ts +4 -0
  131. package/templates/shopify-online-storefront/schemaTypes/objects/shop.ts +16 -0
  132. package/templates/shopify-online-storefront/schemaTypes/objects/shopifyCollection.ts +19 -0
  133. package/templates/shopify-online-storefront/schemaTypes/objects/shopifyMetafield.ts +53 -0
  134. package/templates/shopify-online-storefront/schemaTypes/objects/shopifyProduct.ts +19 -0
  135. package/templates/shopify-online-storefront/utils/formatMetafieldValue.ts +30 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/mcpPolicy.ts"],"sourcesContent":["import {\n allow,\n type CommandPolicySet,\n conditionalDenyFlags,\n conditionalPolicy,\n deny,\n} from './policy.js'\n\nfunction apiValidator({\n args,\n flags,\n}: {\n args: Readonly<Record<string, unknown>>\n flags: Readonly<Record<string, unknown>>\n}): boolean {\n const fields: unknown[] = Array.isArray(flags.field) ? flags.field : []\n\n const fieldReadsFromHost = fields.some((field) => {\n if (typeof field !== 'string') return false\n const separatorIndex = field.indexOf('=')\n return separatorIndex > 0 && field[separatorIndex + 1] === '@'\n })\n if (fieldReadsFromHost) return false\n\n const headers: unknown[] = Array.isArray(flags.header) ? flags.header : []\n\n const headerOverridesAuthentication = headers.some((header) => {\n if (typeof header !== 'string') return false\n const separatorIndex = header.indexOf(':')\n if (separatorIndex <= 0) return false\n\n const name = header.slice(0, separatorIndex).trim().toLowerCase()\n return name === 'authorization' || name === 'cookie'\n })\n\n if (headerOverridesAuthentication) return false\n\n const endpoint = typeof args.endpoint === 'string' ? URL.parse(args.endpoint) : null\n const urlEmbedsCredentials =\n endpoint !== null && (endpoint.username !== '' || endpoint.password !== '')\n\n return !urlEmbedsCredentials\n}\n\n/**\n * MCP programmatic mode disables local project/config discovery (see the CLI\n * execution context). Missing project or dataset values may therefore produce\n * a usage error, but cannot cause local filesystem access. Destructive remote\n * operations are allowed and do not by themselves make a command unsafe.\n *\n * Every manifest command must have exactly one policy here:\n * - allow: every valid invocation is safe\n * - conditional: safety depends on parsed arguments or flags\n * - deny: no invocation is safe\n */\nexport const mcpPolicy: CommandPolicySet = {\n // Special exception, this can be very dangerous but is also super useful\n // to expose. Refuse authentication overrides and host input channels:\n // `--token`, Authorization, and Cookie headers replace the MCP user's authentication,\n // URL-embedded credentials replace it with Basic authentication,\n // `--input` reads the request body from the host's filesystem or stdin, and\n // `-F key=@<file>` / `-F key=@-` field values do the same.\n api: conditionalPolicy({\n deniedFlags: ['input', 'token'],\n validate: apiValidator,\n }),\n\n 'backups:disable': allow,\n // Writes a downloaded backup to the local filesystem.\n 'backups:download': deny,\n 'backups:enable': allow,\n 'backups:list': allow,\n\n // Requires a local Studio project and writes build output to disk.\n build: deny,\n\n // Reads and rewrites local source code.\n codemod: deny,\n\n 'cors:add': allow,\n 'cors:delete': allow,\n 'cors:list': allow,\n\n 'datasets:alias:create': allow,\n 'datasets:alias:delete': allow,\n 'datasets:alias:link': allow,\n 'datasets:alias:unlink': allow,\n 'datasets:copy': allow,\n 'datasets:create': allow,\n 'datasets:delete': allow,\n 'datasets:embeddings:disable': allow,\n 'datasets:embeddings:enable': allow,\n 'datasets:embeddings:status': allow,\n // Writes dataset contents and assets to the local filesystem.\n 'datasets:export': deny,\n // Reads import data from the local filesystem and may replace documents.\n 'datasets:import': deny,\n 'datasets:list': allow,\n 'datasets:visibility:get': allow,\n 'datasets:visibility:set': allow,\n\n // Inspects local project files and can print authentication secrets.\n debug: deny,\n\n // Reads, builds, and deploys a local Studio project.\n deploy: deny,\n\n // Loads a local Studio project and starts a development server.\n dev: deny,\n\n // Opens a browser on the machine running the MCP server.\n 'docs:browse': deny,\n // --web opens a browser on the machine running the MCP server.\n 'docs:read': conditionalDenyFlags('web'),\n 'docs:search': allow,\n\n // Reads and executes local project configuration for diagnostics.\n doctor: deny,\n\n // Reads document input from disk or launches a local editor.\n 'documents:create': deny,\n 'documents:delete': allow,\n 'documents:get': allow,\n 'documents:query': allow,\n // Loads a local Studio schema to validate documents.\n 'documents:validate': deny,\n\n // Executes arbitrary code in the local Studio context.\n exec: deny,\n\n // Loads a local schema and deploys a GraphQL API.\n 'graphql:deploy': deny,\n 'graphql:list': allow,\n // --api loads GraphQL definitions from the local project; explicit project/dataset flags are safe.\n 'graphql:undeploy': conditionalDenyFlags('api'),\n\n 'hooks:attempt': allow,\n 'hooks:create': allow,\n 'hooks:delete': allow,\n 'hooks:list': allow,\n 'hooks:logs': allow,\n\n // Creates or modifies a local project and may install dependencies.\n init: deny,\n\n // Installs packages into the local project.\n install: deny,\n\n // Opens a browser on the machine running the MCP server.\n learn: deny,\n\n // Performs an authentication flow.\n login: deny,\n\n // Performs an authentication operation and clears local credentials.\n logout: deny,\n\n // Reads local project configuration and opens a browser.\n manage: deny,\n\n // Loads local Studio configuration and writes manifest files.\n 'manifest:extract': deny,\n\n // Reads and writes local MCP client configuration.\n 'mcp:configure': deny,\n\n // Writes an aspect definition to the local filesystem.\n 'media:create-aspect': deny,\n 'media:delete-aspect': allow,\n // Reads a local aspect definition before deploying it.\n 'media:deploy-aspect': deny,\n // Writes media assets to the local filesystem.\n 'media:export': deny,\n // Reads media assets from the local filesystem.\n 'media:import': deny,\n\n // Creates migration source files in the local project.\n 'migrations:create': deny,\n // Reads and loads migration definitions from the local project.\n 'migrations:list': deny,\n // Executes local migration code that may perform arbitrary document mutations.\n 'migrations:run': deny,\n\n // Writes project setup values and scaffolds local applications.\n new: deny,\n\n // --web opens a browser on the machine running the MCP server.\n 'openapi:get': conditionalDenyFlags('web'),\n 'openapi:list': conditionalDenyFlags('web'),\n\n 'organizations:create': allow,\n 'organizations:delete': allow,\n 'organizations:get': allow,\n 'organizations:list': allow,\n 'organizations:update': allow,\n\n // Serves a local production build.\n preview: deny,\n\n 'projects:create': allow,\n 'projects:list': allow,\n // Reads locally stored claim URLs and robot tokens.\n 'projects:unclaimed': deny,\n\n // Loads the local Studio configuration to resolve the datasets containing each schema.\n 'schemas:delete': deny,\n // Loads local schema files before deploying them.\n 'schemas:deploy': deny,\n // Loads local Studio configuration and writes an extracted schema to disk.\n 'schemas:extract': deny,\n // Requires a local project and loads its schema configuration.\n 'schemas:list': deny,\n // Loads and executes a local Studio schema.\n 'schemas:validate': deny,\n\n // Installs skills into local editor configuration directories.\n 'skills:install': deny,\n\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:disable': deny,\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:enable': deny,\n 'telemetry:status': allow,\n\n // Creates authentication credentials.\n 'tokens:create': deny,\n // Deletes authentication credentials.\n 'tokens:delete': deny,\n // Exposes authentication credential metadata.\n 'tokens:list': deny,\n\n // Loads local CLI and workbench configuration to identify the deployed Studio or application.\n undeploy: deny,\n\n // Grants a user access to a project.\n 'users:invite': deny,\n 'users:list': allow,\n\n // Reads the local project and installed package tree.\n versions: deny,\n}\n"],"names":["allow","conditionalDenyFlags","conditionalPolicy","deny","apiValidator","args","flags","fields","Array","isArray","field","fieldReadsFromHost","some","separatorIndex","indexOf","headers","header","headerOverridesAuthentication","name","slice","trim","toLowerCase","endpoint","URL","parse","urlEmbedsCredentials","username","password","mcpPolicy","api","deniedFlags","validate","build","codemod","debug","deploy","dev","doctor","exec","init","install","learn","login","logout","manage","new","preview","undeploy","versions"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/mcpPolicy.ts"],"sourcesContent":["import {\n allow,\n type CommandPolicySet,\n conditionalDenyFlags,\n conditionalPolicy,\n deny,\n} from './policy.js'\n\nfunction apiValidator({\n args,\n flags,\n}: {\n args: Readonly<Record<string, unknown>>\n flags: Readonly<Record<string, unknown>>\n}): boolean {\n const fields: unknown[] = Array.isArray(flags.field) ? flags.field : []\n\n const fieldReadsFromHost = fields.some((field) => {\n if (typeof field !== 'string') return false\n const separatorIndex = field.indexOf('=')\n return separatorIndex > 0 && field[separatorIndex + 1] === '@'\n })\n if (fieldReadsFromHost) return false\n\n const headers: unknown[] = Array.isArray(flags.header) ? flags.header : []\n\n const headerOverridesAuthentication = headers.some((header) => {\n if (typeof header !== 'string') return false\n const separatorIndex = header.indexOf(':')\n if (separatorIndex <= 0) return false\n\n const name = header.slice(0, separatorIndex).trim().toLowerCase()\n return name === 'authorization' || name === 'cookie'\n })\n\n if (headerOverridesAuthentication) return false\n\n const endpoint = typeof args.endpoint === 'string' ? URL.parse(args.endpoint) : null\n const urlEmbedsCredentials =\n endpoint !== null && (endpoint.username !== '' || endpoint.password !== '')\n\n return !urlEmbedsCredentials\n}\n\n/**\n * MCP programmatic mode disables local project/config discovery (see the CLI\n * execution context). Missing project or dataset values may therefore produce\n * a usage error, but cannot cause local filesystem access. Destructive remote\n * operations are allowed and do not by themselves make a command unsafe.\n *\n * Every manifest command must have exactly one policy here:\n * - allow: every valid invocation is safe\n * - conditional: safety depends on parsed arguments or flags\n * - deny: no invocation is safe\n */\nexport const mcpPolicy: CommandPolicySet = {\n // Special exception, this can be very dangerous but is also super useful\n // to expose. Refuse authentication overrides and host input channels:\n // `--token`, Authorization, and Cookie headers replace the MCP user's authentication,\n // URL-embedded credentials replace it with Basic authentication,\n // `--input` reads the request body from the host's filesystem or stdin, and\n // `-F key=@<file>` / `-F key=@-` field values do the same.\n api: conditionalPolicy({\n deniedFlags: ['input', 'token'],\n validate: apiValidator,\n }),\n\n // Reads a file from the machine running the command and must only run locally.\n 'assets:upload': deny,\n\n 'backups:disable': allow,\n // Writes a downloaded backup to the local filesystem.\n 'backups:download': deny,\n 'backups:enable': allow,\n 'backups:list': allow,\n\n // Requires a local Studio project and writes build output to disk.\n build: deny,\n\n // Reads and rewrites local source code.\n codemod: deny,\n\n 'cors:add': allow,\n 'cors:delete': allow,\n 'cors:list': allow,\n\n 'datasets:alias:create': allow,\n 'datasets:alias:delete': allow,\n 'datasets:alias:link': allow,\n 'datasets:alias:unlink': allow,\n 'datasets:copy': allow,\n 'datasets:create': allow,\n 'datasets:delete': allow,\n 'datasets:embeddings:disable': allow,\n 'datasets:embeddings:enable': allow,\n 'datasets:embeddings:status': allow,\n // Writes dataset contents and assets to the local filesystem.\n 'datasets:export': deny,\n // Reads import data from the local filesystem and may replace documents.\n 'datasets:import': deny,\n 'datasets:list': allow,\n 'datasets:visibility:get': allow,\n 'datasets:visibility:set': allow,\n\n // Inspects local project files and can print authentication secrets.\n debug: deny,\n\n // Reads, builds, and deploys a local Studio project.\n deploy: deny,\n\n // Loads a local Studio project and starts a development server.\n dev: deny,\n\n // Opens a browser on the machine running the MCP server.\n 'docs:browse': deny,\n // --web opens a browser on the machine running the MCP server.\n 'docs:read': conditionalDenyFlags('web'),\n 'docs:search': allow,\n\n // Reads and executes local project configuration for diagnostics.\n doctor: deny,\n\n // Reads document input from disk or launches a local editor.\n 'documents:create': deny,\n 'documents:delete': allow,\n 'documents:get': allow,\n 'documents:query': allow,\n // Loads a local Studio schema to validate documents.\n 'documents:validate': deny,\n\n // Executes arbitrary code in the local Studio context.\n exec: deny,\n\n // Loads a local schema and deploys a GraphQL API.\n 'graphql:deploy': deny,\n 'graphql:list': allow,\n // --api loads GraphQL definitions from the local project; explicit project/dataset flags are safe.\n 'graphql:undeploy': conditionalDenyFlags('api'),\n\n 'hooks:attempt': allow,\n 'hooks:create': allow,\n 'hooks:delete': allow,\n 'hooks:list': allow,\n 'hooks:logs': allow,\n\n // Creates or modifies a local project and may install dependencies.\n init: deny,\n\n // Installs packages into the local project.\n install: deny,\n\n // Opens a browser on the machine running the MCP server.\n learn: deny,\n\n // Performs an authentication flow.\n login: deny,\n\n // Performs an authentication operation and clears local credentials.\n logout: deny,\n\n // Reads local project configuration and opens a browser.\n manage: deny,\n\n // Loads local Studio configuration and writes manifest files.\n 'manifest:extract': deny,\n\n // Reads and writes local MCP client configuration.\n 'mcp:configure': deny,\n\n // Writes an aspect definition to the local filesystem.\n 'media:create-aspect': deny,\n 'media:delete-aspect': allow,\n // Reads a local aspect definition before deploying it.\n 'media:deploy-aspect': deny,\n // Writes media assets to the local filesystem.\n 'media:export': deny,\n // Reads media assets from the local filesystem.\n 'media:import': deny,\n\n // Creates migration source files in the local project.\n 'migrations:create': deny,\n // Reads and loads migration definitions from the local project.\n 'migrations:list': deny,\n // Executes local migration code that may perform arbitrary document mutations.\n 'migrations:run': deny,\n\n // Writes project setup values and scaffolds local applications.\n new: deny,\n\n // --web opens a browser on the machine running the MCP server.\n 'openapi:get': conditionalDenyFlags('web'),\n 'openapi:list': conditionalDenyFlags('web'),\n\n 'organizations:create': allow,\n 'organizations:delete': allow,\n 'organizations:get': allow,\n 'organizations:list': allow,\n 'organizations:update': allow,\n\n // Serves a local production build.\n preview: deny,\n\n 'projects:create': allow,\n 'projects:list': allow,\n // Reads locally stored claim URLs and robot tokens.\n 'projects:unclaimed': deny,\n\n // Loads the local Studio configuration to resolve the datasets containing each schema.\n 'schemas:delete': deny,\n // Loads local schema files before deploying them.\n 'schemas:deploy': deny,\n // Loads local Studio configuration and writes an extracted schema to disk.\n 'schemas:extract': deny,\n // Requires a local project and loads its schema configuration.\n 'schemas:list': deny,\n // Loads and executes a local Studio schema.\n 'schemas:validate': deny,\n\n // Installs skills into local editor configuration directories.\n 'skills:install': deny,\n\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:disable': deny,\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:enable': deny,\n 'telemetry:status': allow,\n\n // Creates authentication credentials.\n 'tokens:create': deny,\n // Deletes authentication credentials.\n 'tokens:delete': deny,\n // Exposes authentication credential metadata.\n 'tokens:list': deny,\n // Replaces authentication credentials and exposes the new secret.\n 'tokens:rotate': deny,\n\n // Reads the local schema and source files and writes generated types to the project.\n 'typegen:generate': deny,\n\n // Loads local CLI and workbench configuration to identify the deployed Studio or application.\n undeploy: deny,\n\n // Grants a user access to a project.\n 'users:invite': deny,\n 'users:list': allow,\n\n // Reads the local project and installed package tree.\n versions: deny,\n}\n"],"names":["allow","conditionalDenyFlags","conditionalPolicy","deny","apiValidator","args","flags","fields","Array","isArray","field","fieldReadsFromHost","some","separatorIndex","indexOf","headers","header","headerOverridesAuthentication","name","slice","trim","toLowerCase","endpoint","URL","parse","urlEmbedsCredentials","username","password","mcpPolicy","api","deniedFlags","validate","build","codemod","debug","deploy","dev","doctor","exec","init","install","learn","login","logout","manage","new","preview","undeploy","versions"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { Help } from '@oclif/core';
2
2
  import { getHelpFlagAdditions } from '@oclif/core/help';
3
+ import { resolveTopicAliasInArgv } from '../../topicAliases.js';
3
4
  import { isConditionalInvocationPolicy } from './commandPolicies/policy.js';
4
5
  /** Command ids a policy exposes: entries that are not denied. */ function visibleCommandIds(policySet) {
5
6
  return new Set(Object.entries(policySet).filter(([, policy])=>policy.kind !== 'deny').map(([id])=>id));
@@ -96,6 +97,9 @@ import { isConditionalInvocationPolicy } from './commandPolicies/policy.js';
96
97
  this.commandId = command.id;
97
98
  return super.showCommandHelp(withoutDeniedFlags(command, this.policySet[command.id]));
98
99
  }
100
+ async showHelp(argv) {
101
+ return super.showHelp(resolveTopicAliasInArgv(argv));
102
+ }
99
103
  async showTopicHelp(topic) {
100
104
  if (!this.topicNames.has(topic.name)) throw new NotInvokableError(topic.name);
101
105
  return super.showTopicHelp({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/exports/invokeSanityCli/help.ts"],"sourcesContent":["import {type Command, type Config, Help, type Interfaces} from '@oclif/core'\nimport {getHelpFlagAdditions} from '@oclif/core/help'\n\nimport {\n type CommandPolicy,\n type CommandPolicySet,\n isConditionalInvocationPolicy,\n} from './commandPolicies/policy.js'\n\n/** Command ids a policy exposes: entries that are not denied. */\nfunction visibleCommandIds(policySet: CommandPolicySet): Set<string> {\n return new Set(\n Object.entries(policySet)\n .filter(([, policy]) => policy.kind !== 'deny')\n .map(([id]) => id),\n )\n}\n\n/** Topic names covering the visible commands: every prefix of every id. */\nfunction visibleTopicNames(commandIds: Set<string>): Set<string> {\n const names = new Set<string>()\n for (const id of commandIds) {\n const parts = id.split(':')\n for (let length = 1; length < parts.length; length++) {\n names.add(parts.slice(0, length).join(':'))\n }\n }\n return names\n}\n\n/** Thrown when help is requested for a subject outside the policy surface. */\nclass NotInvokableError extends Error {}\n\n/**\n * A copy of `command` without the policy's denied flags, and without examples\n * that use them (under any spelling: `--web`, `--w`, `-w`), so rendered help\n * only advertises invocations the policy accepts.\n */\nfunction withoutDeniedFlags(command: Command.Loadable, policy?: CommandPolicy): Command.Loadable {\n if (!policy || !isConditionalInvocationPolicy(policy)) return {...command}\n\n const denied = policy.deniedFlags\n if (denied.length === 0) return {...command}\n\n const flags = {...command.flags}\n const spellings: string[] = []\n for (const name of denied) {\n const definition = flags[name]\n if (!definition) continue\n spellings.push(`--${name}`)\n if (definition.char) spellings.push(`-${definition.char}`)\n for (const alias of definition.aliases ?? []) spellings.push(`--${alias}`, `-${alias}`)\n delete flags[name]\n }\n\n const usesDeniedFlag = (example: Command.Example) => {\n const text = typeof example === 'string' ? example : example.command\n return spellings.some((spelling) =>\n new RegExp(String.raw`(^|\\s)${spelling}(=|\\s|$)`).test(text ?? ''),\n )\n }\n\n return {\n ...command,\n examples: command.examples?.filter((example) => !usesDeniedFlag(example)),\n flags,\n }\n}\n\n/**\n * oclif help renderer scoped to a policy's command surface. Subject\n * resolution (root vs topic vs command help) is oclif's own; the policy is\n * enforced at the rendering entry points so the rest of the CLI stays\n * invisible to embedding hosts, and listings are filtered to non-denied\n * commands and their topics only.\n *\n * Output is collected in {@link InvokableHelp.lines} instead of being written\n * to the process streams.\n */\nclass InvokableHelp extends Help {\n public commandId: string | undefined\n public readonly lines: string[] = []\n\n private readonly commandIds: Set<string>\n private readonly policySet: CommandPolicySet\n private readonly topicNames: Set<string>\n\n constructor(config: Config, policySet: CommandPolicySet) {\n super(config, {stripAnsi: true})\n this.policySet = policySet\n this.commandIds = visibleCommandIds(policySet)\n this.topicNames = visibleTopicNames(this.commandIds)\n }\n\n // The copies below are load-bearing: oclif's formatters rewrite ids/names\n // in place (`cors:list` → `cors list`). Without copies those writes corrupt\n // the shared (cached) config, breaking the policy checks on subsequent\n // invocations.\n\n protected override get sortedCommands(): Command.Loadable[] {\n return super.sortedCommands\n .filter((command) => this.commandIds.has(command.id))\n .map((command) => ({...command}))\n }\n\n protected override get sortedTopics(): Interfaces.Topic[] {\n // Topic names and descriptions come from the oclif config (oclif.config.js)\n return super.sortedTopics\n .filter((topic) => this.topicNames.has(topic.name))\n .map((topic) => ({...topic}))\n }\n\n protected override log(...args: string[]): void {\n this.lines.push(...args)\n }\n\n public override async showCommandHelp(command: Command.Loadable): Promise<void> {\n if (!this.commandIds.has(command.id)) throw new NotInvokableError(command.id)\n this.commandId = command.id\n return super.showCommandHelp(withoutDeniedFlags(command, this.policySet[command.id]))\n }\n\n protected override async showTopicHelp(topic: Interfaces.Topic): Promise<void> {\n if (!this.topicNames.has(topic.name)) throw new NotInvokableError(topic.name)\n return super.showTopicHelp({...topic})\n }\n}\n\n/**\n * Render help text for a policy's command surface: root help for a bare help\n * request, or topic/command help when `argv` names a subject (e.g.\n * `['cors', '--help']`), exactly as oclif would resolve it. ANSI styling is\n * stripped so programmatic callers get plain text.\n *\n * Returns `undefined` when the subject is unknown or denied — callers should\n * respond the same way as for an unknown command, so hosts cannot probe the\n * full CLI surface through help.\n *\n * @internal\n */\nexport async function renderInvokableHelp(\n config: Config,\n argv: string[],\n policySet: CommandPolicySet,\n): Promise<{commandId?: string; output: string} | undefined> {\n const help = new InvokableHelp(config, policySet)\n try {\n await help.showHelp(argv)\n } catch (err) {\n // Subjects outside the policy surface (NotInvokableError) and subjects\n // oclif itself cannot resolve (CLIError, marked with an `oclif` property)\n // both yield no help output.\n if (err instanceof NotInvokableError || err.oclif) return undefined\n throw err\n }\n return {\n ...(help.commandId && {commandId: help.commandId}),\n output: help.lines.join('\\n').trimEnd(),\n }\n}\n\n/**\n * Whether `argv` asks for help rather than a command invocation: a leading\n * `help` (oclif's help command), or a recognized help flag before any `--`\n * terminator. The flags come from {@link getHelpFlagAdditions} — the same\n * source oclif's own dispatch consults — so this surface recognizes exactly\n * what the regular CLI recognizes.\n *\n * @internal\n */\nexport function isHelpRequest(argv: string[], config: Config): boolean {\n if (argv[0] === 'help') return true\n const helpFlags = getHelpFlagAdditions(config)\n for (const token of argv) {\n if (token === '--') return false\n if (helpFlags.includes(token)) return true\n }\n return false\n}\n"],"names":["Help","getHelpFlagAdditions","isConditionalInvocationPolicy","visibleCommandIds","policySet","Set","Object","entries","filter","policy","kind","map","id","visibleTopicNames","commandIds","names","parts","split","length","add","slice","join","NotInvokableError","Error","withoutDeniedFlags","command","denied","deniedFlags","flags","spellings","name","definition","push","char","alias","aliases","usesDeniedFlag","example","text","some","spelling","RegExp","String","raw","test","examples","InvokableHelp","commandId","lines","topicNames","config","stripAnsi","sortedCommands","has","sortedTopics","topic","log","args","showCommandHelp","showTopicHelp","renderInvokableHelp","argv","help","showHelp","err","oclif","undefined","output","trimEnd","isHelpRequest","helpFlags","token","includes"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/exports/invokeSanityCli/help.ts"],"sourcesContent":["import {type Command, type Config, Help, type Interfaces} from '@oclif/core'\nimport {getHelpFlagAdditions} from '@oclif/core/help'\n\nimport {resolveTopicAliasInArgv} from '../../topicAliases.js'\nimport {\n type CommandPolicy,\n type CommandPolicySet,\n isConditionalInvocationPolicy,\n} from './commandPolicies/policy.js'\n\n/** Command ids a policy exposes: entries that are not denied. */\nfunction visibleCommandIds(policySet: CommandPolicySet): Set<string> {\n return new Set(\n Object.entries(policySet)\n .filter(([, policy]) => policy.kind !== 'deny')\n .map(([id]) => id),\n )\n}\n\n/** Topic names covering the visible commands: every prefix of every id. */\nfunction visibleTopicNames(commandIds: Set<string>): Set<string> {\n const names = new Set<string>()\n for (const id of commandIds) {\n const parts = id.split(':')\n for (let length = 1; length < parts.length; length++) {\n names.add(parts.slice(0, length).join(':'))\n }\n }\n return names\n}\n\n/** Thrown when help is requested for a subject outside the policy surface. */\nclass NotInvokableError extends Error {}\n\n/**\n * A copy of `command` without the policy's denied flags, and without examples\n * that use them (under any spelling: `--web`, `--w`, `-w`), so rendered help\n * only advertises invocations the policy accepts.\n */\nfunction withoutDeniedFlags(command: Command.Loadable, policy?: CommandPolicy): Command.Loadable {\n if (!policy || !isConditionalInvocationPolicy(policy)) return {...command}\n\n const denied = policy.deniedFlags\n if (denied.length === 0) return {...command}\n\n const flags = {...command.flags}\n const spellings: string[] = []\n for (const name of denied) {\n const definition = flags[name]\n if (!definition) continue\n spellings.push(`--${name}`)\n if (definition.char) spellings.push(`-${definition.char}`)\n for (const alias of definition.aliases ?? []) spellings.push(`--${alias}`, `-${alias}`)\n delete flags[name]\n }\n\n const usesDeniedFlag = (example: Command.Example) => {\n const text = typeof example === 'string' ? example : example.command\n return spellings.some((spelling) =>\n new RegExp(String.raw`(^|\\s)${spelling}(=|\\s|$)`).test(text ?? ''),\n )\n }\n\n return {\n ...command,\n examples: command.examples?.filter((example) => !usesDeniedFlag(example)),\n flags,\n }\n}\n\n/**\n * oclif help renderer scoped to a policy's command surface. Subject\n * resolution (root vs topic vs command help) is oclif's own; the policy is\n * enforced at the rendering entry points so the rest of the CLI stays\n * invisible to embedding hosts, and listings are filtered to non-denied\n * commands and their topics only.\n *\n * Output is collected in {@link InvokableHelp.lines} instead of being written\n * to the process streams.\n */\nclass InvokableHelp extends Help {\n public commandId: string | undefined\n public readonly lines: string[] = []\n\n private readonly commandIds: Set<string>\n private readonly policySet: CommandPolicySet\n private readonly topicNames: Set<string>\n\n constructor(config: Config, policySet: CommandPolicySet) {\n super(config, {stripAnsi: true})\n this.policySet = policySet\n this.commandIds = visibleCommandIds(policySet)\n this.topicNames = visibleTopicNames(this.commandIds)\n }\n\n // The copies below are load-bearing: oclif's formatters rewrite ids/names\n // in place (`cors:list` → `cors list`). Without copies those writes corrupt\n // the shared (cached) config, breaking the policy checks on subsequent\n // invocations.\n\n protected override get sortedCommands(): Command.Loadable[] {\n return super.sortedCommands\n .filter((command) => this.commandIds.has(command.id))\n .map((command) => ({...command}))\n }\n\n protected override get sortedTopics(): Interfaces.Topic[] {\n // Topic names and descriptions come from the oclif config (oclif.config.js)\n return super.sortedTopics\n .filter((topic) => this.topicNames.has(topic.name))\n .map((topic) => ({...topic}))\n }\n\n protected override log(...args: string[]): void {\n this.lines.push(...args)\n }\n\n public override async showCommandHelp(command: Command.Loadable): Promise<void> {\n if (!this.commandIds.has(command.id)) throw new NotInvokableError(command.id)\n this.commandId = command.id\n return super.showCommandHelp(withoutDeniedFlags(command, this.policySet[command.id]))\n }\n\n public override async showHelp(argv: string[]): Promise<void> {\n return super.showHelp(resolveTopicAliasInArgv(argv))\n }\n\n protected override async showTopicHelp(topic: Interfaces.Topic): Promise<void> {\n if (!this.topicNames.has(topic.name)) throw new NotInvokableError(topic.name)\n return super.showTopicHelp({...topic})\n }\n}\n\n/**\n * Render help text for a policy's command surface: root help for a bare help\n * request, or topic/command help when `argv` names a subject (e.g.\n * `['cors', '--help']`), exactly as oclif would resolve it. ANSI styling is\n * stripped so programmatic callers get plain text.\n *\n * Returns `undefined` when the subject is unknown or denied — callers should\n * respond the same way as for an unknown command, so hosts cannot probe the\n * full CLI surface through help.\n *\n * @internal\n */\nexport async function renderInvokableHelp(\n config: Config,\n argv: string[],\n policySet: CommandPolicySet,\n): Promise<{commandId?: string; output: string} | undefined> {\n const help = new InvokableHelp(config, policySet)\n try {\n await help.showHelp(argv)\n } catch (err) {\n // Subjects outside the policy surface (NotInvokableError) and subjects\n // oclif itself cannot resolve (CLIError, marked with an `oclif` property)\n // both yield no help output.\n if (err instanceof NotInvokableError || err.oclif) return undefined\n throw err\n }\n return {\n ...(help.commandId && {commandId: help.commandId}),\n output: help.lines.join('\\n').trimEnd(),\n }\n}\n\n/**\n * Whether `argv` asks for help rather than a command invocation: a leading\n * `help` (oclif's help command), or a recognized help flag before any `--`\n * terminator. The flags come from {@link getHelpFlagAdditions} — the same\n * source oclif's own dispatch consults — so this surface recognizes exactly\n * what the regular CLI recognizes.\n *\n * @internal\n */\nexport function isHelpRequest(argv: string[], config: Config): boolean {\n if (argv[0] === 'help') return true\n const helpFlags = getHelpFlagAdditions(config)\n for (const token of argv) {\n if (token === '--') return false\n if (helpFlags.includes(token)) return true\n }\n return false\n}\n"],"names":["Help","getHelpFlagAdditions","resolveTopicAliasInArgv","isConditionalInvocationPolicy","visibleCommandIds","policySet","Set","Object","entries","filter","policy","kind","map","id","visibleTopicNames","commandIds","names","parts","split","length","add","slice","join","NotInvokableError","Error","withoutDeniedFlags","command","denied","deniedFlags","flags","spellings","name","definition","push","char","alias","aliases","usesDeniedFlag","example","text","some","spelling","RegExp","String","raw","test","examples","InvokableHelp","commandId","lines","topicNames","config","stripAnsi","sortedCommands","has","sortedTopics","topic","log","args","showCommandHelp","showHelp","argv","showTopicHelp","renderInvokableHelp","help","err","oclif","undefined","output","trimEnd","isHelpRequest","helpFlags","token","includes"],"mappings":"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"}
@@ -24,6 +24,7 @@ import { getHelpFlagAdditions, normalizeArgv } from '@oclif/core/help';
24
24
  import { exitCodes } from '@sanity/cli-core';
25
25
  import { runWithCliExecutionContext } from '@sanity/cli-core/executionContext';
26
26
  import { parseArgsStringToArgv } from 'string-argv';
27
+ import { resolveTopicAliasInArgv } from '../../topicAliases.js';
27
28
  import { commandPolicies } from './commandPolicies/index.js';
28
29
  import { deny, isConditionalInvocationPolicy } from './commandPolicies/policy.js';
29
30
  import { isHelpRequest, renderInvokableHelp } from './help.js';
@@ -111,6 +112,7 @@ async function invokeSanityCliInContext({ args, source }, resolvedConfig, output
111
112
  ...args
112
113
  ];
113
114
  if (argv[0] === 'sanity') argv = argv.slice(1);
115
+ argv = resolveTopicAliasInArgv(argv);
114
116
  // Help requests are routed through oclif's help system, scoped to the
115
117
  // source's policy: root help for a bare request, topic/command help when a
116
118
  // subject is given. Denied subjects get the standard unknown-command
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/exports/invokeSanityCli/index.ts"],"sourcesContent":["/**\n * Programmatic (in-process) invocation of CLI commands, e.g. from an MCP\n * server. The invokable surface is governed by a per-source command policy\n * (see ./commandPolicies): every CLI command is explicitly allowed, denied,\n * or allowed conditionally on the parsed invocation.\n *\n * {@link invokeSanityCli} handles arg parsing, policy enforcement, command\n * dispatch, per-invocation auth, and output capture.\n * ```ts\n * import {invokeSanityCli} from '@sanity/cli/invokeSanityCli'\n *\n * const {exitCode, output} = await invokeSanityCli({\n * args: 'cors list --project-id abc123',\n * source: 'mcp',\n * token: extra.authInfo.token,\n * })\n * ```\n *\n * Help works like the regular CLI, scoped to the source's policy: `--help`\n * (or `help`) renders root help listing the invokable topics, and a subject\n * (`cors --help`, `cors list --help`) renders topic or command help.\n */\nimport {Command, Config, Parser, settings} from '@oclif/core'\nimport {getHelpFlagAdditions, normalizeArgv} from '@oclif/core/help'\nimport {exitCodes} from '@sanity/cli-core'\nimport {runWithCliExecutionContext, type SanityEnvironment} from '@sanity/cli-core/executionContext'\nimport {type SanityCommand} from '@sanity/cli-core/SanityCommand'\nimport {parseArgsStringToArgv} from 'string-argv'\n\nimport {commandPolicies} from './commandPolicies/index.js'\nimport {\n type CommandPolicySet,\n deny,\n type InvocationSource,\n isConditionalInvocationPolicy,\n} from './commandPolicies/policy.js'\nimport {isHelpRequest, renderInvokableHelp} from './help.js'\nimport {prettyPrintError} from './prettyPrintError.js'\n\ntype InvokableCommand = Pick<SanityCommand<typeof Command>, 'runInExecutionContext'>\n\n/**\n * Instantiate a policy-approved command for isolated execution.\n *\n * Every invokable command extends `SanityCommand`, whose\n * `runInExecutionContext()` runs the instance without oclif's static\n * `Command.run()` — the static runner re-enters `Config.load()` (filesystem\n * reads and a process-global config cache write) on every call. The manifest\n * types command classes as the abstract oclif `Command`, hence the\n * constructor cast. The method is probed rather than checked with\n * `instanceof`, because command modules may load from the compiled build\n * while this module runs from source, giving `SanityCommand` two identities.\n */\nfunction instantiateCommand(\n CommandClass: Command.Class,\n argv: string[],\n config: Config,\n): InvokableCommand {\n const ConcreteCommand = CommandClass as unknown as new (argv: string[], config: Config) => Command\n const command = new ConcreteCommand(argv, config) as Command & Partial<InvokableCommand>\n if (typeof command.runInExecutionContext !== 'function') {\n throw new TypeError(`Command \"${CommandClass.id}\" does not support isolated execution`)\n }\n return command as InvokableCommand\n}\n\n/**\n * Load the oclif `Config` for this package, needed to resolve, load, and run\n * commands. Loading it once and reusing it across invocations avoids\n * re-reading the command manifest per call. It only reads this package's own\n * installed files — process-lifetime initialization, not per-invocation host\n * state — so it happens outside any execution context.\n */\nfunction loadCliCommandConfig(): Promise<Config> {\n return Config.load(import.meta.url)\n}\n\nlet cachedConfig: Promise<Config> | undefined\n\nfunction unknownCommandResult(argv: string[], policySet: CommandPolicySet): InvokeSanityCliResult {\n const available = Object.entries(policySet)\n .filter(([, policy]) => policy.kind !== 'deny')\n .map(([id]) => id.replaceAll(':', ' '))\n .toSorted()\n return {\n exitCode: exitCodes.USAGE_ERROR,\n output: [\n `Unknown or unsupported command: ${argv.slice(0, 2).join(' ') || '(none)'}`,\n `Available commands: ${available.join(', ')}`,\n ].join('\\n'),\n }\n}\n\n/**\n * @internal\n */\nexport interface InvokeSanityCliOptions {\n /**\n * Arguments after `sanity` (a leading `sanity` token is tolerated), either\n * as a single string — shell-style quoting is supported, but no shell is\n * ever executed — or as a pre-split argv array.\n */\n args: string | string[]\n\n /**\n * Where this invocation originates. Selects the command policy to enforce:\n * which commands are invokable and which invocations of them are permitted.\n */\n source: InvocationSource\n\n /**\n * Auth token for this invocation. Scoped to this call via the CLI execution\n * context: it never touches process env or the process-wide token cache, so\n * concurrent invocations with different tokens are fully isolated.\n */\n token: string\n\n /**\n * Optional oclif config override (mainly for tests). Defaults to this\n * package's config, loaded once and cached across invocations.\n */\n config?: Config\n\n /**\n * Sanity deployment environment for this invocation. Scoped to this call\n * via the CLI execution context and defaults to production. The embedding\n * process's `SANITY_INTERNAL_ENV` is never consulted.\n */\n sanityEnv?: SanityEnvironment\n}\n\n/**\n * @internal\n */\nexport interface InvokeSanityCliResult {\n /** `0` on success, the command's exit code otherwise. */\n exitCode: number\n\n /** Combined stdout and stderr output, in emission order. */\n output: string\n\n /**\n * Canonical oclif command id when the invocation resolved to a command\n * exposed by the selected policy (for example, `datasets:create`).\n */\n commandId?: string\n}\n\n/**\n * Unlike a shell, string-argv keeps quotes that are glued to unquoted text:\n * `--name=\"my project\"` tokenizes with the quotes intact. Strip a matching\n * wrapping quote pair from the value side of `--flag=`-shaped tokens so the\n * common shell-style form yields the value the caller intended.\n */\nfunction stripFlagQuotes(rawToken: string): string {\n const match = /^(-{1,2}[^\\s=]+=)(['\"])([\\s\\S]*)\\2$/.exec(rawToken)\n return match ? match[1] + match[3] : rawToken\n}\n\n/**\n * Run a policy-permitted CLI command in-process and capture its result.\n *\n * Command-level failures (unknown command, bad flags, API errors) are\n * reported through `exitCode`/`output` rather than thrown, so callers can\n * relay them verbatim.\n *\n * @internal\n */\nexport async function invokeSanityCli(\n options: InvokeSanityCliOptions,\n): Promise<InvokeSanityCliResult> {\n // Always load compiled command modules. Oclif's dev-mode source resolution\n // otherwise probes tsconfigs (reading the filesystem and process.cwd) on\n // every command load, and may register ts-node process-wide.\n settings.enableAutoTranspile = false\n\n const resolvedConfig = options.config ?? (await (cachedConfig ??= loadCliCommandConfig()))\n const output: string[] = []\n const sink = (line: string) => output.push(line)\n const {sanityEnv, token} = options\n\n // Establish the isolation boundary before rendering help, loading command\n // modules, parsing, or executing. Any code reached by an external\n // invocation can therefore fail closed on context.\n return runWithCliExecutionContext({sanityEnv, stderr: sink, stdout: sink, token}, () =>\n invokeSanityCliInContext(options, resolvedConfig, output),\n )\n}\n\nasync function invokeSanityCliInContext(\n {args, source}: InvokeSanityCliOptions,\n resolvedConfig: Config,\n output: string[],\n): Promise<InvokeSanityCliResult> {\n const policySet = commandPolicies[source]\n\n // Pre-split argv arrays are taken verbatim; only string input goes through\n // shell-style tokenization and quote normalization.\n let argv =\n typeof args === 'string'\n ? parseArgsStringToArgv(args).map((t) => stripFlagQuotes(t))\n : [...args]\n if (argv[0] === 'sanity') argv = argv.slice(1)\n\n // Help requests are routed through oclif's help system, scoped to the\n // source's policy: root help for a bare request, topic/command help when a\n // subject is given. Denied subjects get the standard unknown-command\n // response (identical to a truly unknown command, so hosts can't probe the\n // full CLI surface through help), and a help request never executes a\n // command.\n if (isHelpRequest(argv, resolvedConfig)) {\n try {\n // Drop a leading `help` so the rest is the subject, mirroring how\n // oclif's dispatch consumes the token before the help command sees argv\n const helpArgv = argv[0] === 'help' ? argv.slice(1) : argv\n const result = await renderInvokableHelp(resolvedConfig, helpArgv, policySet)\n if (result) return {...result, exitCode: exitCodes.SUCCESS}\n const helpFlags = getHelpFlagAdditions(resolvedConfig)\n return unknownCommandResult(\n helpArgv.filter((token) => !helpFlags.includes(token)),\n policySet,\n )\n } catch (err) {\n return {\n exitCode: exitCodes.RUNTIME_ERROR,\n output: err instanceof Error ? err.message : String(err),\n }\n }\n }\n\n // Resolve the command id the same way oclif's dispatch would (collating\n // space-separated topics, accepting colon-separated ids as-is), then apply\n // the policy. Denied and uncategorized ids fail closed, indistinguishable\n // from commands that don't exist.\n const [commandId = '', ...commandArgv] = normalizeArgv(resolvedConfig, argv)\n const policy = policySet[commandId] ?? deny\n const commandDefinition =\n policy.kind === 'deny' ? undefined : resolvedConfig.findCommand(commandId)\n if (!commandDefinition) return unknownCommandResult(argv, policySet)\n\n const CommandClass = await commandDefinition.load()\n\n // Parse with the command's real definitions (without executing anything) so\n // conditional policies are evaluated against typed args/flags, not tokens.\n let invocation: {args: Record<string, unknown>; flags: Record<string, unknown>}\n try {\n const parsed = await Parser.parse(commandArgv, {\n args: CommandClass.args,\n baseFlags: CommandClass.baseFlags,\n enableJsonFlag: CommandClass.enableJsonFlag,\n flags: CommandClass.flags,\n strict: CommandClass.strict,\n })\n invocation = {\n args: parsed.args as Record<string, unknown>,\n flags: parsed.flags as Record<string, unknown>,\n }\n } catch (err) {\n return {\n commandId,\n exitCode: exitCodes.USAGE_ERROR,\n output: err instanceof Error ? err.message : String(err),\n }\n }\n\n if (!policy.validate(invocation)) {\n const displayId = commandId.replaceAll(':', ' ')\n let output = `This invocation of \\`${displayId}\\` is not supported here`\n\n if (isConditionalInvocationPolicy(policy)) {\n const usedDeniedFlags = policy.deniedFlags.filter(\n (name) => invocation.flags[name] !== undefined && invocation.flags[name] !== false,\n )\n if (usedDeniedFlags.length > 0) {\n output = `\\nThe ${usedDeniedFlags.map((name) => `--${name}`).join(', ')} flag is not supported here for \\`${displayId}\\``\n }\n }\n\n return {\n commandId,\n exitCode: exitCodes.USAGE_ERROR,\n output,\n }\n }\n\n try {\n const command = instantiateCommand(CommandClass, commandArgv, resolvedConfig)\n await command.runInExecutionContext()\n return {commandId, exitCode: exitCodes.SUCCESS, output: output.join('\\n')}\n } catch (err) {\n const exit = err.oclif?.exit\n\n // `this.exit(0)` throws an ExitError but is a successful outcome\n if (exit === exitCodes.SUCCESS) {\n return {commandId, exitCode: exitCodes.SUCCESS, output: output.join('\\n')}\n }\n\n const message = prettyPrintError(err) || String(err)\n if (message) output.push(message)\n return {\n commandId,\n exitCode: typeof exit === 'number' ? exit : exitCodes.RUNTIME_ERROR,\n output: output.join('\\n'),\n }\n }\n}\n"],"names":["Config","Parser","settings","getHelpFlagAdditions","normalizeArgv","exitCodes","runWithCliExecutionContext","parseArgsStringToArgv","commandPolicies","deny","isConditionalInvocationPolicy","isHelpRequest","renderInvokableHelp","prettyPrintError","instantiateCommand","CommandClass","argv","config","ConcreteCommand","command","runInExecutionContext","TypeError","id","loadCliCommandConfig","load","url","cachedConfig","unknownCommandResult","policySet","available","Object","entries","filter","policy","kind","map","replaceAll","toSorted","exitCode","USAGE_ERROR","output","slice","join","stripFlagQuotes","rawToken","match","exec","invokeSanityCli","options","enableAutoTranspile","resolvedConfig","sink","line","push","sanityEnv","token","stderr","stdout","invokeSanityCliInContext","args","source","t","helpArgv","result","SUCCESS","helpFlags","includes","err","RUNTIME_ERROR","Error","message","String","commandId","commandArgv","commandDefinition","undefined","findCommand","invocation","parsed","parse","baseFlags","enableJsonFlag","flags","strict","validate","displayId","usedDeniedFlags","deniedFlags","name","length","exit","oclif"],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,SAAiBA,MAAM,EAAEC,MAAM,EAAEC,QAAQ,QAAO,cAAa;AAC7D,SAAQC,oBAAoB,EAAEC,aAAa,QAAO,mBAAkB;AACpE,SAAQC,SAAS,QAAO,mBAAkB;AAC1C,SAAQC,0BAA0B,QAA+B,oCAAmC;AAEpG,SAAQC,qBAAqB,QAAO,cAAa;AAEjD,SAAQC,eAAe,QAAO,6BAA4B;AAC1D,SAEEC,IAAI,EAEJC,6BAA6B,QACxB,8BAA6B;AACpC,SAAQC,aAAa,EAAEC,mBAAmB,QAAO,YAAW;AAC5D,SAAQC,gBAAgB,QAAO,wBAAuB;AAItD;;;;;;;;;;;CAWC,GACD,SAASC,mBACPC,YAA2B,EAC3BC,IAAc,EACdC,MAAc;IAEd,MAAMC,kBAAkBH;IACxB,MAAMI,UAAU,IAAID,gBAAgBF,MAAMC;IAC1C,IAAI,OAAOE,QAAQC,qBAAqB,KAAK,YAAY;QACvD,MAAM,IAAIC,UAAU,CAAC,SAAS,EAAEN,aAAaO,EAAE,CAAC,qCAAqC,CAAC;IACxF;IACA,OAAOH;AACT;AAEA;;;;;;CAMC,GACD,SAASI;IACP,OAAOvB,OAAOwB,IAAI,CAAC,YAAYC,GAAG;AACpC;AAEA,IAAIC;AAEJ,SAASC,qBAAqBX,IAAc,EAAEY,SAA2B;IACvE,MAAMC,YAAYC,OAAOC,OAAO,CAACH,WAC9BI,MAAM,CAAC,CAAC,GAAGC,OAAO,GAAKA,OAAOC,IAAI,KAAK,QACvCC,GAAG,CAAC,CAAC,CAACb,GAAG,GAAKA,GAAGc,UAAU,CAAC,KAAK,MACjCC,QAAQ;IACX,OAAO;QACLC,UAAUjC,UAAUkC,WAAW;QAC/BC,QAAQ;YACN,CAAC,gCAAgC,EAAExB,KAAKyB,KAAK,CAAC,GAAG,GAAGC,IAAI,CAAC,QAAQ,UAAU;YAC3E,CAAC,oBAAoB,EAAEb,UAAUa,IAAI,CAAC,OAAO;SAC9C,CAACA,IAAI,CAAC;IACT;AACF;AAyDA;;;;;CAKC,GACD,SAASC,gBAAgBC,QAAgB;IACvC,MAAMC,QAAQ,sCAAsCC,IAAI,CAACF;IACzD,OAAOC,QAAQA,KAAK,CAAC,EAAE,GAAGA,KAAK,CAAC,EAAE,GAAGD;AACvC;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeG,gBACpBC,OAA+B;IAE/B,2EAA2E;IAC3E,yEAAyE;IACzE,6DAA6D;IAC7D9C,SAAS+C,mBAAmB,GAAG;IAE/B,MAAMC,iBAAiBF,QAAQ/B,MAAM,IAAK,MAAOS,CAAAA,iBAAiBH,sBAAqB;IACvF,MAAMiB,SAAmB,EAAE;IAC3B,MAAMW,OAAO,CAACC,OAAiBZ,OAAOa,IAAI,CAACD;IAC3C,MAAM,EAACE,SAAS,EAAEC,KAAK,EAAC,GAAGP;IAE3B,0EAA0E;IAC1E,kEAAkE;IAClE,mDAAmD;IACnD,OAAO1C,2BAA2B;QAACgD;QAAWE,QAAQL;QAAMM,QAAQN;QAAMI;IAAK,GAAG,IAChFG,yBAAyBV,SAASE,gBAAgBV;AAEtD;AAEA,eAAekB,yBACb,EAACC,IAAI,EAAEC,MAAM,EAAyB,EACtCV,cAAsB,EACtBV,MAAgB;IAEhB,MAAMZ,YAAYpB,eAAe,CAACoD,OAAO;IAEzC,2EAA2E;IAC3E,oDAAoD;IACpD,IAAI5C,OACF,OAAO2C,SAAS,WACZpD,sBAAsBoD,MAAMxB,GAAG,CAAC,CAAC0B,IAAMlB,gBAAgBkB,MACvD;WAAIF;KAAK;IACf,IAAI3C,IAAI,CAAC,EAAE,KAAK,UAAUA,OAAOA,KAAKyB,KAAK,CAAC;IAE5C,sEAAsE;IACtE,2EAA2E;IAC3E,qEAAqE;IACrE,2EAA2E;IAC3E,sEAAsE;IACtE,WAAW;IACX,IAAI9B,cAAcK,MAAMkC,iBAAiB;QACvC,IAAI;YACF,kEAAkE;YAClE,wEAAwE;YACxE,MAAMY,WAAW9C,IAAI,CAAC,EAAE,KAAK,SAASA,KAAKyB,KAAK,CAAC,KAAKzB;YACtD,MAAM+C,SAAS,MAAMnD,oBAAoBsC,gBAAgBY,UAAUlC;YACnE,IAAImC,QAAQ,OAAO;gBAAC,GAAGA,MAAM;gBAAEzB,UAAUjC,UAAU2D,OAAO;YAAA;YAC1D,MAAMC,YAAY9D,qBAAqB+C;YACvC,OAAOvB,qBACLmC,SAAS9B,MAAM,CAAC,CAACuB,QAAU,CAACU,UAAUC,QAAQ,CAACX,SAC/C3B;QAEJ,EAAE,OAAOuC,KAAK;YACZ,OAAO;gBACL7B,UAAUjC,UAAU+D,aAAa;gBACjC5B,QAAQ2B,eAAeE,QAAQF,IAAIG,OAAO,GAAGC,OAAOJ;YACtD;QACF;IACF;IAEA,wEAAwE;IACxE,2EAA2E;IAC3E,0EAA0E;IAC1E,kCAAkC;IAClC,MAAM,CAACK,YAAY,EAAE,EAAE,GAAGC,YAAY,GAAGrE,cAAc8C,gBAAgBlC;IACvE,MAAMiB,SAASL,SAAS,CAAC4C,UAAU,IAAI/D;IACvC,MAAMiE,oBACJzC,OAAOC,IAAI,KAAK,SAASyC,YAAYzB,eAAe0B,WAAW,CAACJ;IAClE,IAAI,CAACE,mBAAmB,OAAO/C,qBAAqBX,MAAMY;IAE1D,MAAMb,eAAe,MAAM2D,kBAAkBlD,IAAI;IAEjD,4EAA4E;IAC5E,2EAA2E;IAC3E,IAAIqD;IACJ,IAAI;QACF,MAAMC,SAAS,MAAM7E,OAAO8E,KAAK,CAACN,aAAa;YAC7Cd,MAAM5C,aAAa4C,IAAI;YACvBqB,WAAWjE,aAAaiE,SAAS;YACjCC,gBAAgBlE,aAAakE,cAAc;YAC3CC,OAAOnE,aAAamE,KAAK;YACzBC,QAAQpE,aAAaoE,MAAM;QAC7B;QACAN,aAAa;YACXlB,MAAMmB,OAAOnB,IAAI;YACjBuB,OAAOJ,OAAOI,KAAK;QACrB;IACF,EAAE,OAAOf,KAAK;QACZ,OAAO;YACLK;YACAlC,UAAUjC,UAAUkC,WAAW;YAC/BC,QAAQ2B,eAAeE,QAAQF,IAAIG,OAAO,GAAGC,OAAOJ;QACtD;IACF;IAEA,IAAI,CAAClC,OAAOmD,QAAQ,CAACP,aAAa;QAChC,MAAMQ,YAAYb,UAAUpC,UAAU,CAAC,KAAK;QAC5C,IAAII,SAAS,CAAC,qBAAqB,EAAE6C,UAAU,wBAAwB,CAAC;QAExE,IAAI3E,8BAA8BuB,SAAS;YACzC,MAAMqD,kBAAkBrD,OAAOsD,WAAW,CAACvD,MAAM,CAC/C,CAACwD,OAASX,WAAWK,KAAK,CAACM,KAAK,KAAKb,aAAaE,WAAWK,KAAK,CAACM,KAAK,KAAK;YAE/E,IAAIF,gBAAgBG,MAAM,GAAG,GAAG;gBAC9BjD,SAAS,CAAC,MAAM,EAAE8C,gBAAgBnD,GAAG,CAAC,CAACqD,OAAS,CAAC,EAAE,EAAEA,MAAM,EAAE9C,IAAI,CAAC,MAAM,kCAAkC,EAAE2C,UAAU,EAAE,CAAC;YAC3H;QACF;QAEA,OAAO;YACLb;YACAlC,UAAUjC,UAAUkC,WAAW;YAC/BC;QACF;IACF;IAEA,IAAI;QACF,MAAMrB,UAAUL,mBAAmBC,cAAc0D,aAAavB;QAC9D,MAAM/B,QAAQC,qBAAqB;QACnC,OAAO;YAACoD;YAAWlC,UAAUjC,UAAU2D,OAAO;YAAExB,QAAQA,OAAOE,IAAI,CAAC;QAAK;IAC3E,EAAE,OAAOyB,KAAK;QACZ,MAAMuB,OAAOvB,IAAIwB,KAAK,EAAED;QAExB,iEAAiE;QACjE,IAAIA,SAASrF,UAAU2D,OAAO,EAAE;YAC9B,OAAO;gBAACQ;gBAAWlC,UAAUjC,UAAU2D,OAAO;gBAAExB,QAAQA,OAAOE,IAAI,CAAC;YAAK;QAC3E;QAEA,MAAM4B,UAAUzD,iBAAiBsD,QAAQI,OAAOJ;QAChD,IAAIG,SAAS9B,OAAOa,IAAI,CAACiB;QACzB,OAAO;YACLE;YACAlC,UAAU,OAAOoD,SAAS,WAAWA,OAAOrF,UAAU+D,aAAa;YACnE5B,QAAQA,OAAOE,IAAI,CAAC;QACtB;IACF;AACF"}
1
+ {"version":3,"sources":["../../../src/exports/invokeSanityCli/index.ts"],"sourcesContent":["/**\n * Programmatic (in-process) invocation of CLI commands, e.g. from an MCP\n * server. The invokable surface is governed by a per-source command policy\n * (see ./commandPolicies): every CLI command is explicitly allowed, denied,\n * or allowed conditionally on the parsed invocation.\n *\n * {@link invokeSanityCli} handles arg parsing, policy enforcement, command\n * dispatch, per-invocation auth, and output capture.\n * ```ts\n * import {invokeSanityCli} from '@sanity/cli/invokeSanityCli'\n *\n * const {exitCode, output} = await invokeSanityCli({\n * args: 'cors list --project-id abc123',\n * source: 'mcp',\n * token: extra.authInfo.token,\n * })\n * ```\n *\n * Help works like the regular CLI, scoped to the source's policy: `--help`\n * (or `help`) renders root help listing the invokable topics, and a subject\n * (`cors --help`, `cors list --help`) renders topic or command help.\n */\nimport {Command, Config, Parser, settings} from '@oclif/core'\nimport {getHelpFlagAdditions, normalizeArgv} from '@oclif/core/help'\nimport {exitCodes} from '@sanity/cli-core'\nimport {runWithCliExecutionContext, type SanityEnvironment} from '@sanity/cli-core/executionContext'\nimport {type SanityCommand} from '@sanity/cli-core/SanityCommand'\nimport {parseArgsStringToArgv} from 'string-argv'\n\nimport {resolveTopicAliasInArgv} from '../../topicAliases.js'\nimport {commandPolicies} from './commandPolicies/index.js'\nimport {\n type CommandPolicySet,\n deny,\n type InvocationSource,\n isConditionalInvocationPolicy,\n} from './commandPolicies/policy.js'\nimport {isHelpRequest, renderInvokableHelp} from './help.js'\nimport {prettyPrintError} from './prettyPrintError.js'\n\ntype InvokableCommand = Pick<SanityCommand<typeof Command>, 'runInExecutionContext'>\n\n/**\n * Instantiate a policy-approved command for isolated execution.\n *\n * Every invokable command extends `SanityCommand`, whose\n * `runInExecutionContext()` runs the instance without oclif's static\n * `Command.run()` — the static runner re-enters `Config.load()` (filesystem\n * reads and a process-global config cache write) on every call. The manifest\n * types command classes as the abstract oclif `Command`, hence the\n * constructor cast. The method is probed rather than checked with\n * `instanceof`, because command modules may load from the compiled build\n * while this module runs from source, giving `SanityCommand` two identities.\n */\nfunction instantiateCommand(\n CommandClass: Command.Class,\n argv: string[],\n config: Config,\n): InvokableCommand {\n const ConcreteCommand = CommandClass as unknown as new (argv: string[], config: Config) => Command\n const command = new ConcreteCommand(argv, config) as Command & Partial<InvokableCommand>\n if (typeof command.runInExecutionContext !== 'function') {\n throw new TypeError(`Command \"${CommandClass.id}\" does not support isolated execution`)\n }\n return command as InvokableCommand\n}\n\n/**\n * Load the oclif `Config` for this package, needed to resolve, load, and run\n * commands. Loading it once and reusing it across invocations avoids\n * re-reading the command manifest per call. It only reads this package's own\n * installed files — process-lifetime initialization, not per-invocation host\n * state — so it happens outside any execution context.\n */\nfunction loadCliCommandConfig(): Promise<Config> {\n return Config.load(import.meta.url)\n}\n\nlet cachedConfig: Promise<Config> | undefined\n\nfunction unknownCommandResult(argv: string[], policySet: CommandPolicySet): InvokeSanityCliResult {\n const available = Object.entries(policySet)\n .filter(([, policy]) => policy.kind !== 'deny')\n .map(([id]) => id.replaceAll(':', ' '))\n .toSorted()\n return {\n exitCode: exitCodes.USAGE_ERROR,\n output: [\n `Unknown or unsupported command: ${argv.slice(0, 2).join(' ') || '(none)'}`,\n `Available commands: ${available.join(', ')}`,\n ].join('\\n'),\n }\n}\n\n/**\n * @internal\n */\nexport interface InvokeSanityCliOptions {\n /**\n * Arguments after `sanity` (a leading `sanity` token is tolerated), either\n * as a single string — shell-style quoting is supported, but no shell is\n * ever executed — or as a pre-split argv array.\n */\n args: string | string[]\n\n /**\n * Where this invocation originates. Selects the command policy to enforce:\n * which commands are invokable and which invocations of them are permitted.\n */\n source: InvocationSource\n\n /**\n * Auth token for this invocation. Scoped to this call via the CLI execution\n * context: it never touches process env or the process-wide token cache, so\n * concurrent invocations with different tokens are fully isolated.\n */\n token: string\n\n /**\n * Optional oclif config override (mainly for tests). Defaults to this\n * package's config, loaded once and cached across invocations.\n */\n config?: Config\n\n /**\n * Sanity deployment environment for this invocation. Scoped to this call\n * via the CLI execution context and defaults to production. The embedding\n * process's `SANITY_INTERNAL_ENV` is never consulted.\n */\n sanityEnv?: SanityEnvironment\n}\n\n/**\n * @internal\n */\nexport interface InvokeSanityCliResult {\n /** `0` on success, the command's exit code otherwise. */\n exitCode: number\n\n /** Combined stdout and stderr output, in emission order. */\n output: string\n\n /**\n * Canonical oclif command id when the invocation resolved to a command\n * exposed by the selected policy (for example, `datasets:create`).\n */\n commandId?: string\n}\n\n/**\n * Unlike a shell, string-argv keeps quotes that are glued to unquoted text:\n * `--name=\"my project\"` tokenizes with the quotes intact. Strip a matching\n * wrapping quote pair from the value side of `--flag=`-shaped tokens so the\n * common shell-style form yields the value the caller intended.\n */\nfunction stripFlagQuotes(rawToken: string): string {\n const match = /^(-{1,2}[^\\s=]+=)(['\"])([\\s\\S]*)\\2$/.exec(rawToken)\n return match ? match[1] + match[3] : rawToken\n}\n\n/**\n * Run a policy-permitted CLI command in-process and capture its result.\n *\n * Command-level failures (unknown command, bad flags, API errors) are\n * reported through `exitCode`/`output` rather than thrown, so callers can\n * relay them verbatim.\n *\n * @internal\n */\nexport async function invokeSanityCli(\n options: InvokeSanityCliOptions,\n): Promise<InvokeSanityCliResult> {\n // Always load compiled command modules. Oclif's dev-mode source resolution\n // otherwise probes tsconfigs (reading the filesystem and process.cwd) on\n // every command load, and may register ts-node process-wide.\n settings.enableAutoTranspile = false\n\n const resolvedConfig = options.config ?? (await (cachedConfig ??= loadCliCommandConfig()))\n const output: string[] = []\n const sink = (line: string) => output.push(line)\n const {sanityEnv, token} = options\n\n // Establish the isolation boundary before rendering help, loading command\n // modules, parsing, or executing. Any code reached by an external\n // invocation can therefore fail closed on context.\n return runWithCliExecutionContext({sanityEnv, stderr: sink, stdout: sink, token}, () =>\n invokeSanityCliInContext(options, resolvedConfig, output),\n )\n}\n\nasync function invokeSanityCliInContext(\n {args, source}: InvokeSanityCliOptions,\n resolvedConfig: Config,\n output: string[],\n): Promise<InvokeSanityCliResult> {\n const policySet = commandPolicies[source]\n\n // Pre-split argv arrays are taken verbatim; only string input goes through\n // shell-style tokenization and quote normalization.\n let argv =\n typeof args === 'string'\n ? parseArgsStringToArgv(args).map((t) => stripFlagQuotes(t))\n : [...args]\n if (argv[0] === 'sanity') argv = argv.slice(1)\n argv = resolveTopicAliasInArgv(argv)\n\n // Help requests are routed through oclif's help system, scoped to the\n // source's policy: root help for a bare request, topic/command help when a\n // subject is given. Denied subjects get the standard unknown-command\n // response (identical to a truly unknown command, so hosts can't probe the\n // full CLI surface through help), and a help request never executes a\n // command.\n if (isHelpRequest(argv, resolvedConfig)) {\n try {\n // Drop a leading `help` so the rest is the subject, mirroring how\n // oclif's dispatch consumes the token before the help command sees argv\n const helpArgv = argv[0] === 'help' ? argv.slice(1) : argv\n const result = await renderInvokableHelp(resolvedConfig, helpArgv, policySet)\n if (result) return {...result, exitCode: exitCodes.SUCCESS}\n const helpFlags = getHelpFlagAdditions(resolvedConfig)\n return unknownCommandResult(\n helpArgv.filter((token) => !helpFlags.includes(token)),\n policySet,\n )\n } catch (err) {\n return {\n exitCode: exitCodes.RUNTIME_ERROR,\n output: err instanceof Error ? err.message : String(err),\n }\n }\n }\n\n // Resolve the command id the same way oclif's dispatch would (collating\n // space-separated topics, accepting colon-separated ids as-is), then apply\n // the policy. Denied and uncategorized ids fail closed, indistinguishable\n // from commands that don't exist.\n const [commandId = '', ...commandArgv] = normalizeArgv(resolvedConfig, argv)\n const policy = policySet[commandId] ?? deny\n const commandDefinition =\n policy.kind === 'deny' ? undefined : resolvedConfig.findCommand(commandId)\n if (!commandDefinition) return unknownCommandResult(argv, policySet)\n\n const CommandClass = await commandDefinition.load()\n\n // Parse with the command's real definitions (without executing anything) so\n // conditional policies are evaluated against typed args/flags, not tokens.\n let invocation: {args: Record<string, unknown>; flags: Record<string, unknown>}\n try {\n const parsed = await Parser.parse(commandArgv, {\n args: CommandClass.args,\n baseFlags: CommandClass.baseFlags,\n enableJsonFlag: CommandClass.enableJsonFlag,\n flags: CommandClass.flags,\n strict: CommandClass.strict,\n })\n invocation = {\n args: parsed.args as Record<string, unknown>,\n flags: parsed.flags as Record<string, unknown>,\n }\n } catch (err) {\n return {\n commandId,\n exitCode: exitCodes.USAGE_ERROR,\n output: err instanceof Error ? err.message : String(err),\n }\n }\n\n if (!policy.validate(invocation)) {\n const displayId = commandId.replaceAll(':', ' ')\n let output = `This invocation of \\`${displayId}\\` is not supported here`\n\n if (isConditionalInvocationPolicy(policy)) {\n const usedDeniedFlags = policy.deniedFlags.filter(\n (name) => invocation.flags[name] !== undefined && invocation.flags[name] !== false,\n )\n if (usedDeniedFlags.length > 0) {\n output = `\\nThe ${usedDeniedFlags.map((name) => `--${name}`).join(', ')} flag is not supported here for \\`${displayId}\\``\n }\n }\n\n return {\n commandId,\n exitCode: exitCodes.USAGE_ERROR,\n output,\n }\n }\n\n try {\n const command = instantiateCommand(CommandClass, commandArgv, resolvedConfig)\n await command.runInExecutionContext()\n return {commandId, exitCode: exitCodes.SUCCESS, output: output.join('\\n')}\n } catch (err) {\n const exit = err.oclif?.exit\n\n // `this.exit(0)` throws an ExitError but is a successful outcome\n if (exit === exitCodes.SUCCESS) {\n return {commandId, exitCode: exitCodes.SUCCESS, output: output.join('\\n')}\n }\n\n const message = prettyPrintError(err) || String(err)\n if (message) output.push(message)\n return {\n commandId,\n exitCode: typeof exit === 'number' ? exit : exitCodes.RUNTIME_ERROR,\n output: output.join('\\n'),\n }\n }\n}\n"],"names":["Config","Parser","settings","getHelpFlagAdditions","normalizeArgv","exitCodes","runWithCliExecutionContext","parseArgsStringToArgv","resolveTopicAliasInArgv","commandPolicies","deny","isConditionalInvocationPolicy","isHelpRequest","renderInvokableHelp","prettyPrintError","instantiateCommand","CommandClass","argv","config","ConcreteCommand","command","runInExecutionContext","TypeError","id","loadCliCommandConfig","load","url","cachedConfig","unknownCommandResult","policySet","available","Object","entries","filter","policy","kind","map","replaceAll","toSorted","exitCode","USAGE_ERROR","output","slice","join","stripFlagQuotes","rawToken","match","exec","invokeSanityCli","options","enableAutoTranspile","resolvedConfig","sink","line","push","sanityEnv","token","stderr","stdout","invokeSanityCliInContext","args","source","t","helpArgv","result","SUCCESS","helpFlags","includes","err","RUNTIME_ERROR","Error","message","String","commandId","commandArgv","commandDefinition","undefined","findCommand","invocation","parsed","parse","baseFlags","enableJsonFlag","flags","strict","validate","displayId","usedDeniedFlags","deniedFlags","name","length","exit","oclif"],"mappings":"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"}
@@ -13,53 +13,26 @@
13
13
  * 3. If no: fall back to oclif plugin-not-found's "did you mean?" behavior
14
14
  */ import { toStandardizedId } from '@oclif/core';
15
15
  import { subdebug } from '@sanity/cli-core';
16
- import { topicAliases } from '../../topicAliases.js';
16
+ import { resolveTopicAlias } from '../../topicAliases.js';
17
17
  const debug = subdebug('hooks:topicAliases');
18
- // Build a lookup from alias names to candidate topics.
19
- // This hook only fires for names oclif doesn't recognize. Since canonical
20
- // topics (eg "datasets") are registered and resolved by oclif directly,
21
- // only aliases (eg "dataset") reach here. We still map both directions
22
- // so the hook is resilient to future renames or plugin-provided topics
23
- // where the canonical name may not be a registered topic.
24
- const topicMappings = new Map();
25
- for (const [canonical, aliases] of Object.entries(topicAliases)){
26
- // canonical -> all aliases
27
- topicMappings.set(canonical, aliases);
28
- // each alias -> [canonical]
29
- for (const alias of aliases){
30
- topicMappings.set(alias, [
31
- canonical
32
- ]);
33
- }
34
- }
35
18
  const hook = async function(opts) {
36
19
  const { config, id } = opts;
37
20
  // Standardize the ID (handles topic separator differences)
38
21
  const standardId = toStandardizedId(id, config);
39
22
  const parts = standardId.split(':');
40
- const topic = parts[0];
41
- const candidates = topicMappings.get(topic);
42
- if (candidates) {
43
- // Find which candidate topic actually has commands registered
44
- const resolvedTopic = candidates.find((candidate)=>parts.length === 1 ? config.findTopic(candidate) : config.findCommand([
45
- candidate,
46
- ...parts.slice(1)
47
- ].join(':')));
48
- if (resolvedTopic) {
49
- debug('Rewriting topic: %s -> %s', topic, resolvedTopic);
50
- // Bare topic (eg "sanity dataset" with no subcommand) -> show topic help
51
- if (parts.length === 1) {
52
- return config.runCommand('help', [
53
- resolvedTopic
54
- ]);
55
- }
56
- // Full command (eg "sanity dataset create") -> run the resolved command
57
- const rewrittenId = [
58
- resolvedTopic,
59
- ...parts.slice(1)
60
- ].join(':');
61
- return config.runCommand(rewrittenId, opts.argv ?? []);
23
+ const hasTopic = parts.length === 1;
24
+ const rewrittenId = resolveTopicAlias(standardId);
25
+ const aliasExists = rewrittenId && (hasTopic ? config.findTopic(rewrittenId) : config.findCommand(rewrittenId));
26
+ if (aliasExists) {
27
+ debug('Rewriting topic: %s -> %s', parts[0], rewrittenId.split(':')[0]);
28
+ // Bare topic (eg "sanity dataset" with no subcommand) -> show topic help
29
+ if (hasTopic) {
30
+ return config.runCommand('help', [
31
+ rewrittenId
32
+ ]);
62
33
  }
34
+ // Full command (eg "sanity dataset create") -> run the resolved command
35
+ return config.runCommand(rewrittenId, opts.argv ?? []);
63
36
  }
64
37
  // No alias match - fall back to oclif plugin-not-found's "did you mean?" behavior
65
38
  // eslint-disable-next-line no-restricted-syntax -- package import, not a file path
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/commandNotFound/topicAliases.ts"],"sourcesContent":["/**\n * Hook: Topic Alias Resolution + Command Not Found\n *\n * Replaces oclif plugin-not-found as a plugin, calling it as a fallback instead.\n * This avoids a race condition where both hooks run concurrently via Promise.all.\n *\n * This hook only fires for names oclif doesn't recognize. Registered topics\n * and commands (including hiddenAliases) are resolved by oclif before this runs.\n *\n * Flow:\n * 1. Check if the unrecognized name is a known alias (eg \"dataset\" for \"datasets\")\n * 2. If yes: rewrite and run the resolved command (or show topic help for bare topics)\n * 3. If no: fall back to oclif plugin-not-found's \"did you mean?\" behavior\n */\n\nimport {type Hook, toStandardizedId} from '@oclif/core'\nimport {subdebug} from '@sanity/cli-core'\n\nimport {topicAliases} from '../../topicAliases.js'\n\nconst debug = subdebug('hooks:topicAliases')\n\n// Build a lookup from alias names to candidate topics.\n// This hook only fires for names oclif doesn't recognize. Since canonical\n// topics (eg \"datasets\") are registered and resolved by oclif directly,\n// only aliases (eg \"dataset\") reach here. We still map both directions\n// so the hook is resilient to future renames or plugin-provided topics\n// where the canonical name may not be a registered topic.\nconst topicMappings = new Map<string, string[]>()\nfor (const [canonical, aliases] of Object.entries(topicAliases)) {\n // canonical -> all aliases\n topicMappings.set(canonical, aliases)\n // each alias -> [canonical]\n for (const alias of aliases) {\n topicMappings.set(alias, [canonical])\n }\n}\n\nconst hook: Hook.CommandNotFound = async function (opts) {\n const {config, id} = opts\n\n // Standardize the ID (handles topic separator differences)\n const standardId = toStandardizedId(id, config)\n const parts = standardId.split(':')\n const topic = parts[0]\n\n const candidates = topicMappings.get(topic)\n if (candidates) {\n // Find which candidate topic actually has commands registered\n const resolvedTopic = candidates.find((candidate) =>\n parts.length === 1\n ? config.findTopic(candidate)\n : config.findCommand([candidate, ...parts.slice(1)].join(':')),\n )\n\n if (resolvedTopic) {\n debug('Rewriting topic: %s -> %s', topic, resolvedTopic)\n\n // Bare topic (eg \"sanity dataset\" with no subcommand) -> show topic help\n if (parts.length === 1) {\n return config.runCommand('help', [resolvedTopic])\n }\n\n // Full command (eg \"sanity dataset create\") -> run the resolved command\n const rewrittenId = [resolvedTopic, ...parts.slice(1)].join(':')\n return config.runCommand(rewrittenId, opts.argv ?? [])\n }\n }\n\n // No alias match - fall back to oclif plugin-not-found's \"did you mean?\" behavior\n // eslint-disable-next-line no-restricted-syntax -- package import, not a file path\n const {default: notFoundHook} = await import('@oclif/plugin-not-found')\n return notFoundHook.call(this, opts)\n}\n\nexport default hook\n"],"names":["toStandardizedId","subdebug","topicAliases","debug","topicMappings","Map","canonical","aliases","Object","entries","set","alias","hook","opts","config","id","standardId","parts","split","topic","candidates","get","resolvedTopic","find","candidate","length","findTopic","findCommand","slice","join","runCommand","rewrittenId","argv","default","notFoundHook","call"],"mappings":"AAAA;;;;;;;;;;;;;CAaC,GAED,SAAmBA,gBAAgB,QAAO,cAAa;AACvD,SAAQC,QAAQ,QAAO,mBAAkB;AAEzC,SAAQC,YAAY,QAAO,wBAAuB;AAElD,MAAMC,QAAQF,SAAS;AAEvB,uDAAuD;AACvD,0EAA0E;AAC1E,wEAAwE;AACxE,uEAAuE;AACvE,uEAAuE;AACvE,0DAA0D;AAC1D,MAAMG,gBAAgB,IAAIC;AAC1B,KAAK,MAAM,CAACC,WAAWC,QAAQ,IAAIC,OAAOC,OAAO,CAACP,cAAe;IAC/D,2BAA2B;IAC3BE,cAAcM,GAAG,CAACJ,WAAWC;IAC7B,4BAA4B;IAC5B,KAAK,MAAMI,SAASJ,QAAS;QAC3BH,cAAcM,GAAG,CAACC,OAAO;YAACL;SAAU;IACtC;AACF;AAEA,MAAMM,OAA6B,eAAgBC,IAAI;IACrD,MAAM,EAACC,MAAM,EAAEC,EAAE,EAAC,GAAGF;IAErB,2DAA2D;IAC3D,MAAMG,aAAahB,iBAAiBe,IAAID;IACxC,MAAMG,QAAQD,WAAWE,KAAK,CAAC;IAC/B,MAAMC,QAAQF,KAAK,CAAC,EAAE;IAEtB,MAAMG,aAAahB,cAAciB,GAAG,CAACF;IACrC,IAAIC,YAAY;QACd,8DAA8D;QAC9D,MAAME,gBAAgBF,WAAWG,IAAI,CAAC,CAACC,YACrCP,MAAMQ,MAAM,KAAK,IACbX,OAAOY,SAAS,CAACF,aACjBV,OAAOa,WAAW,CAAC;gBAACH;mBAAcP,MAAMW,KAAK,CAAC;aAAG,CAACC,IAAI,CAAC;QAG7D,IAAIP,eAAe;YACjBnB,MAAM,6BAA6BgB,OAAOG;YAE1C,yEAAyE;YACzE,IAAIL,MAAMQ,MAAM,KAAK,GAAG;gBACtB,OAAOX,OAAOgB,UAAU,CAAC,QAAQ;oBAACR;iBAAc;YAClD;YAEA,wEAAwE;YACxE,MAAMS,cAAc;gBAACT;mBAAkBL,MAAMW,KAAK,CAAC;aAAG,CAACC,IAAI,CAAC;YAC5D,OAAOf,OAAOgB,UAAU,CAACC,aAAalB,KAAKmB,IAAI,IAAI,EAAE;QACvD;IACF;IAEA,kFAAkF;IAClF,mFAAmF;IACnF,MAAM,EAACC,SAASC,YAAY,EAAC,GAAG,MAAM,MAAM,CAAC;IAC7C,OAAOA,aAAaC,IAAI,CAAC,IAAI,EAAEtB;AACjC;AAEA,eAAeD,KAAI"}
1
+ {"version":3,"sources":["../../../src/hooks/commandNotFound/topicAliases.ts"],"sourcesContent":["/**\n * Hook: Topic Alias Resolution + Command Not Found\n *\n * Replaces oclif plugin-not-found as a plugin, calling it as a fallback instead.\n * This avoids a race condition where both hooks run concurrently via Promise.all.\n *\n * This hook only fires for names oclif doesn't recognize. Registered topics\n * and commands (including hiddenAliases) are resolved by oclif before this runs.\n *\n * Flow:\n * 1. Check if the unrecognized name is a known alias (eg \"dataset\" for \"datasets\")\n * 2. If yes: rewrite and run the resolved command (or show topic help for bare topics)\n * 3. If no: fall back to oclif plugin-not-found's \"did you mean?\" behavior\n */\n\nimport {type Hook, toStandardizedId} from '@oclif/core'\nimport {subdebug} from '@sanity/cli-core'\n\nimport {resolveTopicAlias} from '../../topicAliases.js'\n\nconst debug = subdebug('hooks:topicAliases')\n\nconst hook: Hook.CommandNotFound = async function (opts) {\n const {config, id} = opts\n\n // Standardize the ID (handles topic separator differences)\n const standardId = toStandardizedId(id, config)\n const parts = standardId.split(':')\n const hasTopic = parts.length === 1\n const rewrittenId = resolveTopicAlias(standardId)\n\n const aliasExists =\n rewrittenId && (hasTopic ? config.findTopic(rewrittenId) : config.findCommand(rewrittenId))\n if (aliasExists) {\n debug('Rewriting topic: %s -> %s', parts[0], rewrittenId.split(':')[0])\n\n // Bare topic (eg \"sanity dataset\" with no subcommand) -> show topic help\n if (hasTopic) {\n return config.runCommand('help', [rewrittenId])\n }\n\n // Full command (eg \"sanity dataset create\") -> run the resolved command\n return config.runCommand(rewrittenId, opts.argv ?? [])\n }\n\n // No alias match - fall back to oclif plugin-not-found's \"did you mean?\" behavior\n // eslint-disable-next-line no-restricted-syntax -- package import, not a file path\n const {default: notFoundHook} = await import('@oclif/plugin-not-found')\n return notFoundHook.call(this, opts)\n}\n\nexport default hook\n"],"names":["toStandardizedId","subdebug","resolveTopicAlias","debug","hook","opts","config","id","standardId","parts","split","hasTopic","length","rewrittenId","aliasExists","findTopic","findCommand","runCommand","argv","default","notFoundHook","call"],"mappings":"AAAA;;;;;;;;;;;;;CAaC,GAED,SAAmBA,gBAAgB,QAAO,cAAa;AACvD,SAAQC,QAAQ,QAAO,mBAAkB;AAEzC,SAAQC,iBAAiB,QAAO,wBAAuB;AAEvD,MAAMC,QAAQF,SAAS;AAEvB,MAAMG,OAA6B,eAAgBC,IAAI;IACrD,MAAM,EAACC,MAAM,EAAEC,EAAE,EAAC,GAAGF;IAErB,2DAA2D;IAC3D,MAAMG,aAAaR,iBAAiBO,IAAID;IACxC,MAAMG,QAAQD,WAAWE,KAAK,CAAC;IAC/B,MAAMC,WAAWF,MAAMG,MAAM,KAAK;IAClC,MAAMC,cAAcX,kBAAkBM;IAEtC,MAAMM,cACJD,eAAgBF,CAAAA,WAAWL,OAAOS,SAAS,CAACF,eAAeP,OAAOU,WAAW,CAACH,YAAW;IAC3F,IAAIC,aAAa;QACfX,MAAM,6BAA6BM,KAAK,CAAC,EAAE,EAAEI,YAAYH,KAAK,CAAC,IAAI,CAAC,EAAE;QAEtE,yEAAyE;QACzE,IAAIC,UAAU;YACZ,OAAOL,OAAOW,UAAU,CAAC,QAAQ;gBAACJ;aAAY;QAChD;QAEA,wEAAwE;QACxE,OAAOP,OAAOW,UAAU,CAACJ,aAAaR,KAAKa,IAAI,IAAI,EAAE;IACvD;IAEA,kFAAkF;IAClF,mFAAmF;IACnF,MAAM,EAACC,SAASC,YAAY,EAAC,GAAG,MAAM,MAAM,CAAC;IAC7C,OAAOA,aAAaC,IAAI,CAAC,IAAI,EAAEhB;AACjC;AAEA,eAAeD,KAAI"}
@@ -9,7 +9,7 @@ import { CliCommandTelemetry } from '../../telemetry/cli.telemetry.js';
9
9
  import { detectRuntime } from '../../util/detectRuntime.js';
10
10
  import { parseArguments } from '../../util/parseArguments.js';
11
11
  import { createTelemetryStore } from '../../util/telemetry/createTelemetryStore.js';
12
- export const setupTelemetry = async function({ config }) {
12
+ export const setupTelemetry = async function({ Command, config }) {
13
13
  // Show telemetry disclosure
14
14
  telemetryDisclosure({
15
15
  logToStderr: (message)=>process.stderr.write(`${message}\n`)
@@ -34,7 +34,8 @@ export const setupTelemetry = async function({ config }) {
34
34
  runtime: detectRuntime(),
35
35
  runtimeVersion: process.version
36
36
  });
37
- const args = parseArguments();
37
+ const commandTelemetry = Command?.telemetry;
38
+ const args = parseArguments(process.argv, commandTelemetry);
38
39
  const traceOptions = {
39
40
  commandArguments: args.argsWithoutOptions,
40
41
  coreOptions: {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/hooks/prerun/setupTelemetry.ts"],"sourcesContent":["import {spawn} from 'node:child_process'\nimport {fileURLToPath} from 'node:url'\n\nimport {type Hook} from '@oclif/core'\nimport {\n type CliConfig,\n debug,\n findProjectRoot,\n getCliConfig,\n setCliTelemetry,\n} from '@sanity/cli-core'\nimport {createSessionId} from '@sanity/telemetry'\n\nimport {resolveConsent} from '../../actions/telemetry/resolveConsent.js'\nimport {telemetryDebug} from '../../actions/telemetry/telemetryDebug.js'\nimport {telemetryDisclosure} from '../../actions/telemetry/telemetryDisclosure.js'\nimport {CliCommandTelemetry, type CLITraceData} from '../../telemetry/cli.telemetry.js'\nimport {detectRuntime} from '../../util/detectRuntime.js'\nimport {parseArguments} from '../../util/parseArguments.js'\nimport {createTelemetryStore} from '../../util/telemetry/createTelemetryStore.js'\n\nexport const setupTelemetry: Hook.Prerun = async function ({config}) {\n // Show telemetry disclosure\n telemetryDisclosure({logToStderr: (message) => process.stderr.write(`${message}\\n`)})\n\n const sessionId = createSessionId()\n\n const telemetry = createTelemetryStore(sessionId, {\n resolveConsent,\n })\n\n let cliConfig: CliConfig | undefined\n try {\n const projectRoot = await findProjectRoot(process.cwd())\n cliConfig = await getCliConfig(projectRoot.directory)\n } catch {\n // Accept not finding a project root and/or CLI config\n }\n\n telemetry.updateUserProperties({\n cliVersion: config.version,\n cpuArchitecture: process.arch,\n dataset: cliConfig?.api?.dataset,\n machinePlatform: process.platform,\n projectId: cliConfig?.api?.projectId,\n runtime: detectRuntime(),\n runtimeVersion: process.version,\n })\n\n const args = parseArguments()\n\n const traceOptions: CLITraceData = {\n commandArguments: args.argsWithoutOptions,\n coreOptions: {\n debug: args.coreOptions.debug ?? undefined,\n help: args.coreOptions.help ?? undefined,\n version: args.coreOptions.version ?? undefined,\n },\n extraArguments: args.extraArguments,\n groupOrCommand: args.groupOrCommand,\n }\n\n telemetryDebug('Starting command trace', traceOptions)\n\n const cliCommandTrace = telemetry.trace(CliCommandTelemetry, traceOptions)\n cliCommandTrace.start()\n\n // Set the global telemetry store and trace error reporter\n setCliTelemetry(cliCommandTrace.newContext(args.groupOrCommand), {\n reportTraceError: (error) => cliCommandTrace.error(error),\n })\n\n // Handle process exit - complete trace on success, spawn worker to flush telemetry.\n // On non-zero exit, the trace is either already errored (from SanityCommand.catch)\n // or it was a user abort (SIGINT/ctrl+c) — left incomplete, matching old CLI behavior.\n process.once('exit', (status) => {\n if (status === 0) {\n cliCommandTrace.complete()\n }\n\n const workerPath = fileURLToPath(new URL('flushTelemetry.worker.js', import.meta.url))\n telemetryDebug(`Spawning \"${process.execPath} ${workerPath}\"`)\n\n // Spawn detached worker to flush all telemetry files\n // unref will ensure the child process can keep doing work even after the parent process exits\n spawn(process.execPath, [workerPath], {\n detached: true,\n env: {\n ...process.env,\n SANITY_TELEMETRY_PROJECT_ID: cliConfig?.api?.projectId || '',\n },\n // If debug is enabled, spawn the worker with stdio inherit to see the output\n stdio: debug.enabled || telemetryDebug.enabled ? 'inherit' : 'ignore',\n }).unref()\n })\n}\n"],"names":["spawn","fileURLToPath","debug","findProjectRoot","getCliConfig","setCliTelemetry","createSessionId","resolveConsent","telemetryDebug","telemetryDisclosure","CliCommandTelemetry","detectRuntime","parseArguments","createTelemetryStore","setupTelemetry","config","logToStderr","message","process","stderr","write","sessionId","telemetry","cliConfig","projectRoot","cwd","directory","updateUserProperties","cliVersion","version","cpuArchitecture","arch","dataset","api","machinePlatform","platform","projectId","runtime","runtimeVersion","args","traceOptions","commandArguments","argsWithoutOptions","coreOptions","undefined","help","extraArguments","groupOrCommand","cliCommandTrace","trace","start","newContext","reportTraceError","error","once","status","complete","workerPath","URL","url","execPath","detached","env","SANITY_TELEMETRY_PROJECT_ID","stdio","enabled","unref"],"mappings":"AAAA,SAAQA,KAAK,QAAO,qBAAoB;AACxC,SAAQC,aAAa,QAAO,WAAU;AAGtC,SAEEC,KAAK,EACLC,eAAe,EACfC,YAAY,EACZC,eAAe,QACV,mBAAkB;AACzB,SAAQC,eAAe,QAAO,oBAAmB;AAEjD,SAAQC,cAAc,QAAO,4CAA2C;AACxE,SAAQC,cAAc,QAAO,4CAA2C;AACxE,SAAQC,mBAAmB,QAAO,iDAAgD;AAClF,SAAQC,mBAAmB,QAA0B,mCAAkC;AACvF,SAAQC,aAAa,QAAO,8BAA6B;AACzD,SAAQC,cAAc,QAAO,+BAA8B;AAC3D,SAAQC,oBAAoB,QAAO,+CAA8C;AAEjF,OAAO,MAAMC,iBAA8B,eAAgB,EAACC,MAAM,EAAC;IACjE,4BAA4B;IAC5BN,oBAAoB;QAACO,aAAa,CAACC,UAAYC,QAAQC,MAAM,CAACC,KAAK,CAAC,GAAGH,QAAQ,EAAE,CAAC;IAAC;IAEnF,MAAMI,YAAYf;IAElB,MAAMgB,YAAYT,qBAAqBQ,WAAW;QAChDd;IACF;IAEA,IAAIgB;IACJ,IAAI;QACF,MAAMC,cAAc,MAAMrB,gBAAgBe,QAAQO,GAAG;QACrDF,YAAY,MAAMnB,aAAaoB,YAAYE,SAAS;IACtD,EAAE,OAAM;IACN,sDAAsD;IACxD;IAEAJ,UAAUK,oBAAoB,CAAC;QAC7BC,YAAYb,OAAOc,OAAO;QAC1BC,iBAAiBZ,QAAQa,IAAI;QAC7BC,SAAST,WAAWU,KAAKD;QACzBE,iBAAiBhB,QAAQiB,QAAQ;QACjCC,WAAWb,WAAWU,KAAKG;QAC3BC,SAAS1B;QACT2B,gBAAgBpB,QAAQW,OAAO;IACjC;IAEA,MAAMU,OAAO3B;IAEb,MAAM4B,eAA6B;QACjCC,kBAAkBF,KAAKG,kBAAkB;QACzCC,aAAa;YACXzC,OAAOqC,KAAKI,WAAW,CAACzC,KAAK,IAAI0C;YACjCC,MAAMN,KAAKI,WAAW,CAACE,IAAI,IAAID;YAC/Bf,SAASU,KAAKI,WAAW,CAACd,OAAO,IAAIe;QACvC;QACAE,gBAAgBP,KAAKO,cAAc;QACnCC,gBAAgBR,KAAKQ,cAAc;IACrC;IAEAvC,eAAe,0BAA0BgC;IAEzC,MAAMQ,kBAAkB1B,UAAU2B,KAAK,CAACvC,qBAAqB8B;IAC7DQ,gBAAgBE,KAAK;IAErB,0DAA0D;IAC1D7C,gBAAgB2C,gBAAgBG,UAAU,CAACZ,KAAKQ,cAAc,GAAG;QAC/DK,kBAAkB,CAACC,QAAUL,gBAAgBK,KAAK,CAACA;IACrD;IAEA,oFAAoF;IACpF,mFAAmF;IACnF,uFAAuF;IACvFnC,QAAQoC,IAAI,CAAC,QAAQ,CAACC;QACpB,IAAIA,WAAW,GAAG;YAChBP,gBAAgBQ,QAAQ;QAC1B;QAEA,MAAMC,aAAaxD,cAAc,IAAIyD,IAAI,4BAA4B,YAAYC,GAAG;QACpFnD,eAAe,CAAC,UAAU,EAAEU,QAAQ0C,QAAQ,CAAC,CAAC,EAAEH,WAAW,CAAC,CAAC;QAE7D,qDAAqD;QACrD,8FAA8F;QAC9FzD,MAAMkB,QAAQ0C,QAAQ,EAAE;YAACH;SAAW,EAAE;YACpCI,UAAU;YACVC,KAAK;gBACH,GAAG5C,QAAQ4C,GAAG;gBACdC,6BAA6BxC,WAAWU,KAAKG,aAAa;YAC5D;YACA,6EAA6E;YAC7E4B,OAAO9D,MAAM+D,OAAO,IAAIzD,eAAeyD,OAAO,GAAG,YAAY;QAC/D,GAAGC,KAAK;IACV;AACF,EAAC"}
1
+ {"version":3,"sources":["../../../src/hooks/prerun/setupTelemetry.ts"],"sourcesContent":["import {spawn} from 'node:child_process'\nimport {fileURLToPath} from 'node:url'\n\nimport {type Hook} from '@oclif/core'\nimport {\n type CliConfig,\n debug,\n findProjectRoot,\n getCliConfig,\n setCliTelemetry,\n} from '@sanity/cli-core'\nimport {createSessionId} from '@sanity/telemetry'\n\nimport {resolveConsent} from '../../actions/telemetry/resolveConsent.js'\nimport {telemetryDebug} from '../../actions/telemetry/telemetryDebug.js'\nimport {telemetryDisclosure} from '../../actions/telemetry/telemetryDisclosure.js'\nimport {CliCommandTelemetry, type CLITraceData} from '../../telemetry/cli.telemetry.js'\nimport {detectRuntime} from '../../util/detectRuntime.js'\nimport {parseArguments} from '../../util/parseArguments.js'\nimport {type CommandTelemetry} from '../../util/telemetry/commandTelemetry.js'\nimport {createTelemetryStore} from '../../util/telemetry/createTelemetryStore.js'\n\nexport const setupTelemetry: Hook.Prerun = async function ({Command, config}) {\n // Show telemetry disclosure\n telemetryDisclosure({logToStderr: (message) => process.stderr.write(`${message}\\n`)})\n\n const sessionId = createSessionId()\n\n const telemetry = createTelemetryStore(sessionId, {\n resolveConsent,\n })\n\n let cliConfig: CliConfig | undefined\n try {\n const projectRoot = await findProjectRoot(process.cwd())\n cliConfig = await getCliConfig(projectRoot.directory)\n } catch {\n // Accept not finding a project root and/or CLI config\n }\n\n telemetry.updateUserProperties({\n cliVersion: config.version,\n cpuArchitecture: process.arch,\n dataset: cliConfig?.api?.dataset,\n machinePlatform: process.platform,\n projectId: cliConfig?.api?.projectId,\n runtime: detectRuntime(),\n runtimeVersion: process.version,\n })\n\n const commandTelemetry = (\n Command as (typeof Command & {telemetry?: CommandTelemetry}) | undefined\n )?.telemetry\n const args = parseArguments(process.argv, commandTelemetry)\n\n const traceOptions: CLITraceData = {\n commandArguments: args.argsWithoutOptions,\n coreOptions: {\n debug: args.coreOptions.debug ?? undefined,\n help: args.coreOptions.help ?? undefined,\n version: args.coreOptions.version ?? undefined,\n },\n extraArguments: args.extraArguments,\n groupOrCommand: args.groupOrCommand,\n }\n\n telemetryDebug('Starting command trace', traceOptions)\n\n const cliCommandTrace = telemetry.trace(CliCommandTelemetry, traceOptions)\n cliCommandTrace.start()\n\n // Set the global telemetry store and trace error reporter\n setCliTelemetry(cliCommandTrace.newContext(args.groupOrCommand), {\n reportTraceError: (error) => cliCommandTrace.error(error),\n })\n\n // Handle process exit - complete trace on success, spawn worker to flush telemetry.\n // On non-zero exit, the trace is either already errored (from SanityCommand.catch)\n // or it was a user abort (SIGINT/ctrl+c) — left incomplete, matching old CLI behavior.\n process.once('exit', (status) => {\n if (status === 0) {\n cliCommandTrace.complete()\n }\n\n const workerPath = fileURLToPath(new URL('flushTelemetry.worker.js', import.meta.url))\n telemetryDebug(`Spawning \"${process.execPath} ${workerPath}\"`)\n\n // Spawn detached worker to flush all telemetry files\n // unref will ensure the child process can keep doing work even after the parent process exits\n spawn(process.execPath, [workerPath], {\n detached: true,\n env: {\n ...process.env,\n SANITY_TELEMETRY_PROJECT_ID: cliConfig?.api?.projectId || '',\n },\n // If debug is enabled, spawn the worker with stdio inherit to see the output\n stdio: debug.enabled || telemetryDebug.enabled ? 'inherit' : 'ignore',\n }).unref()\n })\n}\n"],"names":["spawn","fileURLToPath","debug","findProjectRoot","getCliConfig","setCliTelemetry","createSessionId","resolveConsent","telemetryDebug","telemetryDisclosure","CliCommandTelemetry","detectRuntime","parseArguments","createTelemetryStore","setupTelemetry","Command","config","logToStderr","message","process","stderr","write","sessionId","telemetry","cliConfig","projectRoot","cwd","directory","updateUserProperties","cliVersion","version","cpuArchitecture","arch","dataset","api","machinePlatform","platform","projectId","runtime","runtimeVersion","commandTelemetry","args","argv","traceOptions","commandArguments","argsWithoutOptions","coreOptions","undefined","help","extraArguments","groupOrCommand","cliCommandTrace","trace","start","newContext","reportTraceError","error","once","status","complete","workerPath","URL","url","execPath","detached","env","SANITY_TELEMETRY_PROJECT_ID","stdio","enabled","unref"],"mappings":"AAAA,SAAQA,KAAK,QAAO,qBAAoB;AACxC,SAAQC,aAAa,QAAO,WAAU;AAGtC,SAEEC,KAAK,EACLC,eAAe,EACfC,YAAY,EACZC,eAAe,QACV,mBAAkB;AACzB,SAAQC,eAAe,QAAO,oBAAmB;AAEjD,SAAQC,cAAc,QAAO,4CAA2C;AACxE,SAAQC,cAAc,QAAO,4CAA2C;AACxE,SAAQC,mBAAmB,QAAO,iDAAgD;AAClF,SAAQC,mBAAmB,QAA0B,mCAAkC;AACvF,SAAQC,aAAa,QAAO,8BAA6B;AACzD,SAAQC,cAAc,QAAO,+BAA8B;AAE3D,SAAQC,oBAAoB,QAAO,+CAA8C;AAEjF,OAAO,MAAMC,iBAA8B,eAAgB,EAACC,OAAO,EAAEC,MAAM,EAAC;IAC1E,4BAA4B;IAC5BP,oBAAoB;QAACQ,aAAa,CAACC,UAAYC,QAAQC,MAAM,CAACC,KAAK,CAAC,GAAGH,QAAQ,EAAE,CAAC;IAAC;IAEnF,MAAMI,YAAYhB;IAElB,MAAMiB,YAAYV,qBAAqBS,WAAW;QAChDf;IACF;IAEA,IAAIiB;IACJ,IAAI;QACF,MAAMC,cAAc,MAAMtB,gBAAgBgB,QAAQO,GAAG;QACrDF,YAAY,MAAMpB,aAAaqB,YAAYE,SAAS;IACtD,EAAE,OAAM;IACN,sDAAsD;IACxD;IAEAJ,UAAUK,oBAAoB,CAAC;QAC7BC,YAAYb,OAAOc,OAAO;QAC1BC,iBAAiBZ,QAAQa,IAAI;QAC7BC,SAAST,WAAWU,KAAKD;QACzBE,iBAAiBhB,QAAQiB,QAAQ;QACjCC,WAAWb,WAAWU,KAAKG;QAC3BC,SAAS3B;QACT4B,gBAAgBpB,QAAQW,OAAO;IACjC;IAEA,MAAMU,mBACJzB,SACCQ;IACH,MAAMkB,OAAO7B,eAAeO,QAAQuB,IAAI,EAAEF;IAE1C,MAAMG,eAA6B;QACjCC,kBAAkBH,KAAKI,kBAAkB;QACzCC,aAAa;YACX5C,OAAOuC,KAAKK,WAAW,CAAC5C,KAAK,IAAI6C;YACjCC,MAAMP,KAAKK,WAAW,CAACE,IAAI,IAAID;YAC/BjB,SAASW,KAAKK,WAAW,CAAChB,OAAO,IAAIiB;QACvC;QACAE,gBAAgBR,KAAKQ,cAAc;QACnCC,gBAAgBT,KAAKS,cAAc;IACrC;IAEA1C,eAAe,0BAA0BmC;IAEzC,MAAMQ,kBAAkB5B,UAAU6B,KAAK,CAAC1C,qBAAqBiC;IAC7DQ,gBAAgBE,KAAK;IAErB,0DAA0D;IAC1DhD,gBAAgB8C,gBAAgBG,UAAU,CAACb,KAAKS,cAAc,GAAG;QAC/DK,kBAAkB,CAACC,QAAUL,gBAAgBK,KAAK,CAACA;IACrD;IAEA,oFAAoF;IACpF,mFAAmF;IACnF,uFAAuF;IACvFrC,QAAQsC,IAAI,CAAC,QAAQ,CAACC;QACpB,IAAIA,WAAW,GAAG;YAChBP,gBAAgBQ,QAAQ;QAC1B;QAEA,MAAMC,aAAa3D,cAAc,IAAI4D,IAAI,4BAA4B,YAAYC,GAAG;QACpFtD,eAAe,CAAC,UAAU,EAAEW,QAAQ4C,QAAQ,CAAC,CAAC,EAAEH,WAAW,CAAC,CAAC;QAE7D,qDAAqD;QACrD,8FAA8F;QAC9F5D,MAAMmB,QAAQ4C,QAAQ,EAAE;YAACH;SAAW,EAAE;YACpCI,UAAU;YACVC,KAAK;gBACH,GAAG9C,QAAQ8C,GAAG;gBACdC,6BAA6B1C,WAAWU,KAAKG,aAAa;YAC5D;YACA,6EAA6E;YAC7E8B,OAAOjE,MAAMkE,OAAO,IAAI5D,eAAe4D,OAAO,GAAG,YAAY;QAC/D,GAAGC,KAAK;IACV;AACF,EAAC"}
@@ -1,19 +1,12 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { logSymbols } from '@sanity/cli-core/ux';
4
- import { runTypegenGenerate, TypegenWatchModeTrace, TypesGeneratedTrace } from '@sanity/codegen';
4
+ import { configDefinition, runTypegenGenerate, TypegenWatchModeTrace, TypesGeneratedTrace } from '@sanity/codegen';
5
5
  import debounce from 'lodash-es/debounce.js';
6
6
  import mean from 'lodash-es/mean.js';
7
7
  import once from 'lodash-es/once.js';
8
8
  import picomatch from 'picomatch';
9
9
  import { toForwardSlashes } from '../../util/toForwardSlashes.js';
10
- /**
11
- * Default glob patterns to watch for query file changes.
12
- * Covers common source directory naming conventions.
13
- */ const DEFAULT_QUERY_PATTERNS = [
14
- './src/**/*.{ts,tsx,js,jsx}',
15
- './app/**/*.{ts,tsx,js,jsx}'
16
- ];
17
10
  /** Default debounce delay in milliseconds */ const DEFAULT_DEBOUNCE_MS = 1000;
18
11
  /**
19
12
  * Delay before initial generation to allow Vite to finish startup
@@ -39,14 +32,9 @@ import { toForwardSlashes } from '../../util/toForwardSlashes.js';
39
32
  * @internal
40
33
  */ export function sanityTypegenPlugin(options) {
41
34
  const { config: inputConfig, output = console, telemetryLogger, workDir } = options;
42
- // Apply defaults to config
43
- const config = {
44
- formatGeneratedCode: inputConfig.formatGeneratedCode ?? false,
45
- generates: inputConfig.generates ?? 'sanity.types.ts',
46
- overloadClientMethods: inputConfig.overloadClientMethods ?? false,
47
- path: inputConfig.path ?? DEFAULT_QUERY_PATTERNS,
48
- schema: inputConfig.schema ?? 'schema.json'
49
- };
35
+ // Apply defaults through the same schema as `sanity typegen generate`,
36
+ // so watch mode and manual generation produce identical output
37
+ const config = configDefinition.parse(inputConfig);
50
38
  // Build query patterns from config
51
39
  const queryPatterns = Array.isArray(config.path) ? config.path : [
52
40
  config.path
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/server/vite/plugin-typegen.ts"],"sourcesContent":["import {existsSync} from 'node:fs'\nimport path from 'node:path'\n\nimport {CLITelemetryStore} from '@sanity/cli-core'\nimport {logSymbols} from '@sanity/cli-core/ux'\nimport {\n type GenerationResult,\n runTypegenGenerate,\n type TypeGenConfig,\n TypegenWatchModeTrace,\n TypesGeneratedTrace,\n} from '@sanity/codegen'\nimport debounce from 'lodash-es/debounce.js'\nimport mean from 'lodash-es/mean.js'\nimport once from 'lodash-es/once.js'\nimport picomatch from 'picomatch'\nimport {type Plugin} from 'vite'\n\nimport {toForwardSlashes} from '../../util/toForwardSlashes.js'\n\n/**\n * Default glob patterns to watch for query file changes.\n * Covers common source directory naming conventions.\n */\nconst DEFAULT_QUERY_PATTERNS = ['./src/**/*.{ts,tsx,js,jsx}', './app/**/*.{ts,tsx,js,jsx}']\n\n/** Default debounce delay in milliseconds */\nconst DEFAULT_DEBOUNCE_MS = 1000\n\n/**\n * Delay before initial generation to allow Vite to finish startup\n * and avoid race conditions with module resolution.\n */\nconst INITIAL_GENERATION_DELAY_MS = 1000\n\n/**\n * Options for the Sanity typegen Vite plugin.\n *\n * @internal\n */\ninterface TypegenPluginOptions {\n /**\n * Typegen configuration from sanity.cli.ts.\n * All fields are optional and will use sensible defaults.\n */\n config: Partial<TypeGenConfig>\n\n /**\n * Working directory containing the Sanity configuration.\n * This should be the root of your Sanity Studio project.\n */\n workDir: string\n\n /**\n * Logger for output messages. Must implement `log`, `info`, and `error` methods.\n * @defaultValue `console`\n */\n output?: Pick<Console, 'error' | 'info' | 'log'>\n\n /**\n * Telemetry logger for the Sanity CLI tooling.\n */\n telemetryLogger?: CLITelemetryStore\n}\n\n/**\n * Creates a Vite plugin that automatically generates TypeScript types during development and build.\n *\n * **During development:**\n * The plugin performs an initial generation when the dev server starts, then watches\n * for file changes and re-generates types when relevant files are modified.\n *\n * **During build:**\n * The plugin generates types once at the end of the build process.\n *\n * **Prerequisites:**\n * The schema.json file must exist before typegen can run. Run `sanity schema extract`\n * first to generate the schema file.\n *\n * @param options - Configuration options for the plugin\n * @returns A Vite plugin configured for typegen\n *\n * @internal\n */\nexport function sanityTypegenPlugin(options: TypegenPluginOptions): Plugin {\n const {config: inputConfig, output = console, telemetryLogger, workDir} = options\n\n // Apply defaults to config\n const config: TypeGenConfig = {\n formatGeneratedCode: inputConfig.formatGeneratedCode ?? false,\n generates: inputConfig.generates ?? 'sanity.types.ts',\n overloadClientMethods: inputConfig.overloadClientMethods ?? false,\n path: inputConfig.path ?? DEFAULT_QUERY_PATTERNS,\n schema: inputConfig.schema ?? 'schema.json',\n }\n\n // Build query patterns from config\n const queryPatterns = Array.isArray(config.path) ? config.path : [config.path]\n\n // Resolved after Vite config is available\n let resolvedSchemaPath: string\n\n // State for concurrency control\n let isGenerating = false\n let pendingGeneration = false\n\n // Stats for telemetry\n const startTime = Date.now()\n const stats: {failedCount: number; successfulDurations: number[]} = {\n failedCount: 0,\n successfulDurations: [],\n }\n\n /**\n * Runs type generation with concurrency control.\n * If generation is already running, queues one more generation to run after completion.\n * Returns the generation result, or null if generation was skipped or failed.\n */\n async function runGeneration(isBuilding = false): Promise<GenerationResult | null> {\n if (isGenerating) {\n pendingGeneration = true\n return null\n }\n\n isGenerating = true\n pendingGeneration = false\n\n const generationStartTime = Date.now()\n\n try {\n // Validate schema.json exists\n if (!existsSync(resolvedSchemaPath)) {\n output.error(\n `${logSymbols.error} Schema file not found: ${config.schema}\\n` +\n `Run \"sanity schema extract\" first to generate the schema file.`,\n )\n return null\n }\n\n const result = await runTypegenGenerate({\n config,\n workDir,\n })\n\n if (isBuilding) {\n // Add newline for better formatting in build output\n output.log('')\n }\n\n if (result.filesWithErrors > 0) {\n output.log(\n logSymbols.warning,\n `Generated types to ${config.generates} (with errors in ${result.filesWithErrors} file${result.filesWithErrors === 1 ? '' : 's'})`,\n )\n } else {\n output.log(logSymbols.success, `Generated types to ${config.generates}`)\n }\n\n // Track stats for telemetry (all completed generations count as successful)\n stats.successfulDurations.push(Date.now() - generationStartTime)\n\n return result\n } catch (err) {\n output.error(\n `${logSymbols.error} Generation failed: ${err instanceof Error ? err.message : String(err)}`,\n )\n stats.failedCount++\n return null\n } finally {\n isGenerating = false\n\n // If a change came in during generation, run again\n if (pendingGeneration) {\n pendingGeneration = false\n void runGeneration()\n }\n }\n }\n\n const debouncedGenerate = debounce(() => {\n void runGeneration()\n }, DEFAULT_DEBOUNCE_MS)\n\n // Create a matcher function from query patterns\n const isQueryMatch = picomatch(queryPatterns)\n\n return {\n name: 'sanity/typegen',\n\n configResolved() {\n const rootDir = workDir\n\n resolvedSchemaPath = path.isAbsolute(config.schema)\n ? config.schema\n : path.join(rootDir, config.schema)\n },\n\n configureServer(server) {\n const trace = telemetryLogger?.trace(TypegenWatchModeTrace)\n trace?.start()\n trace?.log({step: 'started'})\n\n // Build absolute patterns for query files\n const absoluteQueryPatterns = queryPatterns.map((pattern) =>\n path.isAbsolute(pattern) ? pattern : path.join(workDir, pattern),\n )\n\n // Add query patterns AND schema.json to Vite's watcher\n server.watcher.add([...absoluteQueryPatterns, resolvedSchemaPath])\n\n // Listen for file changes\n const handleChange = (filePath: string) => {\n const relativePath = path.isAbsolute(filePath) ? path.relative(workDir, filePath) : filePath\n // Normalize to forward slashes for cross-platform picomatch compatibility\n const normalizedPath = toForwardSlashes(relativePath)\n\n // Trigger on schema.json change OR matching query file\n if (filePath === resolvedSchemaPath || isQueryMatch(normalizedPath)) {\n debouncedGenerate()\n }\n }\n\n // Prepare function to log \"stopped\" event to trace and complete the trace\n const onClose = once(() => {\n // Cancel any pending debounced extractions\n debouncedGenerate.cancel()\n\n if (trace) {\n trace.log({\n averageGenerationDuration: mean(stats.successfulDurations) || 0,\n generationFailedCount: stats.failedCount,\n generationSuccessfulCount: stats.successfulDurations.length,\n step: 'stopped',\n watcherDuration: Date.now() - startTime,\n })\n trace.complete()\n }\n\n // Clean up process listeners (must always run, not just when trace exists)\n process.off('SIGTERM', onClose)\n process.off('SIGINT', onClose)\n })\n\n server.watcher.on('change', handleChange)\n server.watcher.on('add', handleChange)\n server.watcher.on('unlink', handleChange)\n\n // Call the onClose method when watcher is closed or when process is stopped/killed\n server.watcher.on('close', onClose)\n process.on('SIGTERM', onClose)\n process.on('SIGINT', onClose)\n\n // Run initial generation after server is ready\n const startGeneration = () => {\n setTimeout(() => {\n // Notify about typegen enabled\n output.info(logSymbols.info, 'Typegen enabled. Watching:')\n for (const pattern of queryPatterns) {\n output.info(` - ${pattern}`)\n }\n output.info(` - ${config.schema} (schema)`)\n\n // Perform first generation\n void runGeneration()\n }, INITIAL_GENERATION_DELAY_MS)\n }\n\n if (server.httpServer) {\n server.httpServer.once('listening', startGeneration)\n } else {\n // Middleware mode - no HTTP server, run generation immediately\n startGeneration()\n }\n },\n\n async buildEnd() {\n // Build mode: One-time generation with telemetry tracking\n const result = await runGeneration(true)\n\n // Only log telemetry if generation completed successfully\n if (result && telemetryLogger) {\n const trace = telemetryLogger.trace(TypesGeneratedTrace)\n trace.start()\n trace.log({\n configMethod: 'cli',\n configOverloadClientMethods: config.overloadClientMethods ?? false,\n emptyUnionTypeNodesGenerated: result.emptyUnionTypeNodesGenerated,\n filesWithErrors: result.filesWithErrors,\n outputSize: result.code.length,\n queriesCount: result.queriesCount,\n queryFilesCount: result.queryFilesCount,\n schemaTypesCount: result.schemaTypesCount,\n typeNodesGenerated: result.typeNodesGenerated,\n unknownTypeNodesGenerated: result.unknownTypeNodesGenerated,\n unknownTypeNodesRatio:\n result.typeNodesGenerated > 0\n ? result.unknownTypeNodesGenerated / result.typeNodesGenerated\n : 0,\n })\n trace.complete()\n }\n },\n } satisfies Plugin\n}\n"],"names":["existsSync","path","logSymbols","runTypegenGenerate","TypegenWatchModeTrace","TypesGeneratedTrace","debounce","mean","once","picomatch","toForwardSlashes","DEFAULT_QUERY_PATTERNS","DEFAULT_DEBOUNCE_MS","INITIAL_GENERATION_DELAY_MS","sanityTypegenPlugin","options","config","inputConfig","output","console","telemetryLogger","workDir","formatGeneratedCode","generates","overloadClientMethods","schema","queryPatterns","Array","isArray","resolvedSchemaPath","isGenerating","pendingGeneration","startTime","Date","now","stats","failedCount","successfulDurations","runGeneration","isBuilding","generationStartTime","error","result","log","filesWithErrors","warning","success","push","err","Error","message","String","debouncedGenerate","isQueryMatch","name","configResolved","rootDir","isAbsolute","join","configureServer","server","trace","start","step","absoluteQueryPatterns","map","pattern","watcher","add","handleChange","filePath","relativePath","relative","normalizedPath","onClose","cancel","averageGenerationDuration","generationFailedCount","generationSuccessfulCount","length","watcherDuration","complete","process","off","on","startGeneration","setTimeout","info","httpServer","buildEnd","configMethod","configOverloadClientMethods","emptyUnionTypeNodesGenerated","outputSize","code","queriesCount","queryFilesCount","schemaTypesCount","typeNodesGenerated","unknownTypeNodesGenerated","unknownTypeNodesRatio"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/server/vite/plugin-typegen.ts"],"sourcesContent":["import {existsSync} from 'node:fs'\nimport path from 'node:path'\n\nimport {CLITelemetryStore} from '@sanity/cli-core'\nimport {logSymbols} from '@sanity/cli-core/ux'\nimport {\n configDefinition,\n type GenerationResult,\n runTypegenGenerate,\n type TypeGenConfig,\n TypegenWatchModeTrace,\n TypesGeneratedTrace,\n} from '@sanity/codegen'\nimport debounce from 'lodash-es/debounce.js'\nimport mean from 'lodash-es/mean.js'\nimport once from 'lodash-es/once.js'\nimport picomatch from 'picomatch'\nimport {type Plugin} from 'vite'\n\nimport {toForwardSlashes} from '../../util/toForwardSlashes.js'\n\n/** Default debounce delay in milliseconds */\nconst DEFAULT_DEBOUNCE_MS = 1000\n\n/**\n * Delay before initial generation to allow Vite to finish startup\n * and avoid race conditions with module resolution.\n */\nconst INITIAL_GENERATION_DELAY_MS = 1000\n\n/**\n * Options for the Sanity typegen Vite plugin.\n *\n * @internal\n */\ninterface TypegenPluginOptions {\n /**\n * Typegen configuration from sanity.cli.ts.\n * All fields are optional and will use sensible defaults.\n */\n config: Partial<TypeGenConfig>\n\n /**\n * Working directory containing the Sanity configuration.\n * This should be the root of your Sanity Studio project.\n */\n workDir: string\n\n /**\n * Logger for output messages. Must implement `log`, `info`, and `error` methods.\n * @defaultValue `console`\n */\n output?: Pick<Console, 'error' | 'info' | 'log'>\n\n /**\n * Telemetry logger for the Sanity CLI tooling.\n */\n telemetryLogger?: CLITelemetryStore\n}\n\n/**\n * Creates a Vite plugin that automatically generates TypeScript types during development and build.\n *\n * **During development:**\n * The plugin performs an initial generation when the dev server starts, then watches\n * for file changes and re-generates types when relevant files are modified.\n *\n * **During build:**\n * The plugin generates types once at the end of the build process.\n *\n * **Prerequisites:**\n * The schema.json file must exist before typegen can run. Run `sanity schema extract`\n * first to generate the schema file.\n *\n * @param options - Configuration options for the plugin\n * @returns A Vite plugin configured for typegen\n *\n * @internal\n */\nexport function sanityTypegenPlugin(options: TypegenPluginOptions): Plugin {\n const {config: inputConfig, output = console, telemetryLogger, workDir} = options\n\n // Apply defaults through the same schema as `sanity typegen generate`,\n // so watch mode and manual generation produce identical output\n const config: TypeGenConfig = configDefinition.parse(inputConfig)\n\n // Build query patterns from config\n const queryPatterns = Array.isArray(config.path) ? config.path : [config.path]\n\n // Resolved after Vite config is available\n let resolvedSchemaPath: string\n\n // State for concurrency control\n let isGenerating = false\n let pendingGeneration = false\n\n // Stats for telemetry\n const startTime = Date.now()\n const stats: {failedCount: number; successfulDurations: number[]} = {\n failedCount: 0,\n successfulDurations: [],\n }\n\n /**\n * Runs type generation with concurrency control.\n * If generation is already running, queues one more generation to run after completion.\n * Returns the generation result, or null if generation was skipped or failed.\n */\n async function runGeneration(isBuilding = false): Promise<GenerationResult | null> {\n if (isGenerating) {\n pendingGeneration = true\n return null\n }\n\n isGenerating = true\n pendingGeneration = false\n\n const generationStartTime = Date.now()\n\n try {\n // Validate schema.json exists\n if (!existsSync(resolvedSchemaPath)) {\n output.error(\n `${logSymbols.error} Schema file not found: ${config.schema}\\n` +\n `Run \"sanity schema extract\" first to generate the schema file.`,\n )\n return null\n }\n\n const result = await runTypegenGenerate({\n config,\n workDir,\n })\n\n if (isBuilding) {\n // Add newline for better formatting in build output\n output.log('')\n }\n\n if (result.filesWithErrors > 0) {\n output.log(\n logSymbols.warning,\n `Generated types to ${config.generates} (with errors in ${result.filesWithErrors} file${result.filesWithErrors === 1 ? '' : 's'})`,\n )\n } else {\n output.log(logSymbols.success, `Generated types to ${config.generates}`)\n }\n\n // Track stats for telemetry (all completed generations count as successful)\n stats.successfulDurations.push(Date.now() - generationStartTime)\n\n return result\n } catch (err) {\n output.error(\n `${logSymbols.error} Generation failed: ${err instanceof Error ? err.message : String(err)}`,\n )\n stats.failedCount++\n return null\n } finally {\n isGenerating = false\n\n // If a change came in during generation, run again\n if (pendingGeneration) {\n pendingGeneration = false\n void runGeneration()\n }\n }\n }\n\n const debouncedGenerate = debounce(() => {\n void runGeneration()\n }, DEFAULT_DEBOUNCE_MS)\n\n // Create a matcher function from query patterns\n const isQueryMatch = picomatch(queryPatterns)\n\n return {\n name: 'sanity/typegen',\n\n configResolved() {\n const rootDir = workDir\n\n resolvedSchemaPath = path.isAbsolute(config.schema)\n ? config.schema\n : path.join(rootDir, config.schema)\n },\n\n configureServer(server) {\n const trace = telemetryLogger?.trace(TypegenWatchModeTrace)\n trace?.start()\n trace?.log({step: 'started'})\n\n // Build absolute patterns for query files\n const absoluteQueryPatterns = queryPatterns.map((pattern) =>\n path.isAbsolute(pattern) ? pattern : path.join(workDir, pattern),\n )\n\n // Add query patterns AND schema.json to Vite's watcher\n server.watcher.add([...absoluteQueryPatterns, resolvedSchemaPath])\n\n // Listen for file changes\n const handleChange = (filePath: string) => {\n const relativePath = path.isAbsolute(filePath) ? path.relative(workDir, filePath) : filePath\n // Normalize to forward slashes for cross-platform picomatch compatibility\n const normalizedPath = toForwardSlashes(relativePath)\n\n // Trigger on schema.json change OR matching query file\n if (filePath === resolvedSchemaPath || isQueryMatch(normalizedPath)) {\n debouncedGenerate()\n }\n }\n\n // Prepare function to log \"stopped\" event to trace and complete the trace\n const onClose = once(() => {\n // Cancel any pending debounced extractions\n debouncedGenerate.cancel()\n\n if (trace) {\n trace.log({\n averageGenerationDuration: mean(stats.successfulDurations) || 0,\n generationFailedCount: stats.failedCount,\n generationSuccessfulCount: stats.successfulDurations.length,\n step: 'stopped',\n watcherDuration: Date.now() - startTime,\n })\n trace.complete()\n }\n\n // Clean up process listeners (must always run, not just when trace exists)\n process.off('SIGTERM', onClose)\n process.off('SIGINT', onClose)\n })\n\n server.watcher.on('change', handleChange)\n server.watcher.on('add', handleChange)\n server.watcher.on('unlink', handleChange)\n\n // Call the onClose method when watcher is closed or when process is stopped/killed\n server.watcher.on('close', onClose)\n process.on('SIGTERM', onClose)\n process.on('SIGINT', onClose)\n\n // Run initial generation after server is ready\n const startGeneration = () => {\n setTimeout(() => {\n // Notify about typegen enabled\n output.info(logSymbols.info, 'Typegen enabled. Watching:')\n for (const pattern of queryPatterns) {\n output.info(` - ${pattern}`)\n }\n output.info(` - ${config.schema} (schema)`)\n\n // Perform first generation\n void runGeneration()\n }, INITIAL_GENERATION_DELAY_MS)\n }\n\n if (server.httpServer) {\n server.httpServer.once('listening', startGeneration)\n } else {\n // Middleware mode - no HTTP server, run generation immediately\n startGeneration()\n }\n },\n\n async buildEnd() {\n // Build mode: One-time generation with telemetry tracking\n const result = await runGeneration(true)\n\n // Only log telemetry if generation completed successfully\n if (result && telemetryLogger) {\n const trace = telemetryLogger.trace(TypesGeneratedTrace)\n trace.start()\n trace.log({\n configMethod: 'cli',\n configOverloadClientMethods: config.overloadClientMethods ?? false,\n emptyUnionTypeNodesGenerated: result.emptyUnionTypeNodesGenerated,\n filesWithErrors: result.filesWithErrors,\n outputSize: result.code.length,\n queriesCount: result.queriesCount,\n queryFilesCount: result.queryFilesCount,\n schemaTypesCount: result.schemaTypesCount,\n typeNodesGenerated: result.typeNodesGenerated,\n unknownTypeNodesGenerated: result.unknownTypeNodesGenerated,\n unknownTypeNodesRatio:\n result.typeNodesGenerated > 0\n ? result.unknownTypeNodesGenerated / result.typeNodesGenerated\n : 0,\n })\n trace.complete()\n }\n },\n } satisfies Plugin\n}\n"],"names":["existsSync","path","logSymbols","configDefinition","runTypegenGenerate","TypegenWatchModeTrace","TypesGeneratedTrace","debounce","mean","once","picomatch","toForwardSlashes","DEFAULT_DEBOUNCE_MS","INITIAL_GENERATION_DELAY_MS","sanityTypegenPlugin","options","config","inputConfig","output","console","telemetryLogger","workDir","parse","queryPatterns","Array","isArray","resolvedSchemaPath","isGenerating","pendingGeneration","startTime","Date","now","stats","failedCount","successfulDurations","runGeneration","isBuilding","generationStartTime","error","schema","result","log","filesWithErrors","warning","generates","success","push","err","Error","message","String","debouncedGenerate","isQueryMatch","name","configResolved","rootDir","isAbsolute","join","configureServer","server","trace","start","step","absoluteQueryPatterns","map","pattern","watcher","add","handleChange","filePath","relativePath","relative","normalizedPath","onClose","cancel","averageGenerationDuration","generationFailedCount","generationSuccessfulCount","length","watcherDuration","complete","process","off","on","startGeneration","setTimeout","info","httpServer","buildEnd","configMethod","configOverloadClientMethods","overloadClientMethods","emptyUnionTypeNodesGenerated","outputSize","code","queriesCount","queryFilesCount","schemaTypesCount","typeNodesGenerated","unknownTypeNodesGenerated","unknownTypeNodesRatio"],"mappings":"AAAA,SAAQA,UAAU,QAAO,UAAS;AAClC,OAAOC,UAAU,YAAW;AAG5B,SAAQC,UAAU,QAAO,sBAAqB;AAC9C,SACEC,gBAAgB,EAEhBC,kBAAkB,EAElBC,qBAAqB,EACrBC,mBAAmB,QACd,kBAAiB;AACxB,OAAOC,cAAc,wBAAuB;AAC5C,OAAOC,UAAU,oBAAmB;AACpC,OAAOC,UAAU,oBAAmB;AACpC,OAAOC,eAAe,YAAW;AAGjC,SAAQC,gBAAgB,QAAO,iCAAgC;AAE/D,2CAA2C,GAC3C,MAAMC,sBAAsB;AAE5B;;;CAGC,GACD,MAAMC,8BAA8B;AAgCpC;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,SAASC,oBAAoBC,OAA6B;IAC/D,MAAM,EAACC,QAAQC,WAAW,EAAEC,SAASC,OAAO,EAAEC,eAAe,EAAEC,OAAO,EAAC,GAAGN;IAE1E,uEAAuE;IACvE,+DAA+D;IAC/D,MAAMC,SAAwBb,iBAAiBmB,KAAK,CAACL;IAErD,mCAAmC;IACnC,MAAMM,gBAAgBC,MAAMC,OAAO,CAACT,OAAOf,IAAI,IAAIe,OAAOf,IAAI,GAAG;QAACe,OAAOf,IAAI;KAAC;IAE9E,0CAA0C;IAC1C,IAAIyB;IAEJ,gCAAgC;IAChC,IAAIC,eAAe;IACnB,IAAIC,oBAAoB;IAExB,sBAAsB;IACtB,MAAMC,YAAYC,KAAKC,GAAG;IAC1B,MAAMC,QAA8D;QAClEC,aAAa;QACbC,qBAAqB,EAAE;IACzB;IAEA;;;;GAIC,GACD,eAAeC,cAAcC,aAAa,KAAK;QAC7C,IAAIT,cAAc;YAChBC,oBAAoB;YACpB,OAAO;QACT;QAEAD,eAAe;QACfC,oBAAoB;QAEpB,MAAMS,sBAAsBP,KAAKC,GAAG;QAEpC,IAAI;YACF,8BAA8B;YAC9B,IAAI,CAAC/B,WAAW0B,qBAAqB;gBACnCR,OAAOoB,KAAK,CACV,GAAGpC,WAAWoC,KAAK,CAAC,wBAAwB,EAAEtB,OAAOuB,MAAM,CAAC,EAAE,CAAC,GAC7D,CAAC,8DAA8D,CAAC;gBAEpE,OAAO;YACT;YAEA,MAAMC,SAAS,MAAMpC,mBAAmB;gBACtCY;gBACAK;YACF;YAEA,IAAIe,YAAY;gBACd,oDAAoD;gBACpDlB,OAAOuB,GAAG,CAAC;YACb;YAEA,IAAID,OAAOE,eAAe,GAAG,GAAG;gBAC9BxB,OAAOuB,GAAG,CACRvC,WAAWyC,OAAO,EAClB,CAAC,mBAAmB,EAAE3B,OAAO4B,SAAS,CAAC,iBAAiB,EAAEJ,OAAOE,eAAe,CAAC,KAAK,EAAEF,OAAOE,eAAe,KAAK,IAAI,KAAK,IAAI,CAAC,CAAC;YAEtI,OAAO;gBACLxB,OAAOuB,GAAG,CAACvC,WAAW2C,OAAO,EAAE,CAAC,mBAAmB,EAAE7B,OAAO4B,SAAS,EAAE;YACzE;YAEA,4EAA4E;YAC5EZ,MAAME,mBAAmB,CAACY,IAAI,CAAChB,KAAKC,GAAG,KAAKM;YAE5C,OAAOG;QACT,EAAE,OAAOO,KAAK;YACZ7B,OAAOoB,KAAK,CACV,GAAGpC,WAAWoC,KAAK,CAAC,oBAAoB,EAAES,eAAeC,QAAQD,IAAIE,OAAO,GAAGC,OAAOH,MAAM;YAE9Ff,MAAMC,WAAW;YACjB,OAAO;QACT,SAAU;YACRN,eAAe;YAEf,mDAAmD;YACnD,IAAIC,mBAAmB;gBACrBA,oBAAoB;gBACpB,KAAKO;YACP;QACF;IACF;IAEA,MAAMgB,oBAAoB5C,SAAS;QACjC,KAAK4B;IACP,GAAGvB;IAEH,gDAAgD;IAChD,MAAMwC,eAAe1C,UAAUa;IAE/B,OAAO;QACL8B,MAAM;QAENC;YACE,MAAMC,UAAUlC;YAEhBK,qBAAqBzB,KAAKuD,UAAU,CAACxC,OAAOuB,MAAM,IAC9CvB,OAAOuB,MAAM,GACbtC,KAAKwD,IAAI,CAACF,SAASvC,OAAOuB,MAAM;QACtC;QAEAmB,iBAAgBC,MAAM;YACpB,MAAMC,QAAQxC,iBAAiBwC,MAAMvD;YACrCuD,OAAOC;YACPD,OAAOnB,IAAI;gBAACqB,MAAM;YAAS;YAE3B,0CAA0C;YAC1C,MAAMC,wBAAwBxC,cAAcyC,GAAG,CAAC,CAACC,UAC/ChE,KAAKuD,UAAU,CAACS,WAAWA,UAAUhE,KAAKwD,IAAI,CAACpC,SAAS4C;YAG1D,uDAAuD;YACvDN,OAAOO,OAAO,CAACC,GAAG,CAAC;mBAAIJ;gBAAuBrC;aAAmB;YAEjE,0BAA0B;YAC1B,MAAM0C,eAAe,CAACC;gBACpB,MAAMC,eAAerE,KAAKuD,UAAU,CAACa,YAAYpE,KAAKsE,QAAQ,CAAClD,SAASgD,YAAYA;gBACpF,0EAA0E;gBAC1E,MAAMG,iBAAiB7D,iBAAiB2D;gBAExC,uDAAuD;gBACvD,IAAID,aAAa3C,sBAAsB0B,aAAaoB,iBAAiB;oBACnErB;gBACF;YACF;YAEA,0EAA0E;YAC1E,MAAMsB,UAAUhE,KAAK;gBACnB,2CAA2C;gBAC3C0C,kBAAkBuB,MAAM;gBAExB,IAAId,OAAO;oBACTA,MAAMnB,GAAG,CAAC;wBACRkC,2BAA2BnE,KAAKwB,MAAME,mBAAmB,KAAK;wBAC9D0C,uBAAuB5C,MAAMC,WAAW;wBACxC4C,2BAA2B7C,MAAME,mBAAmB,CAAC4C,MAAM;wBAC3DhB,MAAM;wBACNiB,iBAAiBjD,KAAKC,GAAG,KAAKF;oBAChC;oBACA+B,MAAMoB,QAAQ;gBAChB;gBAEA,2EAA2E;gBAC3EC,QAAQC,GAAG,CAAC,WAAWT;gBACvBQ,QAAQC,GAAG,CAAC,UAAUT;YACxB;YAEAd,OAAOO,OAAO,CAACiB,EAAE,CAAC,UAAUf;YAC5BT,OAAOO,OAAO,CAACiB,EAAE,CAAC,OAAOf;YACzBT,OAAOO,OAAO,CAACiB,EAAE,CAAC,UAAUf;YAE5B,mFAAmF;YACnFT,OAAOO,OAAO,CAACiB,EAAE,CAAC,SAASV;YAC3BQ,QAAQE,EAAE,CAAC,WAAWV;YACtBQ,QAAQE,EAAE,CAAC,UAAUV;YAErB,+CAA+C;YAC/C,MAAMW,kBAAkB;gBACtBC,WAAW;oBACT,+BAA+B;oBAC/BnE,OAAOoE,IAAI,CAACpF,WAAWoF,IAAI,EAAE;oBAC7B,KAAK,MAAMrB,WAAW1C,cAAe;wBACnCL,OAAOoE,IAAI,CAAC,CAAC,IAAI,EAAErB,SAAS;oBAC9B;oBACA/C,OAAOoE,IAAI,CAAC,CAAC,IAAI,EAAEtE,OAAOuB,MAAM,CAAC,SAAS,CAAC;oBAE3C,2BAA2B;oBAC3B,KAAKJ;gBACP,GAAGtB;YACL;YAEA,IAAI8C,OAAO4B,UAAU,EAAE;gBACrB5B,OAAO4B,UAAU,CAAC9E,IAAI,CAAC,aAAa2E;YACtC,OAAO;gBACL,+DAA+D;gBAC/DA;YACF;QACF;QAEA,MAAMI;YACJ,0DAA0D;YAC1D,MAAMhD,SAAS,MAAML,cAAc;YAEnC,0DAA0D;YAC1D,IAAIK,UAAUpB,iBAAiB;gBAC7B,MAAMwC,QAAQxC,gBAAgBwC,KAAK,CAACtD;gBACpCsD,MAAMC,KAAK;gBACXD,MAAMnB,GAAG,CAAC;oBACRgD,cAAc;oBACdC,6BAA6B1E,OAAO2E,qBAAqB,IAAI;oBAC7DC,8BAA8BpD,OAAOoD,4BAA4B;oBACjElD,iBAAiBF,OAAOE,eAAe;oBACvCmD,YAAYrD,OAAOsD,IAAI,CAAChB,MAAM;oBAC9BiB,cAAcvD,OAAOuD,YAAY;oBACjCC,iBAAiBxD,OAAOwD,eAAe;oBACvCC,kBAAkBzD,OAAOyD,gBAAgB;oBACzCC,oBAAoB1D,OAAO0D,kBAAkB;oBAC7CC,2BAA2B3D,OAAO2D,yBAAyB;oBAC3DC,uBACE5D,OAAO0D,kBAAkB,GAAG,IACxB1D,OAAO2D,yBAAyB,GAAG3D,OAAO0D,kBAAkB,GAC5D;gBACR;gBACAtC,MAAMoB,QAAQ;YAChB;QACF;IACF;AACF"}
@@ -0,0 +1,46 @@
1
+ import { Transform } from 'node:stream';
2
+ import { getProjectCliClient } from '@sanity/cli-core';
3
+ import { filter, lastValueFrom, map, Observable, race } from 'rxjs';
4
+ export const ASSETS_API_VERSION = 'v2024-06-24';
5
+ export async function uploadAsset({ assetType, body, contentType, dataset, filename, fileSize, onProgress, projectId, signal }) {
6
+ signal?.throwIfAborted();
7
+ const client = await getProjectCliClient({
8
+ apiVersion: ASSETS_API_VERSION,
9
+ dataset,
10
+ projectId,
11
+ requestTagPrefix: 'sanity.assets.upload',
12
+ requireUser: true
13
+ });
14
+ signal?.throwIfAborted();
15
+ let uploadedBytes = 0;
16
+ const uploadStream = new Transform({
17
+ transform (chunk, _encoding, callback) {
18
+ uploadedBytes += chunk.length;
19
+ onProgress?.(uploadedBytes / fileSize * 100);
20
+ callback(null, chunk);
21
+ }
22
+ });
23
+ body.on('error', (error)=>uploadStream.destroy(error)).pipe(uploadStream);
24
+ const response$ = client.observable.assets.upload(assetType, uploadStream, {
25
+ ...contentType ? {
26
+ contentType
27
+ } : {},
28
+ filename,
29
+ tag: 'asset.upload'
30
+ }).pipe(filter((event)=>event.type === 'response'), map((event)=>event.body.document));
31
+ if (!signal) return lastValueFrom(response$);
32
+ const abort$ = new Observable((subscriber)=>{
33
+ const abortUpload = ()=>{
34
+ body.destroy();
35
+ uploadStream.destroy();
36
+ subscriber.error(signal.reason instanceof Error ? signal.reason : new Error('SIGINT'));
37
+ };
38
+ signal.addEventListener('abort', abortUpload, {
39
+ once: true
40
+ });
41
+ return ()=>signal.removeEventListener('abort', abortUpload);
42
+ });
43
+ return lastValueFrom(race(response$, abort$));
44
+ }
45
+
46
+ //# sourceMappingURL=assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/services/assets.ts"],"sourcesContent":["import {type Readable, Transform} from 'node:stream'\n\nimport {getProjectCliClient} from '@sanity/cli-core'\nimport {filter, lastValueFrom, map, Observable, race} from 'rxjs'\n\nexport const ASSETS_API_VERSION = 'v2024-06-24'\n\nexport type AssetType = 'file' | 'image'\n\ninterface UploadAssetOptions {\n assetType: AssetType\n body: Readable\n dataset: string\n filename: string\n fileSize: number\n projectId: string\n\n contentType?: string\n onProgress?: (percent: number) => void\n signal?: AbortSignal\n}\n\nexport async function uploadAsset({\n assetType,\n body,\n contentType,\n dataset,\n filename,\n fileSize,\n onProgress,\n projectId,\n signal,\n}: UploadAssetOptions) {\n signal?.throwIfAborted()\n\n const client = await getProjectCliClient({\n apiVersion: ASSETS_API_VERSION,\n dataset,\n projectId,\n requestTagPrefix: 'sanity.assets.upload',\n requireUser: true,\n })\n signal?.throwIfAborted()\n\n let uploadedBytes = 0\n const uploadStream = new Transform({\n transform(chunk: Buffer, _encoding, callback) {\n uploadedBytes += chunk.length\n onProgress?.((uploadedBytes / fileSize) * 100)\n callback(null, chunk)\n },\n })\n body.on('error', (error) => uploadStream.destroy(error)).pipe(uploadStream)\n\n const response$ = client.observable.assets\n .upload(assetType, uploadStream, {\n ...(contentType ? {contentType} : {}),\n filename,\n tag: 'asset.upload',\n })\n .pipe(\n filter((event) => event.type === 'response'),\n map((event) => event.body.document),\n )\n\n if (!signal) return lastValueFrom(response$)\n\n const abort$ = new Observable<never>((subscriber) => {\n const abortUpload = () => {\n body.destroy()\n uploadStream.destroy()\n subscriber.error(signal.reason instanceof Error ? signal.reason : new Error('SIGINT'))\n }\n\n signal.addEventListener('abort', abortUpload, {once: true})\n return () => signal.removeEventListener('abort', abortUpload)\n })\n\n return lastValueFrom(race(response$, abort$))\n}\n"],"names":["Transform","getProjectCliClient","filter","lastValueFrom","map","Observable","race","ASSETS_API_VERSION","uploadAsset","assetType","body","contentType","dataset","filename","fileSize","onProgress","projectId","signal","throwIfAborted","client","apiVersion","requestTagPrefix","requireUser","uploadedBytes","uploadStream","transform","chunk","_encoding","callback","length","on","error","destroy","pipe","response$","observable","assets","upload","tag","event","type","document","abort$","subscriber","abortUpload","reason","Error","addEventListener","once","removeEventListener"],"mappings":"AAAA,SAAuBA,SAAS,QAAO,cAAa;AAEpD,SAAQC,mBAAmB,QAAO,mBAAkB;AACpD,SAAQC,MAAM,EAAEC,aAAa,EAAEC,GAAG,EAAEC,UAAU,EAAEC,IAAI,QAAO,OAAM;AAEjE,OAAO,MAAMC,qBAAqB,cAAa;AAiB/C,OAAO,eAAeC,YAAY,EAChCC,SAAS,EACTC,IAAI,EACJC,WAAW,EACXC,OAAO,EACPC,QAAQ,EACRC,QAAQ,EACRC,UAAU,EACVC,SAAS,EACTC,MAAM,EACa;IACnBA,QAAQC;IAER,MAAMC,SAAS,MAAMlB,oBAAoB;QACvCmB,YAAYb;QACZK;QACAI;QACAK,kBAAkB;QAClBC,aAAa;IACf;IACAL,QAAQC;IAER,IAAIK,gBAAgB;IACpB,MAAMC,eAAe,IAAIxB,UAAU;QACjCyB,WAAUC,KAAa,EAAEC,SAAS,EAAEC,QAAQ;YAC1CL,iBAAiBG,MAAMG,MAAM;YAC7Bd,aAAa,AAACQ,gBAAgBT,WAAY;YAC1Cc,SAAS,MAAMF;QACjB;IACF;IACAhB,KAAKoB,EAAE,CAAC,SAAS,CAACC,QAAUP,aAAaQ,OAAO,CAACD,QAAQE,IAAI,CAACT;IAE9D,MAAMU,YAAYf,OAAOgB,UAAU,CAACC,MAAM,CACvCC,MAAM,CAAC5B,WAAWe,cAAc;QAC/B,GAAIb,cAAc;YAACA;QAAW,IAAI,CAAC,CAAC;QACpCE;QACAyB,KAAK;IACP,GACCL,IAAI,CACH/B,OAAO,CAACqC,QAAUA,MAAMC,IAAI,KAAK,aACjCpC,IAAI,CAACmC,QAAUA,MAAM7B,IAAI,CAAC+B,QAAQ;IAGtC,IAAI,CAACxB,QAAQ,OAAOd,cAAc+B;IAElC,MAAMQ,SAAS,IAAIrC,WAAkB,CAACsC;QACpC,MAAMC,cAAc;YAClBlC,KAAKsB,OAAO;YACZR,aAAaQ,OAAO;YACpBW,WAAWZ,KAAK,CAACd,OAAO4B,MAAM,YAAYC,QAAQ7B,OAAO4B,MAAM,GAAG,IAAIC,MAAM;QAC9E;QAEA7B,OAAO8B,gBAAgB,CAAC,SAASH,aAAa;YAACI,MAAM;QAAI;QACzD,OAAO,IAAM/B,OAAOgC,mBAAmB,CAAC,SAASL;IACnD;IAEA,OAAOzC,cAAcG,KAAK4B,WAAWQ;AACvC"}