@sanity/cli 7.14.0 → 7.15.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.
- package/README.md +25 -15
- package/dist/SanityHelp.js +10 -0
- package/dist/SanityHelp.js.map +1 -1
- package/dist/actions/telemetry/resolveConsent.js +4 -2
- package/dist/actions/telemetry/resolveConsent.js.map +1 -1
- package/dist/commands/datasets/copy.js +5 -3
- package/dist/commands/datasets/copy.js.map +1 -1
- package/dist/commands/documents/query.js +5 -4
- package/dist/commands/documents/query.js.map +1 -1
- package/dist/commands/login.js +12 -11
- package/dist/commands/login.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +12 -5
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
- package/dist/exports/invokeSanityCli/help.js +8 -1
- package/dist/exports/invokeSanityCli/help.js.map +1 -1
- package/dist/exports/invokeSanityCli/index.d.ts +15 -6
- package/dist/exports/invokeSanityCli/index.js +61 -25
- package/dist/exports/invokeSanityCli/index.js.map +1 -1
- package/dist/exports/invokeSanityCli/prettyPrintError.js +39 -0
- package/dist/exports/invokeSanityCli/prettyPrintError.js.map +1 -0
- package/dist/services/telemetry.js +4 -0
- package/dist/services/telemetry.js.map +1 -1
- package/dist/util/getSanityEnv.js +6 -1
- package/dist/util/getSanityEnv.js.map +1 -1
- package/dist/util/warnAboutMissingAppId.js +2 -1
- package/dist/util/warnAboutMissingAppId.js.map +1 -1
- package/oclif.manifest.json +15 -16
- package/package.json +14 -14
|
@@ -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\n/**\n * A typed `-F/--field` value of `@<file>` or `@-` makes the `api` command\n * read the host's filesystem or stdin (raw `-f` fields are always verbatim).\n */\nfunction fieldReadsFromHost(field: unknown): boolean {\n if (typeof field !== 'string') return false\n const separatorIndex = field.indexOf('=')\n return separatorIndex > 0 && field[separatorIndex + 1] === '@'\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. Only the host input channels are refused: `--input` reads the\n // request body from the host's filesystem or stdin, and `-F key=@<file>` /\n // `-F key=@-` field values do the same.\n api: conditionalPolicy({\n deniedFlags: ['input'],\n validate: ({flags}) =>\n !Array.isArray(flags.field) || !flags.field.some((field) => fieldReadsFromHost(field)),\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 // --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\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","fieldReadsFromHost","field","separatorIndex","indexOf","mcpPolicy","api","deniedFlags","validate","flags","Array","isArray","some","build","codemod","debug","deploy","dev","doctor","exec","init","install","learn","login","logout","manage","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\n/**\n * A typed `-F/--field` value of `@<file>` or `@-` makes the `api` command\n * read the host's filesystem or stdin (raw `-f` fields are always verbatim).\n */\nfunction fieldReadsFromHost(field: unknown): boolean {\n if (typeof field !== 'string') return false\n const separatorIndex = field.indexOf('=')\n return separatorIndex > 0 && field[separatorIndex + 1] === '@'\n}\n\n/** A `-H/--header` value that replaces the invocation's authentication. */\nfunction headerOverridesAuthentication(header: unknown): boolean {\n if (typeof header !== 'string') return false\n const separatorIndex = header.indexOf(':')\n return (\n separatorIndex > 0 && header.slice(0, separatorIndex).trim().toLowerCase() === 'authorization'\n )\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` and an Authorization header replace the MCP user's token,\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: ({flags}) =>\n (!Array.isArray(flags.field) || !flags.field.some((field) => fieldReadsFromHost(field))) &&\n (!Array.isArray(flags.header) ||\n !flags.header.some((header) => headerOverridesAuthentication(header))),\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 // --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\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","fieldReadsFromHost","field","separatorIndex","indexOf","headerOverridesAuthentication","header","slice","trim","toLowerCase","mcpPolicy","api","deniedFlags","validate","flags","Array","isArray","some","build","codemod","debug","deploy","dev","doctor","exec","init","install","learn","login","logout","manage","preview","undeploy","versions"],"mappings":"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"}
|
|
@@ -60,6 +60,7 @@ import { isConditionalInvocationPolicy } from './commandPolicies/policy.js';
|
|
|
60
60
|
* Output is collected in {@link InvokableHelp.lines} instead of being written
|
|
61
61
|
* to the process streams.
|
|
62
62
|
*/ class InvokableHelp extends Help {
|
|
63
|
+
commandId;
|
|
63
64
|
lines = [];
|
|
64
65
|
commandIds;
|
|
65
66
|
policySet;
|
|
@@ -92,6 +93,7 @@ import { isConditionalInvocationPolicy } from './commandPolicies/policy.js';
|
|
|
92
93
|
}
|
|
93
94
|
async showCommandHelp(command) {
|
|
94
95
|
if (!this.commandIds.has(command.id)) throw new NotInvokableError(command.id);
|
|
96
|
+
this.commandId = command.id;
|
|
95
97
|
return super.showCommandHelp(withoutDeniedFlags(command, this.policySet[command.id]));
|
|
96
98
|
}
|
|
97
99
|
async showTopicHelp(topic) {
|
|
@@ -123,7 +125,12 @@ import { isConditionalInvocationPolicy } from './commandPolicies/policy.js';
|
|
|
123
125
|
if (err instanceof NotInvokableError || err.oclif) return undefined;
|
|
124
126
|
throw err;
|
|
125
127
|
}
|
|
126
|
-
return
|
|
128
|
+
return {
|
|
129
|
+
...help.commandId && {
|
|
130
|
+
commandId: help.commandId
|
|
131
|
+
},
|
|
132
|
+
output: help.lines.join('\n').trimEnd()
|
|
133
|
+
};
|
|
127
134
|
}
|
|
128
135
|
/**
|
|
129
136
|
* Whether `argv` asks for help rather than a command invocation: a leading
|
|
@@ -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 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 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<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 help.lines.join('\\n').trimEnd()\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","lines","topicNames","config","stripAnsi","sortedCommands","has","sortedTopics","topic","log","args","showCommandHelp","showTopicHelp","renderInvokableHelp","argv","help","showHelp","err","oclif","undefined","trimEnd","isHelpRequest","helpFlags","token","includes"],"mappings":"AAAA,SAAmCA,IAAI,QAAwB,cAAa;AAC5E,SAAQC,oBAAoB,QAAO,mBAAkB;AAErD,SAGEC,6BAA6B,QACxB,8BAA6B;AAEpC,+DAA+D,GAC/D,SAASC,kBAAkBC,SAA2B;IACpD,OAAO,IAAIC,IACTC,OAAOC,OAAO,CAACH,WACZI,MAAM,CAAC,CAAC,GAAGC,OAAO,GAAKA,OAAOC,IAAI,KAAK,QACvCC,GAAG,CAAC,CAAC,CAACC,GAAG,GAAKA;AAErB;AAEA,yEAAyE,GACzE,SAASC,kBAAkBC,UAAuB;IAChD,MAAMC,QAAQ,IAAIV;IAClB,KAAK,MAAMO,MAAME,WAAY;QAC3B,MAAME,QAAQJ,GAAGK,KAAK,CAAC;QACvB,IAAK,IAAIC,SAAS,GAAGA,SAASF,MAAME,MAAM,EAAEA,SAAU;YACpDH,MAAMI,GAAG,CAACH,MAAMI,KAAK,CAAC,GAAGF,QAAQG,IAAI,CAAC;QACxC;IACF;IACA,OAAON;AACT;AAEA,4EAA4E,GAC5E,MAAMO,0BAA0BC;AAAO;AAEvC;;;;CAIC,GACD,SAASC,mBAAmBC,OAAyB,EAAEhB,MAAsB;IAC3E,IAAI,CAACA,UAAU,CAACP,8BAA8BO,SAAS,OAAO;QAAC,GAAGgB,OAAO;IAAA;IAEzE,MAAMC,SAASjB,OAAOkB,WAAW;IACjC,IAAID,OAAOR,MAAM,KAAK,GAAG,OAAO;QAAC,GAAGO,OAAO;IAAA;IAE3C,MAAMG,QAAQ;QAAC,GAAGH,QAAQG,KAAK;IAAA;IAC/B,MAAMC,YAAsB,EAAE;IAC9B,KAAK,MAAMC,QAAQJ,OAAQ;QACzB,MAAMK,aAAaH,KAAK,CAACE,KAAK;QAC9B,IAAI,CAACC,YAAY;QACjBF,UAAUG,IAAI,CAAC,CAAC,EAAE,EAAEF,MAAM;QAC1B,IAAIC,WAAWE,IAAI,EAAEJ,UAAUG,IAAI,CAAC,CAAC,CAAC,EAAED,WAAWE,IAAI,EAAE;QACzD,KAAK,MAAMC,SAASH,WAAWI,OAAO,IAAI,EAAE,CAAEN,UAAUG,IAAI,CAAC,CAAC,EAAE,EAAEE,OAAO,EAAE,CAAC,CAAC,EAAEA,OAAO;QACtF,OAAON,KAAK,CAACE,KAAK;IACpB;IAEA,MAAMM,iBAAiB,CAACC;QACtB,MAAMC,OAAO,OAAOD,YAAY,WAAWA,UAAUA,QAAQZ,OAAO;QACpE,OAAOI,UAAUU,IAAI,CAAC,CAACC,WACrB,IAAIC,OAAOC,OAAOC,GAAG,CAAC,MAAM,EAAEH,SAAS,QAAQ,CAAC,EAAEI,IAAI,CAACN,QAAQ;IAEnE;IAEA,OAAO;QACL,GAAGb,OAAO;QACVoB,UAAUpB,QAAQoB,QAAQ,EAAErC,OAAO,CAAC6B,UAAY,CAACD,eAAeC;QAChET;IACF;AACF;AAEA;;;;;;;;;CASC,GACD,MAAMkB,sBAAsB9C;IACV+C,QAAkB,EAAE,CAAA;IAEnBjC,WAAuB;IACvBV,UAA2B;IAC3B4C,WAAuB;IAExC,YAAYC,MAAc,EAAE7C,SAA2B,CAAE;QACvD,KAAK,CAAC6C,QAAQ;YAACC,WAAW;QAAI;QAC9B,IAAI,CAAC9C,SAAS,GAAGA;QACjB,IAAI,CAACU,UAAU,GAAGX,kBAAkBC;QACpC,IAAI,CAAC4C,UAAU,GAAGnC,kBAAkB,IAAI,CAACC,UAAU;IACrD;IAEA,0EAA0E;IAC1E,4EAA4E;IAC5E,uEAAuE;IACvE,eAAe;IAEf,IAAuBqC,iBAAqC;QAC1D,OAAO,KAAK,CAACA,eACV3C,MAAM,CAAC,CAACiB,UAAY,IAAI,CAACX,UAAU,CAACsC,GAAG,CAAC3B,QAAQb,EAAE,GAClDD,GAAG,CAAC,CAACc,UAAa,CAAA;gBAAC,GAAGA,OAAO;YAAA,CAAA;IAClC;IAEA,IAAuB4B,eAAmC;QACxD,4EAA4E;QAC5E,OAAO,KAAK,CAACA,aACV7C,MAAM,CAAC,CAAC8C,QAAU,IAAI,CAACN,UAAU,CAACI,GAAG,CAACE,MAAMxB,IAAI,GAChDnB,GAAG,CAAC,CAAC2C,QAAW,CAAA;gBAAC,GAAGA,KAAK;YAAA,CAAA;IAC9B;IAEmBC,IAAI,GAAGC,IAAc,EAAQ;QAC9C,IAAI,CAACT,KAAK,CAACf,IAAI,IAAIwB;IACrB;IAEA,MAAsBC,gBAAgBhC,OAAyB,EAAiB;QAC9E,IAAI,CAAC,IAAI,CAACX,UAAU,CAACsC,GAAG,CAAC3B,QAAQb,EAAE,GAAG,MAAM,IAAIU,kBAAkBG,QAAQb,EAAE;QAC5E,OAAO,KAAK,CAAC6C,gBAAgBjC,mBAAmBC,SAAS,IAAI,CAACrB,SAAS,CAACqB,QAAQb,EAAE,CAAC;IACrF;IAEA,MAAyB8C,cAAcJ,KAAuB,EAAiB;QAC7E,IAAI,CAAC,IAAI,CAACN,UAAU,CAACI,GAAG,CAACE,MAAMxB,IAAI,GAAG,MAAM,IAAIR,kBAAkBgC,MAAMxB,IAAI;QAC5E,OAAO,KAAK,CAAC4B,cAAc;YAAC,GAAGJ,KAAK;QAAA;IACtC;AACF;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,eAAeK,oBACpBV,MAAc,EACdW,IAAc,EACdxD,SAA2B;IAE3B,MAAMyD,OAAO,IAAIf,cAAcG,QAAQ7C;IACvC,IAAI;QACF,MAAMyD,KAAKC,QAAQ,CAACF;IACtB,EAAE,OAAOG,KAAK;QACZ,uEAAuE;QACvE,0EAA0E;QAC1E,6BAA6B;QAC7B,IAAIA,eAAezC,qBAAqByC,IAAIC,KAAK,EAAE,OAAOC;QAC1D,MAAMF;IACR;IACA,OAAOF,KAAKd,KAAK,CAAC1B,IAAI,CAAC,MAAM6C,OAAO;AACtC;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,cAAcP,IAAc,EAAEX,MAAc;IAC1D,IAAIW,IAAI,CAAC,EAAE,KAAK,QAAQ,OAAO;IAC/B,MAAMQ,YAAYnE,qBAAqBgD;IACvC,KAAK,MAAMoB,SAAST,KAAM;QACxB,IAAIS,UAAU,MAAM,OAAO;QAC3B,IAAID,UAAUE,QAAQ,CAACD,QAAQ,OAAO;IACxC;IACA,OAAO;AACT"}
|
|
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,4 +1,5 @@
|
|
|
1
1
|
import { Config } from "@oclif/core";
|
|
2
|
+
import { SanityEnvironment } from "@sanity/cli-core/executionContext";
|
|
2
3
|
|
|
3
4
|
/** Where an invocation originates; selects the policy to enforce. */
|
|
4
5
|
declare type InvocationSource = "mcp";
|
|
@@ -12,12 +13,9 @@ declare type InvocationSource = "mcp";
|
|
|
12
13
|
*
|
|
13
14
|
* @internal
|
|
14
15
|
*/
|
|
15
|
-
export declare function invokeSanityCli(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
source,
|
|
19
|
-
token,
|
|
20
|
-
}: InvokeSanityCliOptions): Promise<InvokeSanityCliResult>;
|
|
16
|
+
export declare function invokeSanityCli(
|
|
17
|
+
options: InvokeSanityCliOptions,
|
|
18
|
+
): Promise<InvokeSanityCliResult>;
|
|
21
19
|
|
|
22
20
|
/**
|
|
23
21
|
* @internal
|
|
@@ -45,6 +43,12 @@ export declare interface InvokeSanityCliOptions {
|
|
|
45
43
|
* package's config, loaded once and cached across invocations.
|
|
46
44
|
*/
|
|
47
45
|
config?: Config;
|
|
46
|
+
/**
|
|
47
|
+
* Sanity deployment environment for this invocation. Scoped to this call
|
|
48
|
+
* via the CLI execution context and defaults to production. The embedding
|
|
49
|
+
* process's `SANITY_INTERNAL_ENV` is never consulted.
|
|
50
|
+
*/
|
|
51
|
+
sanityEnv?: SanityEnvironment;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/**
|
|
@@ -55,6 +59,11 @@ export declare interface InvokeSanityCliResult {
|
|
|
55
59
|
exitCode: number;
|
|
56
60
|
/** Combined stdout and stderr output, in emission order. */
|
|
57
61
|
output: string;
|
|
62
|
+
/**
|
|
63
|
+
* Canonical oclif command id when the invocation resolved to a command
|
|
64
|
+
* exposed by the selected policy (for example, `datasets:create`).
|
|
65
|
+
*/
|
|
66
|
+
commandId?: string;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
export {};
|
|
@@ -19,21 +19,44 @@
|
|
|
19
19
|
* Help works like the regular CLI, scoped to the source's policy: `--help`
|
|
20
20
|
* (or `help`) renders root help listing the invokable topics, and a subject
|
|
21
21
|
* (`cors --help`, `cors list --help`) renders topic or command help.
|
|
22
|
-
*/ import { Config, Parser } from '@oclif/core';
|
|
22
|
+
*/ import { Config, Parser, settings } from '@oclif/core';
|
|
23
23
|
import { getHelpFlagAdditions, normalizeArgv } from '@oclif/core/help';
|
|
24
|
-
import {
|
|
24
|
+
import { exitCodes } from '@sanity/cli-core';
|
|
25
25
|
import { runWithCliExecutionContext } from '@sanity/cli-core/executionContext';
|
|
26
26
|
import { parseArgsStringToArgv } from 'string-argv';
|
|
27
27
|
import { commandPolicies } from './commandPolicies/index.js';
|
|
28
28
|
import { deny, isConditionalInvocationPolicy } from './commandPolicies/policy.js';
|
|
29
29
|
import { isHelpRequest, renderInvokableHelp } from './help.js';
|
|
30
|
+
import { prettyPrintError } from './prettyPrintError.js';
|
|
31
|
+
/**
|
|
32
|
+
* Instantiate a policy-approved command for isolated execution.
|
|
33
|
+
*
|
|
34
|
+
* Every invokable command extends `SanityCommand`, whose
|
|
35
|
+
* `runInExecutionContext()` runs the instance without oclif's static
|
|
36
|
+
* `Command.run()` — the static runner re-enters `Config.load()` (filesystem
|
|
37
|
+
* reads and a process-global config cache write) on every call. The manifest
|
|
38
|
+
* types command classes as the abstract oclif `Command`, hence the
|
|
39
|
+
* constructor cast. The method is probed rather than checked with
|
|
40
|
+
* `instanceof`, because command modules may load from the compiled build
|
|
41
|
+
* while this module runs from source, giving `SanityCommand` two identities.
|
|
42
|
+
*/ function instantiateCommand(CommandClass, argv, config) {
|
|
43
|
+
const ConcreteCommand = CommandClass;
|
|
44
|
+
const command = new ConcreteCommand(argv, config);
|
|
45
|
+
if (typeof command.runInExecutionContext !== 'function') {
|
|
46
|
+
throw new TypeError(`Command "${CommandClass.id}" does not support isolated execution`);
|
|
47
|
+
}
|
|
48
|
+
return command;
|
|
49
|
+
}
|
|
30
50
|
/**
|
|
31
51
|
* Load the oclif `Config` for this package, needed to resolve, load, and run
|
|
32
52
|
* commands. Loading it once and reusing it across invocations avoids
|
|
33
|
-
* re-reading the command manifest per call.
|
|
53
|
+
* re-reading the command manifest per call. It only reads this package's own
|
|
54
|
+
* installed files — process-lifetime initialization, not per-invocation host
|
|
55
|
+
* state — so it happens outside any execution context.
|
|
34
56
|
*/ function loadCliCommandConfig() {
|
|
35
57
|
return Config.load(import.meta.url);
|
|
36
58
|
}
|
|
59
|
+
let cachedConfig;
|
|
37
60
|
function unknownCommandResult(argv, policySet) {
|
|
38
61
|
const available = Object.entries(policySet).filter(([, policy])=>policy.kind !== 'deny').map(([id])=>id.replaceAll(':', ' ')).toSorted();
|
|
39
62
|
return {
|
|
@@ -44,7 +67,6 @@ function unknownCommandResult(argv, policySet) {
|
|
|
44
67
|
].join('\n')
|
|
45
68
|
};
|
|
46
69
|
}
|
|
47
|
-
let cachedConfig;
|
|
48
70
|
/**
|
|
49
71
|
* Unlike a shell, string-argv keeps quotes that are glued to unquoted text:
|
|
50
72
|
* `--name="my project"` tokenizes with the quotes intact. Strip a matching
|
|
@@ -62,15 +84,27 @@ let cachedConfig;
|
|
|
62
84
|
* relay them verbatim.
|
|
63
85
|
*
|
|
64
86
|
* @internal
|
|
65
|
-
*/ export async function invokeSanityCli(
|
|
66
|
-
|
|
87
|
+
*/ export async function invokeSanityCli(options) {
|
|
88
|
+
// Always load compiled command modules. Oclif's dev-mode source resolution
|
|
89
|
+
// otherwise probes tsconfigs (reading the filesystem and process.cwd) on
|
|
90
|
+
// every command load, and may register ts-node process-wide.
|
|
91
|
+
settings.enableAutoTranspile = false;
|
|
92
|
+
const resolvedConfig = options.config ?? await (cachedConfig ??= loadCliCommandConfig());
|
|
93
|
+
const output = [];
|
|
94
|
+
const sink = (line)=>output.push(line);
|
|
95
|
+
const { sanityEnv, token } = options;
|
|
96
|
+
// Establish the isolation boundary before rendering help, loading command
|
|
97
|
+
// modules, parsing, or executing. Any code reached by an external
|
|
98
|
+
// invocation can therefore fail closed on context.
|
|
99
|
+
return runWithCliExecutionContext({
|
|
100
|
+
sanityEnv,
|
|
101
|
+
stderr: sink,
|
|
102
|
+
stdout: sink,
|
|
103
|
+
token
|
|
104
|
+
}, ()=>invokeSanityCliInContext(options, resolvedConfig, output));
|
|
105
|
+
}
|
|
106
|
+
async function invokeSanityCliInContext({ args, source }, resolvedConfig, output) {
|
|
67
107
|
const policySet = commandPolicies[source];
|
|
68
|
-
// Commands log through the global telemetry store; default it to a noop
|
|
69
|
-
// store so embedding hosts need no telemetry wiring (and see no warnings),
|
|
70
|
-
// without clobbering a store the host may have installed itself.
|
|
71
|
-
if (!globalThis[CLI_TELEMETRY_SYMBOL]) {
|
|
72
|
-
setCliTelemetry(noopLogger);
|
|
73
|
-
}
|
|
74
108
|
// Pre-split argv arrays are taken verbatim; only string input goes through
|
|
75
109
|
// shell-style tokenization and quote normalization.
|
|
76
110
|
let argv = typeof args === 'string' ? parseArgsStringToArgv(args).map((t)=>stripFlagQuotes(t)) : [
|
|
@@ -88,10 +122,10 @@ let cachedConfig;
|
|
|
88
122
|
// Drop a leading `help` so the rest is the subject, mirroring how
|
|
89
123
|
// oclif's dispatch consumes the token before the help command sees argv
|
|
90
124
|
const helpArgv = argv[0] === 'help' ? argv.slice(1) : argv;
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
|
|
125
|
+
const result = await renderInvokableHelp(resolvedConfig, helpArgv, policySet);
|
|
126
|
+
if (result) return {
|
|
127
|
+
...result,
|
|
128
|
+
exitCode: exitCodes.SUCCESS
|
|
95
129
|
};
|
|
96
130
|
const helpFlags = getHelpFlagAdditions(resolvedConfig);
|
|
97
131
|
return unknownCommandResult(helpArgv.filter((token)=>!helpFlags.includes(token)), policySet);
|
|
@@ -128,6 +162,7 @@ let cachedConfig;
|
|
|
128
162
|
};
|
|
129
163
|
} catch (err) {
|
|
130
164
|
return {
|
|
165
|
+
commandId,
|
|
131
166
|
exitCode: exitCodes.USAGE_ERROR,
|
|
132
167
|
output: err instanceof Error ? err.message : String(err)
|
|
133
168
|
};
|
|
@@ -137,22 +172,21 @@ let cachedConfig;
|
|
|
137
172
|
let output = `This invocation of \`${displayId}\` is not supported here`;
|
|
138
173
|
if (isConditionalInvocationPolicy(policy)) {
|
|
139
174
|
const usedDeniedFlags = policy.deniedFlags.filter((name)=>invocation.flags[name] !== undefined && invocation.flags[name] !== false);
|
|
140
|
-
|
|
175
|
+
if (usedDeniedFlags.length > 0) {
|
|
176
|
+
output = `\nThe ${usedDeniedFlags.map((name)=>`--${name}`).join(', ')} flag is not supported here for \`${displayId}\``;
|
|
177
|
+
}
|
|
141
178
|
}
|
|
142
179
|
return {
|
|
180
|
+
commandId,
|
|
143
181
|
exitCode: exitCodes.USAGE_ERROR,
|
|
144
182
|
output
|
|
145
183
|
};
|
|
146
184
|
}
|
|
147
|
-
const output = [];
|
|
148
|
-
const sink = (line)=>output.push(line);
|
|
149
185
|
try {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
stdout: sink,
|
|
153
|
-
token
|
|
154
|
-
}, ()=>CommandClass.run(commandArgv, resolvedConfig));
|
|
186
|
+
const command = instantiateCommand(CommandClass, commandArgv, resolvedConfig);
|
|
187
|
+
await command.runInExecutionContext();
|
|
155
188
|
return {
|
|
189
|
+
commandId,
|
|
156
190
|
exitCode: exitCodes.SUCCESS,
|
|
157
191
|
output: output.join('\n')
|
|
158
192
|
};
|
|
@@ -161,13 +195,15 @@ let cachedConfig;
|
|
|
161
195
|
// `this.exit(0)` throws an ExitError but is a successful outcome
|
|
162
196
|
if (exit === exitCodes.SUCCESS) {
|
|
163
197
|
return {
|
|
198
|
+
commandId,
|
|
164
199
|
exitCode: exitCodes.SUCCESS,
|
|
165
200
|
output: output.join('\n')
|
|
166
201
|
};
|
|
167
202
|
}
|
|
168
|
-
const message = err
|
|
203
|
+
const message = prettyPrintError(err) || String(err);
|
|
169
204
|
if (message) output.push(message);
|
|
170
205
|
return {
|
|
206
|
+
commandId,
|
|
171
207
|
exitCode: typeof exit === 'number' ? exit : exitCodes.RUNTIME_ERROR,
|
|
172
208
|
output: output.join('\n')
|
|
173
209
|
};
|
|
@@ -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 {Config, Parser} from '@oclif/core'\nimport {getHelpFlagAdditions, normalizeArgv} from '@oclif/core/help'\nimport {CLI_TELEMETRY_SYMBOL, exitCodes, noopLogger, setCliTelemetry} from '@sanity/cli-core'\nimport {runWithCliExecutionContext} from '@sanity/cli-core/executionContext'\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'\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.\n */\nfunction loadCliCommandConfig(): Promise<Config> {\n return Config.load(import.meta.url)\n}\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/**\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\nlet cachedConfig: Promise<Config> | undefined\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 args,\n config,\n source,\n token,\n}: InvokeSanityCliOptions): Promise<InvokeSanityCliResult> {\n const resolvedConfig = config ?? (await (cachedConfig ??= loadCliCommandConfig()))\n const policySet = commandPolicies[source]\n\n // Commands log through the global telemetry store; default it to a noop\n // store so embedding hosts need no telemetry wiring (and see no warnings),\n // without clobbering a store the host may have installed itself.\n if (!(globalThis as Record<symbol, unknown>)[CLI_TELEMETRY_SYMBOL]) {\n setCliTelemetry(noopLogger)\n }\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 output = await renderInvokableHelp(resolvedConfig, helpArgv, policySet)\n if (output !== undefined) return {exitCode: exitCodes.SUCCESS, output}\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 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 output = `\\nThe ${usedDeniedFlags.map((name) => `--${name}`).join(', ')} flag is not supported here for \\`${displayId}\\``\n }\n\n return {\n exitCode: exitCodes.USAGE_ERROR,\n output,\n }\n }\n\n const output: string[] = []\n const sink = (line: string) => output.push(line)\n\n try {\n await runWithCliExecutionContext({stderr: sink, stdout: sink, token}, () =>\n CommandClass.run(commandArgv, resolvedConfig),\n )\n return {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 {exitCode: exitCodes.SUCCESS, output: output.join('\\n')}\n }\n\n const message = err instanceof Error ? err.message : String(err)\n if (message) output.push(message)\n return {\n exitCode: typeof exit === 'number' ? exit : exitCodes.RUNTIME_ERROR,\n output: output.join('\\n'),\n }\n }\n}\n"],"names":["Config","Parser","getHelpFlagAdditions","normalizeArgv","CLI_TELEMETRY_SYMBOL","exitCodes","noopLogger","setCliTelemetry","runWithCliExecutionContext","parseArgsStringToArgv","commandPolicies","deny","isConditionalInvocationPolicy","isHelpRequest","renderInvokableHelp","loadCliCommandConfig","load","url","unknownCommandResult","argv","policySet","available","Object","entries","filter","policy","kind","map","id","replaceAll","toSorted","exitCode","USAGE_ERROR","output","slice","join","cachedConfig","stripFlagQuotes","rawToken","match","exec","invokeSanityCli","args","config","source","token","resolvedConfig","globalThis","t","helpArgv","undefined","SUCCESS","helpFlags","includes","err","RUNTIME_ERROR","Error","message","String","commandId","commandArgv","commandDefinition","findCommand","CommandClass","invocation","parsed","parse","baseFlags","enableJsonFlag","flags","strict","validate","displayId","usedDeniedFlags","deniedFlags","name","sink","line","push","stderr","stdout","run","exit","oclif"],"mappings":"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"}
|
|
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":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const formatSuggestions = (suggestions)=>{
|
|
2
|
+
const label = 'Try this:';
|
|
3
|
+
if (!suggestions || suggestions.length === 0) return undefined;
|
|
4
|
+
if (suggestions.length === 1) return `${label} ${suggestions[0]}`;
|
|
5
|
+
const multiple = suggestions.map((suggestion)=>` * ${suggestion}`).join('\n');
|
|
6
|
+
return `${label}\n${multiple}`;
|
|
7
|
+
};
|
|
8
|
+
function isCombinedError(error) {
|
|
9
|
+
return error !== null && typeof error === 'object' && 'name' in error && 'message' in error;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Adapted from oclif's unexported `prettyPrint` implementation:
|
|
13
|
+
* https://github.com/oclif/core/blob/4.11.14/src/errors/errors/pretty-print.ts
|
|
14
|
+
*
|
|
15
|
+
* Terminal wrapping, indentation, ANSI decoration, and debug stack handling are omitted so programmatic callers receive stable plain text.
|
|
16
|
+
*/ export function prettyPrintError(error) {
|
|
17
|
+
const prettyPrintedErrors = [];
|
|
18
|
+
let currentError = error;
|
|
19
|
+
let isDeep = false;
|
|
20
|
+
while(isCombinedError(currentError)){
|
|
21
|
+
const { code, message, name: errorSuffix, ref, suggestions } = currentError;
|
|
22
|
+
const formattedHeader = message ? `${errorSuffix || 'Error'}: ${message}` : undefined;
|
|
23
|
+
const formattedCode = code ? `Code: ${code}` : undefined;
|
|
24
|
+
const formattedSuggestions = formatSuggestions(suggestions);
|
|
25
|
+
const formattedReference = ref ? `Reference: ${ref}` : undefined;
|
|
26
|
+
const formatted = [
|
|
27
|
+
formattedHeader,
|
|
28
|
+
formattedCode,
|
|
29
|
+
formattedSuggestions,
|
|
30
|
+
formattedReference
|
|
31
|
+
].filter(Boolean).join('\n');
|
|
32
|
+
prettyPrintedErrors.push(`${isDeep ? 'Caused by: ' : ''}${formatted}`);
|
|
33
|
+
isDeep = true;
|
|
34
|
+
currentError = currentError.cause ?? null;
|
|
35
|
+
}
|
|
36
|
+
return prettyPrintedErrors.join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=prettyPrintError.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/exports/invokeSanityCli/prettyPrintError.ts"],"sourcesContent":["import {type Interfaces} from '@oclif/core'\n\nconst formatSuggestions = (suggestions?: string[]): string | undefined => {\n const label = 'Try this:'\n if (!suggestions || suggestions.length === 0) return undefined\n if (suggestions.length === 1) return `${label} ${suggestions[0]}`\n\n const multiple = suggestions.map((suggestion) => ` * ${suggestion}`).join('\\n')\n return `${label}\\n${multiple}`\n}\n\ntype CombinedError = Error & Interfaces.PrettyPrintableError\n\nfunction isCombinedError(error: unknown): error is CombinedError {\n return error !== null && typeof error === 'object' && 'name' in error && 'message' in error\n}\n\n/**\n * Adapted from oclif's unexported `prettyPrint` implementation:\n * https://github.com/oclif/core/blob/4.11.14/src/errors/errors/pretty-print.ts\n *\n * Terminal wrapping, indentation, ANSI decoration, and debug stack handling are omitted so programmatic callers receive stable plain text.\n */\nexport function prettyPrintError(error: unknown): string {\n const prettyPrintedErrors: string[] = []\n let currentError = error\n let isDeep = false\n\n while (isCombinedError(currentError)) {\n const {code, message, name: errorSuffix, ref, suggestions} = currentError\n const formattedHeader = message ? `${errorSuffix || 'Error'}: ${message}` : undefined\n const formattedCode = code ? `Code: ${code}` : undefined\n const formattedSuggestions = formatSuggestions(suggestions)\n const formattedReference = ref ? `Reference: ${ref}` : undefined\n const formatted = [formattedHeader, formattedCode, formattedSuggestions, formattedReference]\n .filter(Boolean)\n .join('\\n')\n\n prettyPrintedErrors.push(`${isDeep ? 'Caused by: ' : ''}${formatted}`)\n isDeep = true\n currentError = currentError.cause ?? null\n }\n\n return prettyPrintedErrors.join('\\n')\n}\n"],"names":["formatSuggestions","suggestions","label","length","undefined","multiple","map","suggestion","join","isCombinedError","error","prettyPrintError","prettyPrintedErrors","currentError","isDeep","code","message","name","errorSuffix","ref","formattedHeader","formattedCode","formattedSuggestions","formattedReference","formatted","filter","Boolean","push","cause"],"mappings":"AAEA,MAAMA,oBAAoB,CAACC;IACzB,MAAMC,QAAQ;IACd,IAAI,CAACD,eAAeA,YAAYE,MAAM,KAAK,GAAG,OAAOC;IACrD,IAAIH,YAAYE,MAAM,KAAK,GAAG,OAAO,GAAGD,MAAM,CAAC,EAAED,WAAW,CAAC,EAAE,EAAE;IAEjE,MAAMI,WAAWJ,YAAYK,GAAG,CAAC,CAACC,aAAe,CAAC,IAAI,EAAEA,YAAY,EAAEC,IAAI,CAAC;IAC3E,OAAO,GAAGN,MAAM,EAAE,EAAEG,UAAU;AAChC;AAIA,SAASI,gBAAgBC,KAAc;IACrC,OAAOA,UAAU,QAAQ,OAAOA,UAAU,YAAY,UAAUA,SAAS,aAAaA;AACxF;AAEA;;;;;CAKC,GACD,OAAO,SAASC,iBAAiBD,KAAc;IAC7C,MAAME,sBAAgC,EAAE;IACxC,IAAIC,eAAeH;IACnB,IAAII,SAAS;IAEb,MAAOL,gBAAgBI,cAAe;QACpC,MAAM,EAACE,IAAI,EAAEC,OAAO,EAAEC,MAAMC,WAAW,EAAEC,GAAG,EAAElB,WAAW,EAAC,GAAGY;QAC7D,MAAMO,kBAAkBJ,UAAU,GAAGE,eAAe,QAAQ,EAAE,EAAEF,SAAS,GAAGZ;QAC5E,MAAMiB,gBAAgBN,OAAO,CAAC,MAAM,EAAEA,MAAM,GAAGX;QAC/C,MAAMkB,uBAAuBtB,kBAAkBC;QAC/C,MAAMsB,qBAAqBJ,MAAM,CAAC,WAAW,EAAEA,KAAK,GAAGf;QACvD,MAAMoB,YAAY;YAACJ;YAAiBC;YAAeC;YAAsBC;SAAmB,CACzFE,MAAM,CAACC,SACPlB,IAAI,CAAC;QAERI,oBAAoBe,IAAI,CAAC,GAAGb,SAAS,gBAAgB,KAAKU,WAAW;QACrEV,SAAS;QACTD,eAAeA,aAAae,KAAK,IAAI;IACvC;IAEA,OAAOhB,oBAAoBJ,IAAI,CAAC;AAClC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { getCliToken, getGlobalCliClient, getUserConfig } from '@sanity/cli-core';
|
|
3
|
+
import { getCliExecutionContext } from '@sanity/cli-core/executionContext';
|
|
3
4
|
import { telemetryDebug } from '../actions/telemetry/telemetryDebug.js';
|
|
4
5
|
import { createExpiringConfig } from '../util/createExpiringConfig.js';
|
|
5
6
|
export const TELEMETRY_API_VERSION = 'v2026-01-22';
|
|
@@ -74,6 +75,9 @@ const FIVE_MINUTES = 1000 * 60 * 5;
|
|
|
74
75
|
*
|
|
75
76
|
* @internal
|
|
76
77
|
*/ export async function fetchTelemetryConsent() {
|
|
78
|
+
// Programmatic invocations must not use the host user's disk-backed consent
|
|
79
|
+
// cache. The request is already authenticated with the invocation token.
|
|
80
|
+
if (getCliExecutionContext()) return getTelemetryConsent();
|
|
77
81
|
const token = await getCliToken();
|
|
78
82
|
const cacheKey = getTelemetryConsentCacheKey(token);
|
|
79
83
|
// NOTE: createExpiringConfig is instantiated on every call, so in-flight request
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/services/telemetry.ts"],"sourcesContent":["import {createHash} from 'node:crypto'\n\nimport {getCliToken, getGlobalCliClient, getUserConfig} from '@sanity/cli-core'\nimport {type TelemetryEvent} from '@sanity/telemetry'\n\nimport {telemetryDebug} from '../actions/telemetry/telemetryDebug.js'\nimport {createExpiringConfig} from '../util/createExpiringConfig.js'\n\nexport const TELEMETRY_API_VERSION = 'v2026-01-22'\n\nexport const VALID_API_STATUSES = ['granted', 'denied', 'unset'] as const\nexport type ValidApiConsentStatus = (typeof VALID_API_STATUSES)[number]\n\nexport async function sendEvents(batch: TelemetryEvent[]) {\n const client = await getGlobalCliClient({\n apiVersion: TELEMETRY_API_VERSION,\n requireUser: true,\n })\n\n const projectId = process.env.SANITY_TELEMETRY_PROJECT_ID\n\n return client.request({\n body: {batch, projectId},\n json: true,\n method: 'POST',\n uri: '/intake/batch',\n })\n}\n\nasync function getTelemetryConsent(): Promise<{\n status: ValidApiConsentStatus\n}> {\n const client = await getGlobalCliClient({\n apiVersion: TELEMETRY_API_VERSION,\n requireUser: false,\n })\n\n return client.request({tag: 'telemetry-consent', uri: '/intake/telemetry-status'})\n}\n\n/**\n * Check if the given status is a valid consent status\n *\n * @param status - The status to check\n * @returns True if the status is valid, false otherwise\n * @internal\n */\nexport function isValidApiConsentStatus(status: string): status is ValidApiConsentStatus {\n return VALID_API_STATUSES.includes(status as ValidApiConsentStatus)\n}\n\n/**\n * Check if the given response is a valid API consent response\n *\n * @param response - The response to check\n * @returns True if the response is valid, false otherwise\n * @internal\n */\nfunction isValidApiConsentResponse(response: unknown): response is {status: ValidApiConsentStatus} {\n return (\n typeof response === 'object' &&\n response !== null &&\n 'status' in response &&\n typeof response.status === 'string' &&\n isValidApiConsentStatus(response.status)\n )\n}\n\nexport const TELEMETRY_CONSENT_CONFIG_KEY = 'telemetryConsent'\nconst FIVE_MINUTES = 1000 * 60 * 5\n\n/**\n * Get a token-scoped cache key for telemetry consent. This ensures that switching\n * users (via login/logout) always results in a cache miss, preventing one user\n * from inheriting another user's cached consent status.\n *\n * @param token - The current auth token, or undefined if not logged in\n * @returns A cache key scoped to the token\n */\nexport function getTelemetryConsentCacheKey(token: string | undefined): string {\n if (!token) {\n return TELEMETRY_CONSENT_CONFIG_KEY\n }\n\n const hash = createHash('sha256').update(token).digest('hex').slice(0, 12)\n return `${TELEMETRY_CONSENT_CONFIG_KEY}:${hash}`\n}\n\n/**\n * Fetch the telemetry consent status for the current user\n * @returns The telemetry consent status\n *\n * @internal\n */\nexport async function fetchTelemetryConsent(): Promise<{\n status: ValidApiConsentStatus\n}> {\n const token = await getCliToken()\n const cacheKey = getTelemetryConsentCacheKey(token)\n\n // NOTE: createExpiringConfig is instantiated on every call, so in-flight request\n // deduplication (via currentFetch) does not work across concurrent calls to\n // fetchTelemetryConsent(). Two concurrent callers will make two HTTP requests.\n // Consider moving to module-level instance if this becomes a bottleneck.\n const telemetryConsentConfig = createExpiringConfig<{\n status: ValidApiConsentStatus\n }>({\n fetchValue: () => getTelemetryConsent(),\n key: cacheKey,\n onCacheHit() {\n telemetryDebug('Retrieved telemetry consent status from cache')\n },\n onFetch() {\n telemetryDebug('Fetching telemetry consent status...')\n },\n onRevalidate() {\n telemetryDebug('Revalidating cached telemetry consent status...')\n },\n store: getUserConfig(),\n ttl: FIVE_MINUTES,\n validateValue: isValidApiConsentResponse,\n })\n\n return telemetryConsentConfig.get()\n}\n"],"names":["createHash","getCliToken","getGlobalCliClient","getUserConfig","telemetryDebug","createExpiringConfig","TELEMETRY_API_VERSION","VALID_API_STATUSES","sendEvents","batch","client","apiVersion","requireUser","projectId","process","env","SANITY_TELEMETRY_PROJECT_ID","request","body","json","method","uri","getTelemetryConsent","tag","isValidApiConsentStatus","status","includes","isValidApiConsentResponse","response","TELEMETRY_CONSENT_CONFIG_KEY","FIVE_MINUTES","getTelemetryConsentCacheKey","token","hash","update","digest","slice","fetchTelemetryConsent","cacheKey","telemetryConsentConfig","fetchValue","key","onCacheHit","onFetch","onRevalidate","store","ttl","validateValue","get"],"mappings":"AAAA,SAAQA,UAAU,QAAO,cAAa;AAEtC,SAAQC,WAAW,EAAEC,kBAAkB,EAAEC,aAAa,QAAO,mBAAkB;
|
|
1
|
+
{"version":3,"sources":["../../src/services/telemetry.ts"],"sourcesContent":["import {createHash} from 'node:crypto'\n\nimport {getCliToken, getGlobalCliClient, getUserConfig} from '@sanity/cli-core'\nimport {getCliExecutionContext} from '@sanity/cli-core/executionContext'\nimport {type TelemetryEvent} from '@sanity/telemetry'\n\nimport {telemetryDebug} from '../actions/telemetry/telemetryDebug.js'\nimport {createExpiringConfig} from '../util/createExpiringConfig.js'\n\nexport const TELEMETRY_API_VERSION = 'v2026-01-22'\n\nexport const VALID_API_STATUSES = ['granted', 'denied', 'unset'] as const\nexport type ValidApiConsentStatus = (typeof VALID_API_STATUSES)[number]\n\nexport async function sendEvents(batch: TelemetryEvent[]) {\n const client = await getGlobalCliClient({\n apiVersion: TELEMETRY_API_VERSION,\n requireUser: true,\n })\n\n const projectId = process.env.SANITY_TELEMETRY_PROJECT_ID\n\n return client.request({\n body: {batch, projectId},\n json: true,\n method: 'POST',\n uri: '/intake/batch',\n })\n}\n\nasync function getTelemetryConsent(): Promise<{\n status: ValidApiConsentStatus\n}> {\n const client = await getGlobalCliClient({\n apiVersion: TELEMETRY_API_VERSION,\n requireUser: false,\n })\n\n return client.request({tag: 'telemetry-consent', uri: '/intake/telemetry-status'})\n}\n\n/**\n * Check if the given status is a valid consent status\n *\n * @param status - The status to check\n * @returns True if the status is valid, false otherwise\n * @internal\n */\nexport function isValidApiConsentStatus(status: string): status is ValidApiConsentStatus {\n return VALID_API_STATUSES.includes(status as ValidApiConsentStatus)\n}\n\n/**\n * Check if the given response is a valid API consent response\n *\n * @param response - The response to check\n * @returns True if the response is valid, false otherwise\n * @internal\n */\nfunction isValidApiConsentResponse(response: unknown): response is {status: ValidApiConsentStatus} {\n return (\n typeof response === 'object' &&\n response !== null &&\n 'status' in response &&\n typeof response.status === 'string' &&\n isValidApiConsentStatus(response.status)\n )\n}\n\nexport const TELEMETRY_CONSENT_CONFIG_KEY = 'telemetryConsent'\nconst FIVE_MINUTES = 1000 * 60 * 5\n\n/**\n * Get a token-scoped cache key for telemetry consent. This ensures that switching\n * users (via login/logout) always results in a cache miss, preventing one user\n * from inheriting another user's cached consent status.\n *\n * @param token - The current auth token, or undefined if not logged in\n * @returns A cache key scoped to the token\n */\nexport function getTelemetryConsentCacheKey(token: string | undefined): string {\n if (!token) {\n return TELEMETRY_CONSENT_CONFIG_KEY\n }\n\n const hash = createHash('sha256').update(token).digest('hex').slice(0, 12)\n return `${TELEMETRY_CONSENT_CONFIG_KEY}:${hash}`\n}\n\n/**\n * Fetch the telemetry consent status for the current user\n * @returns The telemetry consent status\n *\n * @internal\n */\nexport async function fetchTelemetryConsent(): Promise<{\n status: ValidApiConsentStatus\n}> {\n // Programmatic invocations must not use the host user's disk-backed consent\n // cache. The request is already authenticated with the invocation token.\n if (getCliExecutionContext()) return getTelemetryConsent()\n\n const token = await getCliToken()\n const cacheKey = getTelemetryConsentCacheKey(token)\n\n // NOTE: createExpiringConfig is instantiated on every call, so in-flight request\n // deduplication (via currentFetch) does not work across concurrent calls to\n // fetchTelemetryConsent(). Two concurrent callers will make two HTTP requests.\n // Consider moving to module-level instance if this becomes a bottleneck.\n const telemetryConsentConfig = createExpiringConfig<{\n status: ValidApiConsentStatus\n }>({\n fetchValue: () => getTelemetryConsent(),\n key: cacheKey,\n onCacheHit() {\n telemetryDebug('Retrieved telemetry consent status from cache')\n },\n onFetch() {\n telemetryDebug('Fetching telemetry consent status...')\n },\n onRevalidate() {\n telemetryDebug('Revalidating cached telemetry consent status...')\n },\n store: getUserConfig(),\n ttl: FIVE_MINUTES,\n validateValue: isValidApiConsentResponse,\n })\n\n return telemetryConsentConfig.get()\n}\n"],"names":["createHash","getCliToken","getGlobalCliClient","getUserConfig","getCliExecutionContext","telemetryDebug","createExpiringConfig","TELEMETRY_API_VERSION","VALID_API_STATUSES","sendEvents","batch","client","apiVersion","requireUser","projectId","process","env","SANITY_TELEMETRY_PROJECT_ID","request","body","json","method","uri","getTelemetryConsent","tag","isValidApiConsentStatus","status","includes","isValidApiConsentResponse","response","TELEMETRY_CONSENT_CONFIG_KEY","FIVE_MINUTES","getTelemetryConsentCacheKey","token","hash","update","digest","slice","fetchTelemetryConsent","cacheKey","telemetryConsentConfig","fetchValue","key","onCacheHit","onFetch","onRevalidate","store","ttl","validateValue","get"],"mappings":"AAAA,SAAQA,UAAU,QAAO,cAAa;AAEtC,SAAQC,WAAW,EAAEC,kBAAkB,EAAEC,aAAa,QAAO,mBAAkB;AAC/E,SAAQC,sBAAsB,QAAO,oCAAmC;AAGxE,SAAQC,cAAc,QAAO,yCAAwC;AACrE,SAAQC,oBAAoB,QAAO,kCAAiC;AAEpE,OAAO,MAAMC,wBAAwB,cAAa;AAElD,OAAO,MAAMC,qBAAqB;IAAC;IAAW;IAAU;CAAQ,CAAS;AAGzE,OAAO,eAAeC,WAAWC,KAAuB;IACtD,MAAMC,SAAS,MAAMT,mBAAmB;QACtCU,YAAYL;QACZM,aAAa;IACf;IAEA,MAAMC,YAAYC,QAAQC,GAAG,CAACC,2BAA2B;IAEzD,OAAON,OAAOO,OAAO,CAAC;QACpBC,MAAM;YAACT;YAAOI;QAAS;QACvBM,MAAM;QACNC,QAAQ;QACRC,KAAK;IACP;AACF;AAEA,eAAeC;IAGb,MAAMZ,SAAS,MAAMT,mBAAmB;QACtCU,YAAYL;QACZM,aAAa;IACf;IAEA,OAAOF,OAAOO,OAAO,CAAC;QAACM,KAAK;QAAqBF,KAAK;IAA0B;AAClF;AAEA;;;;;;CAMC,GACD,OAAO,SAASG,wBAAwBC,MAAc;IACpD,OAAOlB,mBAAmBmB,QAAQ,CAACD;AACrC;AAEA;;;;;;CAMC,GACD,SAASE,0BAA0BC,QAAiB;IAClD,OACE,OAAOA,aAAa,YACpBA,aAAa,QACb,YAAYA,YACZ,OAAOA,SAASH,MAAM,KAAK,YAC3BD,wBAAwBI,SAASH,MAAM;AAE3C;AAEA,OAAO,MAAMI,+BAA+B,mBAAkB;AAC9D,MAAMC,eAAe,OAAO,KAAK;AAEjC;;;;;;;CAOC,GACD,OAAO,SAASC,4BAA4BC,KAAyB;IACnE,IAAI,CAACA,OAAO;QACV,OAAOH;IACT;IAEA,MAAMI,OAAOlC,WAAW,UAAUmC,MAAM,CAACF,OAAOG,MAAM,CAAC,OAAOC,KAAK,CAAC,GAAG;IACvE,OAAO,GAAGP,6BAA6B,CAAC,EAAEI,MAAM;AAClD;AAEA;;;;;CAKC,GACD,OAAO,eAAeI;IAGpB,4EAA4E;IAC5E,yEAAyE;IACzE,IAAIlC,0BAA0B,OAAOmB;IAErC,MAAMU,QAAQ,MAAMhC;IACpB,MAAMsC,WAAWP,4BAA4BC;IAE7C,iFAAiF;IACjF,4EAA4E;IAC5E,+EAA+E;IAC/E,yEAAyE;IACzE,MAAMO,yBAAyBlC,qBAE5B;QACDmC,YAAY,IAAMlB;QAClBmB,KAAKH;QACLI;YACEtC,eAAe;QACjB;QACAuC;YACEvC,eAAe;QACjB;QACAwC;YACExC,eAAe;QACjB;QACAyC,OAAO3C;QACP4C,KAAKhB;QACLiB,eAAepB;IACjB;IAEA,OAAOY,uBAAuBS,GAAG;AACnC"}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
import { getCliExecutionContext } from '@sanity/cli-core/executionContext';
|
|
2
|
+
export function getSanityEnv() {
|
|
3
|
+
const context = getCliExecutionContext();
|
|
4
|
+
if (context) return context.sanityEnv ?? 'production';
|
|
5
|
+
return process.env.SANITY_INTERNAL_ENV ?? 'production';
|
|
6
|
+
}
|
|
2
7
|
|
|
3
8
|
//# sourceMappingURL=getSanityEnv.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/util/getSanityEnv.ts"],"sourcesContent":["
|
|
1
|
+
{"version":3,"sources":["../../src/util/getSanityEnv.ts"],"sourcesContent":["import {getCliExecutionContext} from '@sanity/cli-core/executionContext'\n\nexport function getSanityEnv() {\n const context = getCliExecutionContext()\n\n if (context) return context.sanityEnv ?? 'production'\n\n return process.env.SANITY_INTERNAL_ENV ?? 'production'\n}\n"],"names":["getCliExecutionContext","getSanityEnv","context","sanityEnv","process","env","SANITY_INTERNAL_ENV"],"mappings":"AAAA,SAAQA,sBAAsB,QAAO,oCAAmC;AAExE,OAAO,SAASC;IACd,MAAMC,UAAUF;IAEhB,IAAIE,SAAS,OAAOA,QAAQC,SAAS,IAAI;IAEzC,OAAOC,QAAQC,GAAG,CAACC,mBAAmB,IAAI;AAC5C"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { styleText } from 'node:util';
|
|
3
|
+
import { isStaging } from '@sanity/cli-core/util';
|
|
3
4
|
import { logSymbols } from '@sanity/cli-core/ux';
|
|
4
|
-
const baseUrl = process.env.SANITY_INTERNAL_ENV === 'staging' ? 'https://sanity.work' : 'https://www.sanity.io';
|
|
5
5
|
export function warnAboutMissingAppId({ appType, cliConfigPath, output, projectId }) {
|
|
6
|
+
const baseUrl = isStaging() ? 'https://sanity.work' : 'https://www.sanity.io';
|
|
6
7
|
const manageUrl = `${baseUrl}/manage${projectId ? `/project/${projectId}/studios` : ''}`;
|
|
7
8
|
const cliConfigFile = cliConfigPath ? path.basename(cliConfigPath) : 'sanity.cli.ts/.js';
|
|
8
9
|
output.warn(`${logSymbols.warning} No ${styleText('bold', 'appId')} configured. This ${appType} will auto-update to the ${styleText([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/util/warnAboutMissingAppId.ts"],"sourcesContent":["import path from 'node:path'\nimport {styleText} from 'node:util'\n\nimport {type Output} from '@sanity/cli-core'\nimport {
|
|
1
|
+
{"version":3,"sources":["../../src/util/warnAboutMissingAppId.ts"],"sourcesContent":["import path from 'node:path'\nimport {styleText} from 'node:util'\n\nimport {type Output} from '@sanity/cli-core'\nimport {isStaging} from '@sanity/cli-core/util'\nimport {logSymbols} from '@sanity/cli-core/ux'\n\nexport function warnAboutMissingAppId({\n appType,\n cliConfigPath,\n output,\n projectId,\n}: {\n appType: 'app' | 'studio'\n cliConfigPath?: string\n output: Output\n projectId?: string\n}) {\n const baseUrl = isStaging() ? 'https://sanity.work' : 'https://www.sanity.io'\n const manageUrl = `${baseUrl}/manage${projectId ? `/project/${projectId}/studios` : ''}`\n const cliConfigFile = cliConfigPath ? path.basename(cliConfigPath) : 'sanity.cli.ts/.js'\n output.warn(\n `${logSymbols.warning} No ${styleText('bold', 'appId')} configured. This ${appType} will auto-update to the ${styleText(['green', 'bold'], 'latest')} channel. To enable fine grained version selection, head over to ${styleText('cyan', manageUrl)} and add the appId to the ${styleText('bold', 'deployment')} section in ${styleText('bold', cliConfigFile)}.\n `,\n )\n}\n"],"names":["path","styleText","isStaging","logSymbols","warnAboutMissingAppId","appType","cliConfigPath","output","projectId","baseUrl","manageUrl","cliConfigFile","basename","warn","warning"],"mappings":"AAAA,OAAOA,UAAU,YAAW;AAC5B,SAAQC,SAAS,QAAO,YAAW;AAGnC,SAAQC,SAAS,QAAO,wBAAuB;AAC/C,SAAQC,UAAU,QAAO,sBAAqB;AAE9C,OAAO,SAASC,sBAAsB,EACpCC,OAAO,EACPC,aAAa,EACbC,MAAM,EACNC,SAAS,EAMV;IACC,MAAMC,UAAUP,cAAc,wBAAwB;IACtD,MAAMQ,YAAY,GAAGD,QAAQ,OAAO,EAAED,YAAY,CAAC,SAAS,EAAEA,UAAU,QAAQ,CAAC,GAAG,IAAI;IACxF,MAAMG,gBAAgBL,gBAAgBN,KAAKY,QAAQ,CAACN,iBAAiB;IACrEC,OAAOM,IAAI,CACT,GAAGV,WAAWW,OAAO,CAAC,IAAI,EAAEb,UAAU,QAAQ,SAAS,kBAAkB,EAAEI,QAAQ,yBAAyB,EAAEJ,UAAU;QAAC;QAAS;KAAO,EAAE,UAAU,iEAAiE,EAAEA,UAAU,QAAQS,WAAW,0BAA0B,EAAET,UAAU,QAAQ,cAAc,YAAY,EAAEA,UAAU,QAAQU,eAAe;QAC5V,CAAC;AAET"}
|