@sanity/cli 8.22.1 → 8.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/context/watchJob.js +9 -3
- package/dist/actions/context/watchJob.js.map +1 -1
- package/dist/actions/deploy/blueprintManaged.js +59 -0
- package/dist/actions/deploy/blueprintManaged.js.map +1 -0
- package/dist/actions/deploy/deployApp.js +117 -1
- package/dist/actions/deploy/deployApp.js.map +1 -1
- package/dist/actions/deploy/deployRunner.js.map +1 -1
- package/dist/actions/deploy/deployStudio.js +6 -31
- package/dist/actions/deploy/deployStudio.js.map +1 -1
- package/dist/actions/exec/runScript.worker.js +6 -2
- package/dist/actions/exec/runScript.worker.js.map +1 -1
- package/dist/actions/init/bootstrapLocalTemplate.js +35 -11
- package/dist/actions/init/bootstrapLocalTemplate.js.map +1 -1
- package/dist/actions/init/createMediaLibraryCliConfig.js +12 -0
- package/dist/actions/init/createMediaLibraryCliConfig.js.map +1 -0
- package/dist/actions/init/determineMediaLibraryTemplate.js +14 -0
- package/dist/actions/init/determineMediaLibraryTemplate.js.map +1 -0
- package/dist/actions/init/initAction.js +35 -6
- package/dist/actions/init/initAction.js.map +1 -1
- package/dist/actions/init/initApp.js +81 -6
- package/dist/actions/init/initApp.js.map +1 -1
- package/dist/actions/init/offerAspectPull.js +81 -0
- package/dist/actions/init/offerAspectPull.js.map +1 -0
- package/dist/actions/init/project/getProjectDetails.js +25 -2
- package/dist/actions/init/project/getProjectDetails.js.map +1 -1
- package/dist/actions/init/templates/index.js +3 -0
- package/dist/actions/init/templates/index.js.map +1 -1
- package/dist/actions/init/templates/mediaLibraryConfig.js +51 -0
- package/dist/actions/init/templates/mediaLibraryConfig.js.map +1 -0
- package/dist/actions/media/aspectName.js +8 -0
- package/dist/actions/media/aspectName.js.map +1 -0
- package/dist/actions/media/aspectSync/contract.js +163 -0
- package/dist/actions/media/aspectSync/contract.js.map +1 -0
- package/dist/actions/media/aspectSync/syncAspects.js +115 -0
- package/dist/actions/media/aspectSync/syncAspects.js.map +1 -0
- package/dist/actions/media/cliConfigFields.js +192 -0
- package/dist/actions/media/cliConfigFields.js.map +1 -0
- package/dist/actions/media/pullAspects.js +225 -0
- package/dist/actions/media/pullAspects.js.map +1 -0
- package/dist/actions/media/resolveMediaLibraryId.js +83 -0
- package/dist/actions/media/resolveMediaLibraryId.js.map +1 -0
- package/dist/commands/context/build.js +2 -2
- package/dist/commands/context/build.js.map +1 -1
- package/dist/commands/context/entries/get.js +83 -0
- package/dist/commands/context/entries/get.js.map +1 -0
- package/dist/commands/context/entries/list.js +82 -0
- package/dist/commands/context/entries/list.js.map +1 -0
- package/dist/commands/context/entries/rebuild.js +95 -0
- package/dist/commands/context/entries/rebuild.js.map +1 -0
- package/dist/commands/context/issues/apply.js +86 -0
- package/dist/commands/context/issues/apply.js.map +1 -0
- package/dist/commands/context/issues/dismiss.js +52 -0
- package/dist/commands/context/issues/dismiss.js.map +1 -0
- package/dist/commands/context/issues/get.js +105 -0
- package/dist/commands/context/issues/get.js.map +1 -0
- package/dist/commands/context/issues/list.js +128 -0
- package/dist/commands/context/issues/list.js.map +1 -0
- package/dist/commands/context/issues/reopen.js +53 -0
- package/dist/commands/context/issues/reopen.js.map +1 -0
- package/dist/commands/context/issues/resolve.js +137 -0
- package/dist/commands/context/issues/resolve.js.map +1 -0
- package/dist/commands/context/jobs/get.js +2 -2
- package/dist/commands/context/jobs/get.js.map +1 -1
- package/dist/commands/context/query.js +92 -0
- package/dist/commands/context/query.js.map +1 -0
- package/dist/commands/deploy.js +13 -0
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/media/prune-aspect-data.js +170 -0
- package/dist/commands/media/prune-aspect-data.js.map +1 -0
- package/dist/commands/media/pull-aspects.js +309 -0
- package/dist/commands/media/pull-aspects.js.map +1 -0
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +17 -0
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
- package/dist/services/context.js +96 -3
- package/dist/services/context.js.map +1 -1
- package/dist/services/mediaLibraries.js +175 -8
- package/dist/services/mediaLibraries.js.map +1 -1
- package/oclif.manifest.json +2930 -2241
- package/package.json +6 -5
- package/templates/media-library-config/README.md +52 -0
- package/templates/media-library-config/eslint.config.txt +52 -0
- package/templates/media-library-config/src/fields/caption.tsx +29 -0
- package/templates/media-library-config/src/fields/notes.tsx +54 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { styleText } from 'node:util';
|
|
4
|
+
import { Flags } from '@oclif/core';
|
|
5
|
+
import { exitCodes, ProjectRootNotFoundError, SanityCommand, subdebug } from '@sanity/cli-core';
|
|
6
|
+
import { confirm, logSymbols, spinner } from '@sanity/cli-core/ux';
|
|
7
|
+
import { resolveWorkbenchConfig } from '@sanity/workbench-cli/deploy';
|
|
8
|
+
import { appendFieldEntries } from '../../actions/media/cliConfigFields.js';
|
|
9
|
+
import { convertAspectDocuments, formatFieldEntries } from '../../actions/media/pullAspects.js';
|
|
10
|
+
import { resolveMediaLibraryId } from '../../actions/media/resolveMediaLibraryId.js';
|
|
11
|
+
import { getAspects } from '../../services/mediaLibraries.js';
|
|
12
|
+
import { pluralize } from '../../util/pluralize.js';
|
|
13
|
+
import { defineCommandTelemetry } from '../../util/telemetry/commandTelemetry.js';
|
|
14
|
+
const pullAspectsDebug = subdebug('media:pull-aspects');
|
|
15
|
+
const NOT_A_CONFIG_APP = 'This command must be run inside a Media Library config app — a project whose ' + '`sanity.cli.ts` declares `unstable_defineMediaLibrary(...)`. Scaffold one with ' + '`sanity init --template media-library-config`.';
|
|
16
|
+
const flags = {
|
|
17
|
+
'media-library-id': Flags.string({
|
|
18
|
+
description: 'The id of the media library to pull from (defaults to the single active library of the configured organization)'
|
|
19
|
+
}),
|
|
20
|
+
'out-dir': Flags.string({
|
|
21
|
+
default: 'src/fields',
|
|
22
|
+
description: 'Directory to write the generated field modules to, relative to the project root'
|
|
23
|
+
}),
|
|
24
|
+
overwrite: Flags.boolean({
|
|
25
|
+
description: 'Overwrite existing field modules and re-generate fields already declared in the config'
|
|
26
|
+
}),
|
|
27
|
+
'update-config': Flags.boolean({
|
|
28
|
+
allowNo: true,
|
|
29
|
+
description: 'Declare the generated fields in sanity.cli.ts (--no-update-config to skip). Prompted when omitted; unattended runs only print the entries'
|
|
30
|
+
})
|
|
31
|
+
};
|
|
32
|
+
export class MediaPullAspectsCommand extends SanityCommand {
|
|
33
|
+
static description = "Generate field modules from a media library's stored aspect documents";
|
|
34
|
+
static examples = [
|
|
35
|
+
{
|
|
36
|
+
command: '<%= config.bin %> <%= command.id %>',
|
|
37
|
+
description: 'Generate src/fields/<name>.tsx modules from the stored aspects'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
command: '<%= config.bin %> <%= command.id %> --media-library-id ml123 --overwrite',
|
|
41
|
+
description: 'Pull from a specific media library, overwriting existing modules'
|
|
42
|
+
}
|
|
43
|
+
];
|
|
44
|
+
static flags = flags;
|
|
45
|
+
// Part of the beta config-app flow: only usable inside a config app (like
|
|
46
|
+
// the unlisted media-library-config init template, which points to it), and
|
|
47
|
+
// listing it next to the legacy aspect commands would mix the two
|
|
48
|
+
// paradigms. Unhide when the config app leaves beta.
|
|
49
|
+
static hidden = true;
|
|
50
|
+
// --out-dir is a local filesystem path.
|
|
51
|
+
static telemetry = defineCommandTelemetry(flags, {
|
|
52
|
+
redact: [
|
|
53
|
+
'out-dir'
|
|
54
|
+
]
|
|
55
|
+
});
|
|
56
|
+
async run() {
|
|
57
|
+
const { 'media-library-id': mediaLibraryIdFlag, 'out-dir': outDir, overwrite = false } = this.flags;
|
|
58
|
+
let cliConfig;
|
|
59
|
+
try {
|
|
60
|
+
cliConfig = await this.getCliConfig();
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err instanceof ProjectRootNotFoundError) {
|
|
63
|
+
this.error(NOT_A_CONFIG_APP, {
|
|
64
|
+
exit: exitCodes.RUNTIME_ERROR
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
const config = resolveWorkbenchConfig(cliConfig);
|
|
70
|
+
if (!config) {
|
|
71
|
+
this.error(NOT_A_CONFIG_APP, {
|
|
72
|
+
exit: exitCodes.RUNTIME_ERROR
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const root = await this.getProjectRoot();
|
|
76
|
+
// The generated `src:` entries and the ownership story both assume the
|
|
77
|
+
// modules live inside the project; a path escaping the root is a mistake.
|
|
78
|
+
// `outDirRelative` (not the raw flag) feeds the report so an absolute
|
|
79
|
+
// --out-dir inside the root still prints proper relative `src:` entries.
|
|
80
|
+
const outDirAbsolute = path.resolve(root.directory, outDir);
|
|
81
|
+
const outDirRelative = path.relative(root.directory, outDirAbsolute);
|
|
82
|
+
if (path.isAbsolute(outDirRelative) || outDirRelative === '..' || outDirRelative.startsWith(`..${path.sep}`)) {
|
|
83
|
+
this.error(`--out-dir must stay inside the project root (got "${outDir}")`, {
|
|
84
|
+
exit: exitCodes.USAGE_ERROR
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const mediaLibraryId = await resolveMediaLibraryId({
|
|
89
|
+
mediaLibraryIdFlag,
|
|
90
|
+
organizationId: config.organizationId,
|
|
91
|
+
unattended: this.isUnattended()
|
|
92
|
+
});
|
|
93
|
+
const spin = spinner('Fetching stored aspect documents').start();
|
|
94
|
+
let documents;
|
|
95
|
+
try {
|
|
96
|
+
documents = await getAspects(mediaLibraryId);
|
|
97
|
+
} finally{
|
|
98
|
+
// Also on failure — a spinner left running garbles the error output.
|
|
99
|
+
spin.stop();
|
|
100
|
+
}
|
|
101
|
+
if (documents.length === 0) {
|
|
102
|
+
this.log(`Media library ${styleText('bold', mediaLibraryId)} has no stored aspects — nothing to pull.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Names already declared in the config are owned by existing field
|
|
106
|
+
// modules; pulling the stored mirror back over them would regress local
|
|
107
|
+
// edits, so they are skipped unless --overwrite says otherwise.
|
|
108
|
+
const configuredNames = new Set(config.fields.map((field)=>field.name));
|
|
109
|
+
const configuredSrcByName = new Map(config.fields.map((field)=>[
|
|
110
|
+
field.name,
|
|
111
|
+
field.src
|
|
112
|
+
]));
|
|
113
|
+
// One module per field name; name collisions and invalid documents are skipped.
|
|
114
|
+
const { modules: winners, skipped } = convertAspectDocuments(documents);
|
|
115
|
+
const generated = [];
|
|
116
|
+
// Module name -> written path relative to the project root, so the
|
|
117
|
+
// report shows where each module actually landed.
|
|
118
|
+
const writtenPaths = new Map();
|
|
119
|
+
// Module name -> path of a module skipped because it already exists on
|
|
120
|
+
// disk. Those fields may still be undeclared and need registration.
|
|
121
|
+
const existingPaths = new Map();
|
|
122
|
+
for (const module of winners){
|
|
123
|
+
if (!overwrite && configuredNames.has(module.name)) {
|
|
124
|
+
skipped.push({
|
|
125
|
+
id: module.name,
|
|
126
|
+
reasons: [
|
|
127
|
+
'Already declared in sanity.cli.ts (pass --overwrite to re-generate)'
|
|
128
|
+
],
|
|
129
|
+
status: 'skipped',
|
|
130
|
+
validationErrors: []
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const defaultDestination = path.join(outDirAbsolute, module.filename);
|
|
135
|
+
// The pull generates `.tsx`, but a hand-written module for the same
|
|
136
|
+
// field may be plain `.ts` — treat both as taken.
|
|
137
|
+
const existing = await firstExisting([
|
|
138
|
+
defaultDestination,
|
|
139
|
+
defaultDestination.replace(/\.tsx$/, '.ts')
|
|
140
|
+
]);
|
|
141
|
+
if (!overwrite && existing) {
|
|
142
|
+
// The file is left alone, but the field is undeclared (declared
|
|
143
|
+
// names were skipped above) — so it still joins the registration
|
|
144
|
+
// offer below. An on-disk module without a config entry is exactly
|
|
145
|
+
// the state the next deploy punishes, however the file got there.
|
|
146
|
+
existingPaths.set(module.name, path.relative(root.directory, existing));
|
|
147
|
+
skipped.push({
|
|
148
|
+
id: module.name,
|
|
149
|
+
reasons: [
|
|
150
|
+
`A file already exists at ${path.relative(root.directory, existing)} (pass --overwrite to replace it)`
|
|
151
|
+
],
|
|
152
|
+
status: 'skipped',
|
|
153
|
+
validationErrors: []
|
|
154
|
+
});
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// When overwriting a field the config already declares, the module
|
|
158
|
+
// must land at the declared `src` path — writing <out-dir>/<name>.tsx
|
|
159
|
+
// instead would orphan the new file while deploy keeps evaluating the
|
|
160
|
+
// old one. Failing that, replace the module found on disk (which may
|
|
161
|
+
// be the .ts variant), and only then fall back to the default.
|
|
162
|
+
const declaredSrc = overwrite ? configuredSrcByName.get(module.name) : undefined;
|
|
163
|
+
const destination = declaredSrc ? await resolveDeclaredSrc(path.resolve(root.directory, declaredSrc), module.filename) : existing ?? defaultDestination;
|
|
164
|
+
await mkdir(path.dirname(destination), {
|
|
165
|
+
recursive: true
|
|
166
|
+
});
|
|
167
|
+
await writeFile(destination, module.source);
|
|
168
|
+
writtenPaths.set(module.name, path.relative(root.directory, destination));
|
|
169
|
+
generated.push(module);
|
|
170
|
+
}
|
|
171
|
+
// Fields present locally — written this run or already on disk — but not
|
|
172
|
+
// declared get offered for registration: the deploy mirrors config to
|
|
173
|
+
// stored documents, so an undeclared stored aspect is deleted by the
|
|
174
|
+
// next deploy — leaving the entries unpasted is a data hazard, not just
|
|
175
|
+
// friction. Existing files count because a prior pull may have been
|
|
176
|
+
// declined (or edited by hand): the file being there does not make the
|
|
177
|
+
// field declared.
|
|
178
|
+
const entryPaths = new Map([
|
|
179
|
+
...writtenPaths,
|
|
180
|
+
...existingPaths
|
|
181
|
+
]);
|
|
182
|
+
const newEntries = winners.filter((module)=>!configuredNames.has(module.name) && entryPaths.has(module.name));
|
|
183
|
+
const entryLines = newEntries.length > 0 ? formatFieldEntries(newEntries, outDirRelative, entryPaths).split('\n') : [];
|
|
184
|
+
const registration = newEntries.length > 0 ? await this.maybeRegisterFields({
|
|
185
|
+
configPath: root.path,
|
|
186
|
+
entryLines
|
|
187
|
+
}) : null;
|
|
188
|
+
this.report({
|
|
189
|
+
configuredNames,
|
|
190
|
+
entryLines,
|
|
191
|
+
generated,
|
|
192
|
+
newEntries,
|
|
193
|
+
outDir: outDirRelative,
|
|
194
|
+
registration,
|
|
195
|
+
skipped,
|
|
196
|
+
writtenPaths
|
|
197
|
+
});
|
|
198
|
+
} catch (error) {
|
|
199
|
+
// Ctrl+C on the library prompt and errors that already carry their exit
|
|
200
|
+
// code (`this.error` inside helpers) pass through to SanityCommand.catch.
|
|
201
|
+
if (error instanceof Error && error.name === 'ExitPromptError') throw error;
|
|
202
|
+
if (error && typeof error === 'object' && 'oclif' in error) throw error;
|
|
203
|
+
const err = error;
|
|
204
|
+
pullAspectsDebug('Failed to pull aspects', {
|
|
205
|
+
error: err,
|
|
206
|
+
mediaLibraryId: mediaLibraryIdFlag
|
|
207
|
+
});
|
|
208
|
+
this.error(styleText('bold', 'Failed to pull aspects') + `\n\n${styleText('red', err.message)}`, {
|
|
209
|
+
exit: exitCodes.RUNTIME_ERROR
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Offer to declare the generated fields in `sanity.cli.ts`. `--update-config`
|
|
215
|
+
* / `--no-update-config` decide outright; otherwise an interactive run
|
|
216
|
+
* prompts (default yes) and an unattended run leaves the config alone.
|
|
217
|
+
*/ async maybeRegisterFields({ configPath, entryLines }) {
|
|
218
|
+
const flag = this.flags['update-config'];
|
|
219
|
+
const shouldUpdate = flag ?? (this.isUnattended() ? false : await confirm({
|
|
220
|
+
default: true,
|
|
221
|
+
message: `Declare the ${entryLines.length} pulled ${pluralize('field', entryLines.length)} in sanity.cli.ts?`
|
|
222
|
+
}));
|
|
223
|
+
if (!shouldUpdate) return 'declined';
|
|
224
|
+
const source = await readFile(configPath, 'utf8');
|
|
225
|
+
const result = appendFieldEntries(source, entryLines);
|
|
226
|
+
if (!result.ok) return {
|
|
227
|
+
reason: result.reason
|
|
228
|
+
};
|
|
229
|
+
await writeFile(configPath, result.updated);
|
|
230
|
+
return 'added';
|
|
231
|
+
}
|
|
232
|
+
report({ configuredNames, entryLines, generated, newEntries, outDir, registration, skipped, writtenPaths }) {
|
|
233
|
+
if (skipped.length > 0) {
|
|
234
|
+
this.logToStderr('');
|
|
235
|
+
this.warn(styleText('bold', `Skipped ${skipped.length} ${pluralize('aspect', skipped.length)}`));
|
|
236
|
+
for (const aspect of skipped){
|
|
237
|
+
const errors = aspect.validationErrors.flatMap((group)=>group.map(({ message })=>message));
|
|
238
|
+
const reasons = [
|
|
239
|
+
...aspect.reasons,
|
|
240
|
+
...errors
|
|
241
|
+
];
|
|
242
|
+
this.logToStderr(` - ${aspect.id} ${styleText('dim', reasons.join('; '))}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// No modules written and nothing to declare: done. Skipped-but-undeclared
|
|
246
|
+
// fields keep the report alive — their registration outcome still matters.
|
|
247
|
+
if (generated.length === 0 && newEntries.length === 0) {
|
|
248
|
+
this.log('');
|
|
249
|
+
this.log('No field modules generated.');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (generated.length === 0) {
|
|
253
|
+
this.log('');
|
|
254
|
+
this.log('No field modules generated.');
|
|
255
|
+
} else {
|
|
256
|
+
this.log('');
|
|
257
|
+
this.log(`${logSymbols.success} ${styleText('bold', `Generated ${generated.length} field ${pluralize('module', generated.length)}`)}`);
|
|
258
|
+
for (const module of generated){
|
|
259
|
+
const location = writtenPaths.get(module.name) ?? path.join(outDir, module.filename);
|
|
260
|
+
this.log(` - ${module.name} ${styleText('dim', location)}`);
|
|
261
|
+
for (const warning of module.warnings){
|
|
262
|
+
this.log(` ${logSymbols.warning} ${warning}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (newEntries.length > 0 && registration === 'added') {
|
|
267
|
+
this.log('');
|
|
268
|
+
this.log(`${logSymbols.success} Declared ${newEntries.length} ${pluralize('field', newEntries.length)} in ${styleText('bold', 'sanity.cli.ts')}`);
|
|
269
|
+
} else if (newEntries.length > 0) {
|
|
270
|
+
if (registration !== null && typeof registration === 'object') {
|
|
271
|
+
this.warn(`Could not update sanity.cli.ts automatically: ${registration.reason}`);
|
|
272
|
+
}
|
|
273
|
+
this.log('');
|
|
274
|
+
this.log(configuredNames.size > 0 ? `Add the pulled ${pluralize('field', newEntries.length)} to the \`fields\` array in ${styleText('bold', 'sanity.cli.ts')}:` : `Declare the pulled ${pluralize('field', newEntries.length)} in ${styleText('bold', 'sanity.cli.ts')}:`);
|
|
275
|
+
this.log('');
|
|
276
|
+
this.log(entryLines.map((line)=>` ${line}`).join('\n'));
|
|
277
|
+
// The deploy mirrors config into stored documents; a pulled-but-undeclared
|
|
278
|
+
// field reads as stale there, so leaving the entries out is destructive.
|
|
279
|
+
this.warn('`sanity deploy` deletes the stored definition of any field not declared in sanity.cli.ts.');
|
|
280
|
+
}
|
|
281
|
+
this.log('');
|
|
282
|
+
this.log('Once declared in sanity.cli.ts, the config app owns these definitions — deploying the');
|
|
283
|
+
this.log('config overwrites the stored documents this pull read from. Stored definitions');
|
|
284
|
+
this.log('not declared in sanity.cli.ts are deleted on deploy.');
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* The write target for a declared `src` path. A declared path may omit the
|
|
289
|
+
* extension — the bundler and jiti both resolve `./src/fields/notes` to
|
|
290
|
+
* `notes.ts(x)` — so writing it literally would create an extensionless file
|
|
291
|
+
* next to the module deploy actually evaluates. Resolve to the existing
|
|
292
|
+
* module file; if none exists, append the generated module's extension.
|
|
293
|
+
*/ async function resolveDeclaredSrc(declaredPath, moduleFilename) {
|
|
294
|
+
if (path.extname(declaredPath) !== '') return declaredPath;
|
|
295
|
+
const existing = await firstExisting([
|
|
296
|
+
`${declaredPath}.ts`,
|
|
297
|
+
`${declaredPath}.tsx`
|
|
298
|
+
]);
|
|
299
|
+
return existing ?? `${declaredPath}${path.extname(moduleFilename)}`;
|
|
300
|
+
}
|
|
301
|
+
/** The first path in `paths` that exists, or `undefined`. */ async function firstExisting(paths) {
|
|
302
|
+
for (const filePath of paths){
|
|
303
|
+
const exists = await access(filePath).then(()=>true, ()=>false);
|
|
304
|
+
if (exists) return filePath;
|
|
305
|
+
}
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
//# sourceMappingURL=pull-aspects.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/media/pull-aspects.ts"],"sourcesContent":["import {access, mkdir, readFile, writeFile} from 'node:fs/promises'\nimport path from 'node:path'\nimport {styleText} from 'node:util'\n\nimport {Flags} from '@oclif/core'\nimport {\n type CliConfig,\n exitCodes,\n ProjectRootNotFoundError,\n SanityCommand,\n subdebug,\n} from '@sanity/cli-core'\nimport {confirm, logSymbols, spinner} from '@sanity/cli-core/ux'\nimport {resolveWorkbenchConfig} from '@sanity/workbench-cli/deploy'\n\nimport {appendFieldEntries} from '../../actions/media/cliConfigFields.js'\nimport {\n convertAspectDocuments,\n formatFieldEntries,\n type GeneratedFieldModule,\n type SkippedAspect,\n} from '../../actions/media/pullAspects.js'\nimport {resolveMediaLibraryId} from '../../actions/media/resolveMediaLibraryId.js'\nimport {getAspects} from '../../services/mediaLibraries.js'\nimport {pluralize} from '../../util/pluralize.js'\nimport {defineCommandTelemetry} from '../../util/telemetry/commandTelemetry.js'\n\nconst pullAspectsDebug = subdebug('media:pull-aspects')\n\nconst NOT_A_CONFIG_APP =\n 'This command must be run inside a Media Library config app — a project whose ' +\n '`sanity.cli.ts` declares `unstable_defineMediaLibrary(...)`. Scaffold one with ' +\n '`sanity init --template media-library-config`.'\n\nconst flags = {\n 'media-library-id': Flags.string({\n description:\n 'The id of the media library to pull from (defaults to the single active library of the configured organization)',\n }),\n 'out-dir': Flags.string({\n default: 'src/fields',\n description: 'Directory to write the generated field modules to, relative to the project root',\n }),\n overwrite: Flags.boolean({\n description:\n 'Overwrite existing field modules and re-generate fields already declared in the config',\n }),\n 'update-config': Flags.boolean({\n allowNo: true,\n description:\n 'Declare the generated fields in sanity.cli.ts (--no-update-config to skip). Prompted when omitted; unattended runs only print the entries',\n }),\n}\n\n/**\n * The outcome of offering to declare the generated fields in `sanity.cli.ts`:\n * written, turned down (flag, prompt, or unattended), or not writable (the\n * config shape could not be edited safely — the `reason` says why).\n */\ntype Registration = 'added' | 'declined' | {reason: string}\n\nexport class MediaPullAspectsCommand extends SanityCommand<typeof MediaPullAspectsCommand> {\n static override description =\n \"Generate field modules from a media library's stored aspect documents\"\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Generate src/fields/<name>.tsx modules from the stored aspects',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --media-library-id ml123 --overwrite',\n description: 'Pull from a specific media library, overwriting existing modules',\n },\n ]\n\n static override flags = flags\n\n // Part of the beta config-app flow: only usable inside a config app (like\n // the unlisted media-library-config init template, which points to it), and\n // listing it next to the legacy aspect commands would mix the two\n // paradigms. Unhide when the config app leaves beta.\n static override hidden = true\n\n // --out-dir is a local filesystem path.\n static telemetry = defineCommandTelemetry(flags, {redact: ['out-dir']})\n\n public async run(): Promise<void> {\n const {\n 'media-library-id': mediaLibraryIdFlag,\n 'out-dir': outDir,\n overwrite = false,\n } = this.flags\n\n let cliConfig: CliConfig\n try {\n cliConfig = await this.getCliConfig()\n } catch (err) {\n if (err instanceof ProjectRootNotFoundError) {\n this.error(NOT_A_CONFIG_APP, {exit: exitCodes.RUNTIME_ERROR})\n }\n throw err\n }\n\n const config = resolveWorkbenchConfig(cliConfig)\n if (!config) {\n this.error(NOT_A_CONFIG_APP, {exit: exitCodes.RUNTIME_ERROR})\n }\n\n const root = await this.getProjectRoot()\n // The generated `src:` entries and the ownership story both assume the\n // modules live inside the project; a path escaping the root is a mistake.\n // `outDirRelative` (not the raw flag) feeds the report so an absolute\n // --out-dir inside the root still prints proper relative `src:` entries.\n const outDirAbsolute = path.resolve(root.directory, outDir)\n const outDirRelative = path.relative(root.directory, outDirAbsolute)\n if (\n path.isAbsolute(outDirRelative) ||\n outDirRelative === '..' ||\n outDirRelative.startsWith(`..${path.sep}`)\n ) {\n this.error(`--out-dir must stay inside the project root (got \"${outDir}\")`, {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n\n try {\n const mediaLibraryId = await resolveMediaLibraryId({\n mediaLibraryIdFlag,\n organizationId: config.organizationId,\n unattended: this.isUnattended(),\n })\n\n const spin = spinner('Fetching stored aspect documents').start()\n let documents: Awaited<ReturnType<typeof getAspects>>\n try {\n documents = await getAspects(mediaLibraryId)\n } finally {\n // Also on failure — a spinner left running garbles the error output.\n spin.stop()\n }\n\n if (documents.length === 0) {\n this.log(\n `Media library ${styleText('bold', mediaLibraryId)} has no stored aspects — nothing to pull.`,\n )\n return\n }\n\n // Names already declared in the config are owned by existing field\n // modules; pulling the stored mirror back over them would regress local\n // edits, so they are skipped unless --overwrite says otherwise.\n const configuredNames = new Set(config.fields.map((field) => field.name))\n const configuredSrcByName = new Map(config.fields.map((field) => [field.name, field.src]))\n\n // One module per field name; name collisions and invalid documents are skipped.\n const {modules: winners, skipped} = convertAspectDocuments(documents)\n\n const generated: GeneratedFieldModule[] = []\n // Module name -> written path relative to the project root, so the\n // report shows where each module actually landed.\n const writtenPaths = new Map<string, string>()\n // Module name -> path of a module skipped because it already exists on\n // disk. Those fields may still be undeclared and need registration.\n const existingPaths = new Map<string, string>()\n for (const module of winners) {\n if (!overwrite && configuredNames.has(module.name)) {\n skipped.push({\n id: module.name,\n reasons: ['Already declared in sanity.cli.ts (pass --overwrite to re-generate)'],\n status: 'skipped',\n validationErrors: [],\n })\n continue\n }\n\n const defaultDestination = path.join(outDirAbsolute, module.filename)\n // The pull generates `.tsx`, but a hand-written module for the same\n // field may be plain `.ts` — treat both as taken.\n const existing = await firstExisting([\n defaultDestination,\n defaultDestination.replace(/\\.tsx$/, '.ts'),\n ])\n if (!overwrite && existing) {\n // The file is left alone, but the field is undeclared (declared\n // names were skipped above) — so it still joins the registration\n // offer below. An on-disk module without a config entry is exactly\n // the state the next deploy punishes, however the file got there.\n existingPaths.set(module.name, path.relative(root.directory, existing))\n skipped.push({\n id: module.name,\n reasons: [\n `A file already exists at ${path.relative(root.directory, existing)} (pass --overwrite to replace it)`,\n ],\n status: 'skipped',\n validationErrors: [],\n })\n continue\n }\n\n // When overwriting a field the config already declares, the module\n // must land at the declared `src` path — writing <out-dir>/<name>.tsx\n // instead would orphan the new file while deploy keeps evaluating the\n // old one. Failing that, replace the module found on disk (which may\n // be the .ts variant), and only then fall back to the default.\n const declaredSrc = overwrite ? configuredSrcByName.get(module.name) : undefined\n const destination = declaredSrc\n ? await resolveDeclaredSrc(path.resolve(root.directory, declaredSrc), module.filename)\n : (existing ?? defaultDestination)\n\n await mkdir(path.dirname(destination), {recursive: true})\n await writeFile(destination, module.source)\n writtenPaths.set(module.name, path.relative(root.directory, destination))\n generated.push(module)\n }\n\n // Fields present locally — written this run or already on disk — but not\n // declared get offered for registration: the deploy mirrors config to\n // stored documents, so an undeclared stored aspect is deleted by the\n // next deploy — leaving the entries unpasted is a data hazard, not just\n // friction. Existing files count because a prior pull may have been\n // declined (or edited by hand): the file being there does not make the\n // field declared.\n const entryPaths = new Map([...writtenPaths, ...existingPaths])\n const newEntries = winners.filter(\n (module) => !configuredNames.has(module.name) && entryPaths.has(module.name),\n )\n const entryLines =\n newEntries.length > 0\n ? formatFieldEntries(newEntries, outDirRelative, entryPaths).split('\\n')\n : []\n const registration =\n newEntries.length > 0\n ? await this.maybeRegisterFields({configPath: root.path, entryLines})\n : null\n\n this.report({\n configuredNames,\n entryLines,\n generated,\n newEntries,\n outDir: outDirRelative,\n registration,\n skipped,\n writtenPaths,\n })\n } catch (error) {\n // Ctrl+C on the library prompt and errors that already carry their exit\n // code (`this.error` inside helpers) pass through to SanityCommand.catch.\n if (error instanceof Error && error.name === 'ExitPromptError') throw error\n if (error && typeof error === 'object' && 'oclif' in error) throw error\n const err = error as Error\n pullAspectsDebug('Failed to pull aspects', {error: err, mediaLibraryId: mediaLibraryIdFlag})\n this.error(\n styleText('bold', 'Failed to pull aspects') + `\\n\\n${styleText('red', err.message)}`,\n {exit: exitCodes.RUNTIME_ERROR},\n )\n }\n }\n\n /**\n * Offer to declare the generated fields in `sanity.cli.ts`. `--update-config`\n * / `--no-update-config` decide outright; otherwise an interactive run\n * prompts (default yes) and an unattended run leaves the config alone.\n */\n private async maybeRegisterFields({\n configPath,\n entryLines,\n }: {\n configPath: string\n entryLines: string[]\n }): Promise<Registration> {\n const flag = this.flags['update-config']\n const shouldUpdate =\n flag ??\n (this.isUnattended()\n ? false\n : await confirm({\n default: true,\n message: `Declare the ${entryLines.length} pulled ${pluralize('field', entryLines.length)} in sanity.cli.ts?`,\n }))\n if (!shouldUpdate) return 'declined'\n\n const source = await readFile(configPath, 'utf8')\n const result = appendFieldEntries(source, entryLines)\n if (!result.ok) return {reason: result.reason}\n await writeFile(configPath, result.updated)\n return 'added'\n }\n\n private report({\n configuredNames,\n entryLines,\n generated,\n newEntries,\n outDir,\n registration,\n skipped,\n writtenPaths,\n }: {\n configuredNames: Set<string>\n entryLines: string[]\n generated: GeneratedFieldModule[]\n newEntries: GeneratedFieldModule[]\n outDir: string\n registration: Registration | null\n skipped: SkippedAspect[]\n writtenPaths: Map<string, string>\n }): void {\n if (skipped.length > 0) {\n this.logToStderr('')\n this.warn(\n styleText('bold', `Skipped ${skipped.length} ${pluralize('aspect', skipped.length)}`),\n )\n for (const aspect of skipped) {\n const errors = aspect.validationErrors.flatMap((group) => group.map(({message}) => message))\n const reasons = [...aspect.reasons, ...errors]\n this.logToStderr(` - ${aspect.id} ${styleText('dim', reasons.join('; '))}`)\n }\n }\n\n // No modules written and nothing to declare: done. Skipped-but-undeclared\n // fields keep the report alive — their registration outcome still matters.\n if (generated.length === 0 && newEntries.length === 0) {\n this.log('')\n this.log('No field modules generated.')\n return\n }\n\n if (generated.length === 0) {\n this.log('')\n this.log('No field modules generated.')\n } else {\n this.log('')\n this.log(\n `${logSymbols.success} ${styleText('bold', `Generated ${generated.length} field ${pluralize('module', generated.length)}`)}`,\n )\n for (const module of generated) {\n const location = writtenPaths.get(module.name) ?? path.join(outDir, module.filename)\n this.log(` - ${module.name} ${styleText('dim', location)}`)\n for (const warning of module.warnings) {\n this.log(` ${logSymbols.warning} ${warning}`)\n }\n }\n }\n\n if (newEntries.length > 0 && registration === 'added') {\n this.log('')\n this.log(\n `${logSymbols.success} Declared ${newEntries.length} ${pluralize('field', newEntries.length)} in ${styleText('bold', 'sanity.cli.ts')}`,\n )\n } else if (newEntries.length > 0) {\n if (registration !== null && typeof registration === 'object') {\n this.warn(`Could not update sanity.cli.ts automatically: ${registration.reason}`)\n }\n this.log('')\n this.log(\n configuredNames.size > 0\n ? `Add the pulled ${pluralize('field', newEntries.length)} to the \\`fields\\` array in ${styleText('bold', 'sanity.cli.ts')}:`\n : `Declare the pulled ${pluralize('field', newEntries.length)} in ${styleText('bold', 'sanity.cli.ts')}:`,\n )\n this.log('')\n this.log(entryLines.map((line) => ` ${line}`).join('\\n'))\n // The deploy mirrors config into stored documents; a pulled-but-undeclared\n // field reads as stale there, so leaving the entries out is destructive.\n this.warn(\n '`sanity deploy` deletes the stored definition of any field not declared in sanity.cli.ts.',\n )\n }\n\n this.log('')\n this.log(\n 'Once declared in sanity.cli.ts, the config app owns these definitions — deploying the',\n )\n this.log('config overwrites the stored documents this pull read from. Stored definitions')\n this.log('not declared in sanity.cli.ts are deleted on deploy.')\n }\n}\n\n/**\n * The write target for a declared `src` path. A declared path may omit the\n * extension — the bundler and jiti both resolve `./src/fields/notes` to\n * `notes.ts(x)` — so writing it literally would create an extensionless file\n * next to the module deploy actually evaluates. Resolve to the existing\n * module file; if none exists, append the generated module's extension.\n */\nasync function resolveDeclaredSrc(declaredPath: string, moduleFilename: string): Promise<string> {\n if (path.extname(declaredPath) !== '') return declaredPath\n const existing = await firstExisting([`${declaredPath}.ts`, `${declaredPath}.tsx`])\n return existing ?? `${declaredPath}${path.extname(moduleFilename)}`\n}\n\n/** The first path in `paths` that exists, or `undefined`. */\nasync function firstExisting(paths: string[]): Promise<string | undefined> {\n for (const filePath of paths) {\n const exists = await access(filePath).then(\n () => true,\n () => false,\n )\n if (exists) return filePath\n }\n return undefined\n}\n"],"names":["access","mkdir","readFile","writeFile","path","styleText","Flags","exitCodes","ProjectRootNotFoundError","SanityCommand","subdebug","confirm","logSymbols","spinner","resolveWorkbenchConfig","appendFieldEntries","convertAspectDocuments","formatFieldEntries","resolveMediaLibraryId","getAspects","pluralize","defineCommandTelemetry","pullAspectsDebug","NOT_A_CONFIG_APP","flags","string","description","default","overwrite","boolean","allowNo","MediaPullAspectsCommand","examples","command","hidden","telemetry","redact","run","mediaLibraryIdFlag","outDir","cliConfig","getCliConfig","err","error","exit","RUNTIME_ERROR","config","root","getProjectRoot","outDirAbsolute","resolve","directory","outDirRelative","relative","isAbsolute","startsWith","sep","USAGE_ERROR","mediaLibraryId","organizationId","unattended","isUnattended","spin","start","documents","stop","length","log","configuredNames","Set","fields","map","field","name","configuredSrcByName","Map","src","modules","winners","skipped","generated","writtenPaths","existingPaths","module","has","push","id","reasons","status","validationErrors","defaultDestination","join","filename","existing","firstExisting","replace","set","declaredSrc","get","undefined","destination","resolveDeclaredSrc","dirname","recursive","source","entryPaths","newEntries","filter","entryLines","split","registration","maybeRegisterFields","configPath","report","Error","message","flag","shouldUpdate","result","ok","reason","updated","logToStderr","warn","aspect","errors","flatMap","group","success","location","warning","warnings","size","line","declaredPath","moduleFilename","extname","paths","filePath","exists","then"],"mappings":"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"}
|
|
@@ -71,6 +71,10 @@ function apiValidator({ args, flags }) {
|
|
|
71
71
|
'context:build': conditionalDenyFlags('watch'),
|
|
72
72
|
'context:create': allow,
|
|
73
73
|
'context:delete': allow,
|
|
74
|
+
'context:entries:get': allow,
|
|
75
|
+
'context:entries:list': allow,
|
|
76
|
+
// See context:build — --watch has no deadline and would hang the MCP tool call.
|
|
77
|
+
'context:entries:rebuild': conditionalDenyFlags('watch'),
|
|
74
78
|
'context:get': allow,
|
|
75
79
|
// --file reads the import payload from the local filesystem; text, URL and
|
|
76
80
|
// dataset imports are remote-only.
|
|
@@ -80,8 +84,17 @@ function apiValidator({ args, flags }) {
|
|
|
80
84
|
'context:imports:get': allow,
|
|
81
85
|
'context:imports:list': allow,
|
|
82
86
|
// See context:build — --watch has no deadline and would hang the MCP tool call.
|
|
87
|
+
'context:issues:apply': conditionalDenyFlags('watch'),
|
|
88
|
+
'context:issues:dismiss': allow,
|
|
89
|
+
'context:issues:get': allow,
|
|
90
|
+
'context:issues:list': allow,
|
|
91
|
+
'context:issues:reopen': allow,
|
|
92
|
+
// See context:build — --watch has no deadline and would hang the MCP tool call.
|
|
93
|
+
'context:issues:resolve': conditionalDenyFlags('watch'),
|
|
94
|
+
// See context:build — --watch has no deadline and would hang the MCP tool call.
|
|
83
95
|
'context:jobs:get': conditionalDenyFlags('watch'),
|
|
84
96
|
'context:list': allow,
|
|
97
|
+
'context:query': allow,
|
|
85
98
|
'context:refresh': allow,
|
|
86
99
|
'context:update': allow,
|
|
87
100
|
'cors:add': allow,
|
|
@@ -170,6 +183,10 @@ function apiValidator({ args, flags }) {
|
|
|
170
183
|
],
|
|
171
184
|
validate: ({ args })=>typeof args.source === 'string' && isRemoteAssetSource(args.source)
|
|
172
185
|
}),
|
|
186
|
+
// media:pull-aspects and media:prune-aspect-data are hidden while the
|
|
187
|
+
// config app is in beta, so they take the deny-by-default path like any
|
|
188
|
+
// unlisted command. The exhaustiveness test forces explicit entries when
|
|
189
|
+
// they are unhidden.
|
|
173
190
|
// Creates migration source files in the local project.
|
|
174
191
|
'migrations:create': deny,
|
|
175
192
|
// Reads and loads migration definitions from the local project.
|
|
@@ -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 '@sanity/cli-core/commandPolicy'\n\nimport {isRemoteAssetSource} from '../../../util/isRemoteAssetSource.js'\n\nfunction apiValidator({\n args,\n flags,\n}: {\n args: Readonly<Record<string, unknown>>\n flags: Readonly<Record<string, unknown>>\n}): boolean {\n const fields: unknown[] = Array.isArray(flags.field) ? flags.field : []\n\n const fieldReadsFromHost = fields.some((field) => {\n if (typeof field !== 'string') return false\n const separatorIndex = field.indexOf('=')\n return separatorIndex > 0 && field[separatorIndex + 1] === '@'\n })\n if (fieldReadsFromHost) return false\n\n const headers: unknown[] = Array.isArray(flags.header) ? flags.header : []\n\n const headerOverridesAuthentication = headers.some((header) => {\n if (typeof header !== 'string') return false\n const separatorIndex = header.indexOf(':')\n if (separatorIndex <= 0) return false\n\n const name = header.slice(0, separatorIndex).trim().toLowerCase()\n return name === 'authorization' || name === 'cookie'\n })\n\n if (headerOverridesAuthentication) return false\n\n const endpoint = typeof args.endpoint === 'string' ? URL.parse(args.endpoint) : null\n const urlEmbedsCredentials =\n endpoint !== null && (endpoint.username !== '' || endpoint.password !== '')\n\n return !urlEmbedsCredentials\n}\n\n/**\n * MCP programmatic mode disables local project/config discovery (see the CLI\n * execution context). Missing project or dataset values may therefore produce\n * a usage error, but cannot cause local filesystem access. Destructive remote\n * operations are allowed and do not by themselves make a command unsafe.\n *\n * Every command this package contributes must have exactly one policy here:\n * - allow: every valid invocation is safe\n * - conditional: safety depends on parsed arguments or flags\n * - deny: no invocation is safe\n *\n * Commands contributed by plugins are not listed here. Each plugin declares\n * policies for its own commands (see `PluginInvocationPolicies` in\n * `@sanity/cli-core/commandPolicy`).\n */\nexport const mcpPolicy: CommandPolicySet = {\n // Special exception, this can be very dangerous but is also super useful\n // to expose. Refuse authentication overrides and host input channels:\n // `--token`, Authorization, and Cookie headers replace the MCP user's authentication,\n // URL-embedded credentials replace it with Basic authentication,\n // `--input` reads the request body from the host's filesystem or stdin, and\n // `-F key=@<file>` / `-F key=@-` field values do the same.\n api: conditionalPolicy({\n deniedFlags: ['input', 'token'],\n validate: apiValidator,\n }),\n\n // --file reads from the machine running the command and must only run locally.\n 'assets:upload': conditionalPolicy({\n deniedFlags: ['content-type', 'file'],\n validate: ({flags}) => typeof flags['from-url'] === 'string' && flags['from-url'].length > 0,\n }),\n\n 'backups:disable': allow,\n // Writes a downloaded backup to the local filesystem.\n 'backups:download': deny,\n 'backups:enable': allow,\n 'backups:list': allow,\n\n // Requires a local Studio project and writes build output to disk.\n build: deny,\n\n // Reads and rewrites local source code.\n codemod: deny,\n\n // --watch polls a job with no deadline; a stuck job would hang the MCP tool\n // call indefinitely. Non-watch invocations are single remote requests.\n 'context:build': conditionalDenyFlags('watch'),\n 'context:create': allow,\n 'context:delete': allow,\n 'context:get': allow,\n // --file reads the import payload from the local filesystem; text, URL and\n // dataset imports are remote-only.\n 'context:imports:create': conditionalDenyFlags('file'),\n 'context:imports:delete': allow,\n 'context:imports:download': allow,\n 'context:imports:get': allow,\n 'context:imports:list': allow,\n // See context:build — --watch has no deadline and would hang the MCP tool call.\n 'context:jobs:get': conditionalDenyFlags('watch'),\n 'context:list': allow,\n 'context:refresh': allow,\n 'context:update': allow,\n\n 'cors:add': allow,\n 'cors:delete': allow,\n 'cors:list': allow,\n\n 'datasets:alias:create': allow,\n 'datasets:alias:delete': allow,\n 'datasets:alias:link': allow,\n 'datasets:alias:unlink': allow,\n 'datasets:copy': allow,\n 'datasets:create': allow,\n 'datasets:delete': allow,\n 'datasets:embeddings:disable': allow,\n 'datasets:embeddings:enable': allow,\n 'datasets:embeddings:status': allow,\n // Writes dataset contents and assets to the local filesystem.\n 'datasets:export': deny,\n // Reads import data from the local filesystem and may replace documents.\n 'datasets:import': deny,\n 'datasets:list': allow,\n 'datasets:visibility:get': allow,\n 'datasets:visibility:set': allow,\n\n // Inspects local project files and can print authentication secrets.\n debug: deny,\n\n // Reads, builds, and deploys a local Studio project.\n deploy: deny,\n\n // Loads a local Studio project and starts a development server.\n dev: deny,\n\n // Opens a browser on the machine running the MCP server.\n 'docs:browse': deny,\n // --web opens a browser on the machine running the MCP server.\n 'docs:read': conditionalDenyFlags('web'),\n 'docs:search': allow,\n\n // Reads and executes local project configuration for diagnostics.\n doctor: deny,\n\n // Reads document input from disk or launches a local editor.\n 'documents:create': deny,\n 'documents:delete': allow,\n 'documents:get': allow,\n 'documents:query': allow,\n // Loads a local Studio schema to validate documents.\n 'documents:validate': deny,\n\n // Executes arbitrary code in the local Studio context.\n exec: deny,\n\n // Loads a local schema and deploys a GraphQL API.\n 'graphql:deploy': deny,\n 'graphql:list': allow,\n // --api loads GraphQL definitions from the local project; explicit project/dataset flags are safe.\n 'graphql:undeploy': conditionalDenyFlags('api'),\n\n 'hooks:attempt': allow,\n 'hooks:create': allow,\n 'hooks:delete': allow,\n 'hooks:list': allow,\n 'hooks:logs': allow,\n\n // Creates or modifies a local project and may install dependencies.\n init: deny,\n\n // Installs packages into the local project.\n install: deny,\n\n // Opens a browser on the machine running the MCP server.\n learn: deny,\n\n // Performs an authentication flow.\n login: deny,\n\n // Performs an authentication operation and clears local credentials.\n logout: deny,\n\n // Reads local project configuration and opens a browser.\n manage: deny,\n\n // Loads local Studio configuration and writes manifest files.\n 'manifest:extract': deny,\n\n // Reads and writes local MCP client configuration.\n 'mcp:configure': deny,\n\n // Writes an aspect definition to the local filesystem.\n 'media:create-aspect': deny,\n 'media:delete-aspect': allow,\n // Reads a local aspect definition before deploying it.\n 'media:deploy-aspect': deny,\n // Writes media assets to the local filesystem.\n 'media:export': deny,\n // A directory or archive source reads media assets from the local\n // filesystem. A URL source does not: Sanity fetches the asset itself, so the\n // only local input is the URL. `--replace-aspects` is meaningless for a URL\n // source (the command rejects the combination), so it is hidden here rather\n // than advertised as usable surface.\n 'media:import': conditionalPolicy({\n deniedFlags: ['replace-aspects'],\n validate: ({args}) => typeof args.source === 'string' && isRemoteAssetSource(args.source),\n }),\n\n // Creates migration source files in the local project.\n 'migrations:create': deny,\n // Reads and loads migration definitions from the local project.\n 'migrations:list': deny,\n // Executes local migration code that may perform arbitrary document mutations.\n 'migrations:run': deny,\n\n // Writes project setup values and scaffolds local applications.\n new: deny,\n\n // --web opens a browser on the machine running the MCP server.\n 'openapi:get': conditionalDenyFlags('web'),\n 'openapi:list': conditionalDenyFlags('web'),\n\n 'organizations:create': allow,\n 'organizations:delete': allow,\n 'organizations:get': allow,\n 'organizations:list': allow,\n 'organizations:update': allow,\n\n // Serves a local production build.\n preview: deny,\n\n 'projects:create': allow,\n 'projects:list': allow,\n // Reads locally stored claim URLs and robot tokens.\n 'projects:unclaimed': deny,\n\n // Loads the local Studio configuration to resolve the datasets containing each schema.\n 'schemas:delete': deny,\n // Loads local schema files before deploying them.\n 'schemas:deploy': deny,\n // Loads local Studio configuration and writes an extracted schema to disk.\n 'schemas:extract': deny,\n // Requires a local project and loads its schema configuration.\n 'schemas:list': deny,\n // Loads and executes a local Studio schema.\n 'schemas:validate': deny,\n\n // Installs skills into local editor configuration directories.\n 'skills:install': deny,\n\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:disable': deny,\n // Changes account telemetry preferences and mutates local cached configuration.\n 'telemetry:enable': deny,\n 'telemetry:status': allow,\n\n // Creates authentication credentials.\n 'tokens:create': deny,\n // Deletes authentication credentials.\n 'tokens:delete': deny,\n // Exposes authentication credential metadata.\n 'tokens:list': deny,\n // Replaces authentication credentials and exposes the new secret.\n 'tokens:rotate': deny,\n\n // Reads the local schema and source files and writes generated types to the project.\n 'typegen:generate': deny,\n\n // Loads local CLI and workbench configuration to identify the deployed Studio or application.\n undeploy: deny,\n\n // Grants a user access to a project.\n 'users:invite': deny,\n 'users:list': allow,\n\n // Reads the local project and installed package tree.\n versions: deny,\n\n // Reads the authenticated user from the API using the execution context token.\n whoami: allow,\n}\n"],"names":["allow","conditionalDenyFlags","conditionalPolicy","deny","isRemoteAssetSource","apiValidator","args","flags","fields","Array","isArray","field","fieldReadsFromHost","some","separatorIndex","indexOf","headers","header","headerOverridesAuthentication","name","slice","trim","toLowerCase","endpoint","URL","parse","urlEmbedsCredentials","username","password","mcpPolicy","api","deniedFlags","validate","length","build","codemod","debug","deploy","dev","doctor","exec","init","install","learn","login","logout","manage","source","new","preview","undeploy","versions","whoami"],"mappings":"AAAA,SACEA,KAAK,EAELC,oBAAoB,EACpBC,iBAAiB,EACjBC,IAAI,QACC,iCAAgC;AAEvC,SAAQC,mBAAmB,QAAO,uCAAsC;AAExE,SAASC,aAAa,EACpBC,IAAI,EACJC,KAAK,EAIN;IACC,MAAMC,SAAoBC,MAAMC,OAAO,CAACH,MAAMI,KAAK,IAAIJ,MAAMI,KAAK,GAAG,EAAE;IAEvE,MAAMC,qBAAqBJ,OAAOK,IAAI,CAAC,CAACF;QACtC,IAAI,OAAOA,UAAU,UAAU,OAAO;QACtC,MAAMG,iBAAiBH,MAAMI,OAAO,CAAC;QACrC,OAAOD,iBAAiB,KAAKH,KAAK,CAACG,iBAAiB,EAAE,KAAK;IAC7D;IACA,IAAIF,oBAAoB,OAAO;IAE/B,MAAMI,UAAqBP,MAAMC,OAAO,CAACH,MAAMU,MAAM,IAAIV,MAAMU,MAAM,GAAG,EAAE;IAE1E,MAAMC,gCAAgCF,QAAQH,IAAI,CAAC,CAACI;QAClD,IAAI,OAAOA,WAAW,UAAU,OAAO;QACvC,MAAMH,iBAAiBG,OAAOF,OAAO,CAAC;QACtC,IAAID,kBAAkB,GAAG,OAAO;QAEhC,MAAMK,OAAOF,OAAOG,KAAK,CAAC,GAAGN,gBAAgBO,IAAI,GAAGC,WAAW;QAC/D,OAAOH,SAAS,mBAAmBA,SAAS;IAC9C;IAEA,IAAID,+BAA+B,OAAO;IAE1C,MAAMK,WAAW,OAAOjB,KAAKiB,QAAQ,KAAK,WAAWC,IAAIC,KAAK,CAACnB,KAAKiB,QAAQ,IAAI;IAChF,MAAMG,uBACJH,aAAa,QAASA,CAAAA,SAASI,QAAQ,KAAK,MAAMJ,SAASK,QAAQ,KAAK,EAAC;IAE3E,OAAO,CAACF;AACV;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMG,YAA8B;IACzC,yEAAyE;IACzE,sEAAsE;IACtE,sFAAsF;IACtF,iEAAiE;IACjE,4EAA4E;IAC5E,2DAA2D;IAC3DC,KAAK5B,kBAAkB;QACrB6B,aAAa;YAAC;YAAS;SAAQ;QAC/BC,UAAU3B;IACZ;IAEA,+EAA+E;IAC/E,iBAAiBH,kBAAkB;QACjC6B,aAAa;YAAC;YAAgB;SAAO;QACrCC,UAAU,CAAC,EAACzB,KAAK,EAAC,GAAK,OAAOA,KAAK,CAAC,WAAW,KAAK,YAAYA,KAAK,CAAC,WAAW,CAAC0B,MAAM,GAAG;IAC7F;IAEA,mBAAmBjC;IACnB,sDAAsD;IACtD,oBAAoBG;IACpB,kBAAkBH;IAClB,gBAAgBA;IAEhB,mEAAmE;IACnEkC,OAAO/B;IAEP,wCAAwC;IACxCgC,SAAShC;IAET,4EAA4E;IAC5E,uEAAuE;IACvE,iBAAiBF,qBAAqB;IACtC,kBAAkBD;IAClB,kBAAkBA;IAClB,eAAeA;IACf,2EAA2E;IAC3E,mCAAmC;IACnC,0BAA0BC,qBAAqB;IAC/C,0BAA0BD;IAC1B,4BAA4BA;IAC5B,uBAAuBA;IACvB,wBAAwBA;IACxB,gFAAgF;IAChF,oBAAoBC,qBAAqB;IACzC,gBAAgBD;IAChB,mBAAmBA;IACnB,kBAAkBA;IAElB,YAAYA;IACZ,eAAeA;IACf,aAAaA;IAEb,yBAAyBA;IACzB,yBAAyBA;IACzB,uBAAuBA;IACvB,yBAAyBA;IACzB,iBAAiBA;IACjB,mBAAmBA;IACnB,mBAAmBA;IACnB,+BAA+BA;IAC/B,8BAA8BA;IAC9B,8BAA8BA;IAC9B,8DAA8D;IAC9D,mBAAmBG;IACnB,yEAAyE;IACzE,mBAAmBA;IACnB,iBAAiBH;IACjB,2BAA2BA;IAC3B,2BAA2BA;IAE3B,qEAAqE;IACrEoC,OAAOjC;IAEP,qDAAqD;IACrDkC,QAAQlC;IAER,gEAAgE;IAChEmC,KAAKnC;IAEL,yDAAyD;IACzD,eAAeA;IACf,+DAA+D;IAC/D,aAAaF,qBAAqB;IAClC,eAAeD;IAEf,kEAAkE;IAClEuC,QAAQpC;IAER,6DAA6D;IAC7D,oBAAoBA;IACpB,oBAAoBH;IACpB,iBAAiBA;IACjB,mBAAmBA;IACnB,qDAAqD;IACrD,sBAAsBG;IAEtB,uDAAuD;IACvDqC,MAAMrC;IAEN,kDAAkD;IAClD,kBAAkBA;IAClB,gBAAgBH;IAChB,mGAAmG;IACnG,oBAAoBC,qBAAqB;IAEzC,iBAAiBD;IACjB,gBAAgBA;IAChB,gBAAgBA;IAChB,cAAcA;IACd,cAAcA;IAEd,oEAAoE;IACpEyC,MAAMtC;IAEN,4CAA4C;IAC5CuC,SAASvC;IAET,yDAAyD;IACzDwC,OAAOxC;IAEP,mCAAmC;IACnCyC,OAAOzC;IAEP,qEAAqE;IACrE0C,QAAQ1C;IAER,yDAAyD;IACzD2C,QAAQ3C;IAER,8DAA8D;IAC9D,oBAAoBA;IAEpB,mDAAmD;IACnD,iBAAiBA;IAEjB,uDAAuD;IACvD,uBAAuBA;IACvB,uBAAuBH;IACvB,uDAAuD;IACvD,uBAAuBG;IACvB,+CAA+C;IAC/C,gBAAgBA;IAChB,kEAAkE;IAClE,6EAA6E;IAC7E,4EAA4E;IAC5E,4EAA4E;IAC5E,qCAAqC;IACrC,gBAAgBD,kBAAkB;QAChC6B,aAAa;YAAC;SAAkB;QAChCC,UAAU,CAAC,EAAC1B,IAAI,EAAC,GAAK,OAAOA,KAAKyC,MAAM,KAAK,YAAY3C,oBAAoBE,KAAKyC,MAAM;IAC1F;IAEA,uDAAuD;IACvD,qBAAqB5C;IACrB,gEAAgE;IAChE,mBAAmBA;IACnB,+EAA+E;IAC/E,kBAAkBA;IAElB,gEAAgE;IAChE6C,KAAK7C;IAEL,+DAA+D;IAC/D,eAAeF,qBAAqB;IACpC,gBAAgBA,qBAAqB;IAErC,wBAAwBD;IACxB,wBAAwBA;IACxB,qBAAqBA;IACrB,sBAAsBA;IACtB,wBAAwBA;IAExB,mCAAmC;IACnCiD,SAAS9C;IAET,mBAAmBH;IACnB,iBAAiBA;IACjB,oDAAoD;IACpD,sBAAsBG;IAEtB,uFAAuF;IACvF,kBAAkBA;IAClB,kDAAkD;IAClD,kBAAkBA;IAClB,2EAA2E;IAC3E,mBAAmBA;IACnB,+DAA+D;IAC/D,gBAAgBA;IAChB,4CAA4C;IAC5C,oBAAoBA;IAEpB,+DAA+D;IAC/D,kBAAkBA;IAElB,gFAAgF;IAChF,qBAAqBA;IACrB,gFAAgF;IAChF,oBAAoBA;IACpB,oBAAoBH;IAEpB,sCAAsC;IACtC,iBAAiBG;IACjB,sCAAsC;IACtC,iBAAiBA;IACjB,8CAA8C;IAC9C,eAAeA;IACf,kEAAkE;IAClE,iBAAiBA;IAEjB,qFAAqF;IACrF,oBAAoBA;IAEpB,8FAA8F;IAC9F+C,UAAU/C;IAEV,qCAAqC;IACrC,gBAAgBA;IAChB,cAAcH;IAEd,sDAAsD;IACtDmD,UAAUhD;IAEV,+EAA+E;IAC/EiD,QAAQpD;AACV,EAAC"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/exports/invokeSanityCli/commandPolicies/mcpPolicy.ts"],"sourcesContent":["import {\n allow,\n type CommandPolicySet,\n conditionalDenyFlags,\n conditionalPolicy,\n deny,\n} from '@sanity/cli-core/commandPolicy'\n\nimport {isRemoteAssetSource} from '../../../util/isRemoteAssetSource.js'\n\nfunction apiValidator({\n args,\n flags,\n}: {\n args: Readonly<Record<string, unknown>>\n flags: Readonly<Record<string, unknown>>\n}): boolean {\n const fields: unknown[] = Array.isArray(flags.field) ? flags.field : []\n\n const fieldReadsFromHost = fields.some((field) => {\n if (typeof field !== 'string') return false\n const separatorIndex = field.indexOf('=')\n return separatorIndex > 0 && field[separatorIndex + 1] === '@'\n })\n if (fieldReadsFromHost) return false\n\n const headers: unknown[] = Array.isArray(flags.header) ? flags.header : []\n\n const headerOverridesAuthentication = headers.some((header) => {\n if (typeof header !== 'string') return false\n const separatorIndex = header.indexOf(':')\n if (separatorIndex <= 0) return false\n\n const name = header.slice(0, separatorIndex).trim().toLowerCase()\n return name === 'authorization' || name === 'cookie'\n })\n\n if (headerOverridesAuthentication) return false\n\n const endpoint = typeof args.endpoint === 'string' ? URL.parse(args.endpoint) : null\n const urlEmbedsCredentials =\n endpoint !== null && (endpoint.username !== '' || endpoint.password !== '')\n\n return !urlEmbedsCredentials\n}\n\n/**\n * MCP programmatic mode disables local project/config discovery (see the CLI\n * execution context). Missing project or dataset values may therefore produce\n * a usage error, but cannot cause local filesystem access. Destructive remote\n * operations are allowed and do not by themselves make a command unsafe.\n *\n * Every command this package contributes must have exactly one policy here:\n * - allow: every valid invocation is safe\n * - conditional: safety depends on parsed arguments or flags\n * - deny: no invocation is safe\n *\n * Commands contributed by plugins are not listed here. Each plugin declares\n * policies for its own commands (see `PluginInvocationPolicies` in\n * `@sanity/cli-core/commandPolicy`).\n */\nexport const mcpPolicy: CommandPolicySet = {\n // Special exception, this can be very dangerous but is also super useful\n // to expose. Refuse authentication overrides and host input channels:\n // `--token`, Authorization, and Cookie headers replace the MCP user's authentication,\n // URL-embedded credentials replace it with Basic authentication,\n // `--input` reads the request body from the host's filesystem or stdin, and\n // `-F key=@<file>` / `-F key=@-` field values do the same.\n api: conditionalPolicy({\n deniedFlags: ['input', 'token'],\n validate: apiValidator,\n }),\n\n // --file reads from the machine running the command and must only run locally.\n 'assets:upload': conditionalPolicy({\n deniedFlags: ['content-type', 'file'],\n validate: ({flags}) => typeof flags['from-url'] === 'string' && flags['from-url'].length > 0,\n }),\n\n 'backups:disable': allow,\n // Writes a downloaded backup to the local filesystem.\n 'backups:download': deny,\n 'backups:enable': allow,\n 'backups:list': allow,\n\n // Requires a local Studio project and writes build output to disk.\n build: deny,\n\n // Reads and rewrites local source code.\n codemod: deny,\n\n // --watch polls a job with no deadline; a stuck job would hang the MCP tool\n // call indefinitely. Non-watch invocations are single remote requests.\n 'context:build': conditionalDenyFlags('watch'),\n 'context:create': allow,\n 'context:delete': allow,\n 'context:entries:get': allow,\n 'context:entries:list': allow,\n // See context:build — --watch has no deadline and would hang the MCP tool call.\n 'context:entries:rebuild': conditionalDenyFlags('watch'),\n 'context:get': allow,\n // --file reads the import payload from the local filesystem; text, URL and\n // dataset imports are remote-only.\n 'context:imports:create': conditionalDenyFlags('file'),\n 'context:imports:delete': allow,\n 'context:imports:download': allow,\n 'context:imports:get': allow,\n 'context:imports:list': allow,\n // See context:build — --watch has no deadline and would hang the MCP tool call.\n 'context:issues:apply': conditionalDenyFlags('watch'),\n 'context:issues:dismiss': allow,\n 'context:issues:get': allow,\n 'context:issues:list': allow,\n 'context:issues:reopen': allow,\n // See context:build — --watch has no deadline and would hang the MCP tool call.\n 'context:issues:resolve': conditionalDenyFlags('watch'),\n // See context:build — --watch has no deadline and would hang the MCP tool call.\n 'context:jobs:get': conditionalDenyFlags('watch'),\n 'context:list': allow,\n 'context:query': allow,\n 'context:refresh': allow,\n 'context:update': allow,\n\n 'cors:add': allow,\n 'cors:delete': allow,\n 'cors:list': allow,\n\n 'datasets:alias:create': allow,\n 'datasets:alias:delete': allow,\n 'datasets:alias:link': allow,\n 'datasets:alias:unlink': allow,\n 'datasets:copy': allow,\n 'datasets:create': allow,\n 'datasets:delete': allow,\n 'datasets:embeddings:disable': allow,\n 'datasets:embeddings:enable': allow,\n 'datasets:embeddings:status': allow,\n // Writes dataset contents and assets to the local filesystem.\n 'datasets:export': deny,\n // Reads import data from the local filesystem and may replace documents.\n 'datasets:import': deny,\n 'datasets:list': allow,\n 'datasets:visibility:get': allow,\n 'datasets:visibility:set': allow,\n\n // Inspects local project files and can print authentication secrets.\n debug: deny,\n\n // Reads, builds, and deploys a local Studio project.\n deploy: deny,\n\n // Loads a local Studio project and starts a development server.\n dev: deny,\n\n // Opens a browser on the machine running the MCP server.\n 'docs:browse': deny,\n // --web opens a browser on the machine running the MCP server.\n 'docs:read': conditionalDenyFlags('web'),\n 'docs:search': allow,\n\n // Reads and executes local project configuration for diagnostics.\n doctor: deny,\n\n // Reads document input from disk or launches a local editor.\n 'documents:create': deny,\n 'documents:delete': allow,\n 'documents:get': allow,\n 'documents:query': allow,\n // Loads a local Studio schema to validate documents.\n 'documents:validate': deny,\n\n // Executes arbitrary code in the local Studio context.\n exec: deny,\n\n // Loads a local schema and deploys a GraphQL API.\n 'graphql:deploy': deny,\n 'graphql:list': allow,\n // --api loads GraphQL definitions from the local project; explicit project/dataset flags are safe.\n 'graphql:undeploy': conditionalDenyFlags('api'),\n\n 'hooks:attempt': allow,\n 'hooks:create': allow,\n 'hooks:delete': allow,\n 'hooks:list': allow,\n 'hooks:logs': allow,\n\n // Creates or modifies a local project and may install dependencies.\n init: deny,\n\n // Installs packages into the local project.\n install: deny,\n\n // Opens a browser on the machine running the MCP server.\n learn: deny,\n\n // Performs an authentication flow.\n login: deny,\n\n // Performs an authentication operation and clears local credentials.\n logout: deny,\n\n // Reads local project configuration and opens a browser.\n manage: deny,\n\n // Loads local Studio configuration and writes manifest files.\n 'manifest:extract': deny,\n\n // Reads and writes local MCP client configuration.\n 'mcp:configure': deny,\n\n // Writes an aspect definition to the local filesystem.\n 'media:create-aspect': deny,\n 'media:delete-aspect': allow,\n // Reads a local aspect definition before deploying it.\n 'media:deploy-aspect': deny,\n // Writes media assets to the local filesystem.\n 'media:export': deny,\n // A directory or archive source reads media assets from the local\n // filesystem. A URL source does not: Sanity fetches the asset itself, so the\n // only local input is the URL. `--replace-aspects` is meaningless for a URL\n // source (the command rejects the combination), so it is hidden here rather\n // than advertised as usable surface.\n 'media:import': conditionalPolicy({\n deniedFlags: ['replace-aspects'],\n validate: ({args}) => typeof args.source === 'string' && isRemoteAssetSource(args.source),\n }),\n // media:pull-aspects and media:prune-aspect-data are hidden while the\n // config app is in beta, so they take the deny-by-default path like any\n // unlisted command. The exhaustiveness test forces explicit entries when\n // they are unhidden.\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 // Reads the authenticated user from the API using the execution context token.\n whoami: allow,\n}\n"],"names":["allow","conditionalDenyFlags","conditionalPolicy","deny","isRemoteAssetSource","apiValidator","args","flags","fields","Array","isArray","field","fieldReadsFromHost","some","separatorIndex","indexOf","headers","header","headerOverridesAuthentication","name","slice","trim","toLowerCase","endpoint","URL","parse","urlEmbedsCredentials","username","password","mcpPolicy","api","deniedFlags","validate","length","build","codemod","debug","deploy","dev","doctor","exec","init","install","learn","login","logout","manage","source","new","preview","undeploy","versions","whoami"],"mappings":"AAAA,SACEA,KAAK,EAELC,oBAAoB,EACpBC,iBAAiB,EACjBC,IAAI,QACC,iCAAgC;AAEvC,SAAQC,mBAAmB,QAAO,uCAAsC;AAExE,SAASC,aAAa,EACpBC,IAAI,EACJC,KAAK,EAIN;IACC,MAAMC,SAAoBC,MAAMC,OAAO,CAACH,MAAMI,KAAK,IAAIJ,MAAMI,KAAK,GAAG,EAAE;IAEvE,MAAMC,qBAAqBJ,OAAOK,IAAI,CAAC,CAACF;QACtC,IAAI,OAAOA,UAAU,UAAU,OAAO;QACtC,MAAMG,iBAAiBH,MAAMI,OAAO,CAAC;QACrC,OAAOD,iBAAiB,KAAKH,KAAK,CAACG,iBAAiB,EAAE,KAAK;IAC7D;IACA,IAAIF,oBAAoB,OAAO;IAE/B,MAAMI,UAAqBP,MAAMC,OAAO,CAACH,MAAMU,MAAM,IAAIV,MAAMU,MAAM,GAAG,EAAE;IAE1E,MAAMC,gCAAgCF,QAAQH,IAAI,CAAC,CAACI;QAClD,IAAI,OAAOA,WAAW,UAAU,OAAO;QACvC,MAAMH,iBAAiBG,OAAOF,OAAO,CAAC;QACtC,IAAID,kBAAkB,GAAG,OAAO;QAEhC,MAAMK,OAAOF,OAAOG,KAAK,CAAC,GAAGN,gBAAgBO,IAAI,GAAGC,WAAW;QAC/D,OAAOH,SAAS,mBAAmBA,SAAS;IAC9C;IAEA,IAAID,+BAA+B,OAAO;IAE1C,MAAMK,WAAW,OAAOjB,KAAKiB,QAAQ,KAAK,WAAWC,IAAIC,KAAK,CAACnB,KAAKiB,QAAQ,IAAI;IAChF,MAAMG,uBACJH,aAAa,QAASA,CAAAA,SAASI,QAAQ,KAAK,MAAMJ,SAASK,QAAQ,KAAK,EAAC;IAE3E,OAAO,CAACF;AACV;AAEA;;;;;;;;;;;;;;CAcC,GACD,OAAO,MAAMG,YAA8B;IACzC,yEAAyE;IACzE,sEAAsE;IACtE,sFAAsF;IACtF,iEAAiE;IACjE,4EAA4E;IAC5E,2DAA2D;IAC3DC,KAAK5B,kBAAkB;QACrB6B,aAAa;YAAC;YAAS;SAAQ;QAC/BC,UAAU3B;IACZ;IAEA,+EAA+E;IAC/E,iBAAiBH,kBAAkB;QACjC6B,aAAa;YAAC;YAAgB;SAAO;QACrCC,UAAU,CAAC,EAACzB,KAAK,EAAC,GAAK,OAAOA,KAAK,CAAC,WAAW,KAAK,YAAYA,KAAK,CAAC,WAAW,CAAC0B,MAAM,GAAG;IAC7F;IAEA,mBAAmBjC;IACnB,sDAAsD;IACtD,oBAAoBG;IACpB,kBAAkBH;IAClB,gBAAgBA;IAEhB,mEAAmE;IACnEkC,OAAO/B;IAEP,wCAAwC;IACxCgC,SAAShC;IAET,4EAA4E;IAC5E,uEAAuE;IACvE,iBAAiBF,qBAAqB;IACtC,kBAAkBD;IAClB,kBAAkBA;IAClB,uBAAuBA;IACvB,wBAAwBA;IACxB,gFAAgF;IAChF,2BAA2BC,qBAAqB;IAChD,eAAeD;IACf,2EAA2E;IAC3E,mCAAmC;IACnC,0BAA0BC,qBAAqB;IAC/C,0BAA0BD;IAC1B,4BAA4BA;IAC5B,uBAAuBA;IACvB,wBAAwBA;IACxB,gFAAgF;IAChF,wBAAwBC,qBAAqB;IAC7C,0BAA0BD;IAC1B,sBAAsBA;IACtB,uBAAuBA;IACvB,yBAAyBA;IACzB,gFAAgF;IAChF,0BAA0BC,qBAAqB;IAC/C,gFAAgF;IAChF,oBAAoBA,qBAAqB;IACzC,gBAAgBD;IAChB,iBAAiBA;IACjB,mBAAmBA;IACnB,kBAAkBA;IAElB,YAAYA;IACZ,eAAeA;IACf,aAAaA;IAEb,yBAAyBA;IACzB,yBAAyBA;IACzB,uBAAuBA;IACvB,yBAAyBA;IACzB,iBAAiBA;IACjB,mBAAmBA;IACnB,mBAAmBA;IACnB,+BAA+BA;IAC/B,8BAA8BA;IAC9B,8BAA8BA;IAC9B,8DAA8D;IAC9D,mBAAmBG;IACnB,yEAAyE;IACzE,mBAAmBA;IACnB,iBAAiBH;IACjB,2BAA2BA;IAC3B,2BAA2BA;IAE3B,qEAAqE;IACrEoC,OAAOjC;IAEP,qDAAqD;IACrDkC,QAAQlC;IAER,gEAAgE;IAChEmC,KAAKnC;IAEL,yDAAyD;IACzD,eAAeA;IACf,+DAA+D;IAC/D,aAAaF,qBAAqB;IAClC,eAAeD;IAEf,kEAAkE;IAClEuC,QAAQpC;IAER,6DAA6D;IAC7D,oBAAoBA;IACpB,oBAAoBH;IACpB,iBAAiBA;IACjB,mBAAmBA;IACnB,qDAAqD;IACrD,sBAAsBG;IAEtB,uDAAuD;IACvDqC,MAAMrC;IAEN,kDAAkD;IAClD,kBAAkBA;IAClB,gBAAgBH;IAChB,mGAAmG;IACnG,oBAAoBC,qBAAqB;IAEzC,iBAAiBD;IACjB,gBAAgBA;IAChB,gBAAgBA;IAChB,cAAcA;IACd,cAAcA;IAEd,oEAAoE;IACpEyC,MAAMtC;IAEN,4CAA4C;IAC5CuC,SAASvC;IAET,yDAAyD;IACzDwC,OAAOxC;IAEP,mCAAmC;IACnCyC,OAAOzC;IAEP,qEAAqE;IACrE0C,QAAQ1C;IAER,yDAAyD;IACzD2C,QAAQ3C;IAER,8DAA8D;IAC9D,oBAAoBA;IAEpB,mDAAmD;IACnD,iBAAiBA;IAEjB,uDAAuD;IACvD,uBAAuBA;IACvB,uBAAuBH;IACvB,uDAAuD;IACvD,uBAAuBG;IACvB,+CAA+C;IAC/C,gBAAgBA;IAChB,kEAAkE;IAClE,6EAA6E;IAC7E,4EAA4E;IAC5E,4EAA4E;IAC5E,qCAAqC;IACrC,gBAAgBD,kBAAkB;QAChC6B,aAAa;YAAC;SAAkB;QAChCC,UAAU,CAAC,EAAC1B,IAAI,EAAC,GAAK,OAAOA,KAAKyC,MAAM,KAAK,YAAY3C,oBAAoBE,KAAKyC,MAAM;IAC1F;IACA,sEAAsE;IACtE,wEAAwE;IACxE,yEAAyE;IACzE,qBAAqB;IAErB,uDAAuD;IACvD,qBAAqB5C;IACrB,gEAAgE;IAChE,mBAAmBA;IACnB,+EAA+E;IAC/E,kBAAkBA;IAElB,gEAAgE;IAChE6C,KAAK7C;IAEL,+DAA+D;IAC/D,eAAeF,qBAAqB;IACpC,gBAAgBA,qBAAqB;IAErC,wBAAwBD;IACxB,wBAAwBA;IACxB,qBAAqBA;IACrB,sBAAsBA;IACtB,wBAAwBA;IAExB,mCAAmC;IACnCiD,SAAS9C;IAET,mBAAmBH;IACnB,iBAAiBA;IACjB,oDAAoD;IACpD,sBAAsBG;IAEtB,uFAAuF;IACvF,kBAAkBA;IAClB,kDAAkD;IAClD,kBAAkBA;IAClB,2EAA2E;IAC3E,mBAAmBA;IACnB,+DAA+D;IAC/D,gBAAgBA;IAChB,4CAA4C;IAC5C,oBAAoBA;IAEpB,+DAA+D;IAC/D,kBAAkBA;IAElB,gFAAgF;IAChF,qBAAqBA;IACrB,gFAAgF;IAChF,oBAAoBA;IACpB,oBAAoBH;IAEpB,sCAAsC;IACtC,iBAAiBG;IACjB,sCAAsC;IACtC,iBAAiBA;IACjB,8CAA8C;IAC9C,eAAeA;IACf,kEAAkE;IAClE,iBAAiBA;IAEjB,qFAAqF;IACrF,oBAAoBA;IAEpB,8FAA8F;IAC9F+C,UAAU/C;IAEV,qCAAqC;IACrC,gBAAgBA;IAChB,cAAcH;IAEd,sDAAsD;IACtDmD,UAAUhD;IAEV,+EAA+E;IAC/EiD,QAAQpD;AACV,EAAC"}
|
package/dist/services/context.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { getGlobalCliClient } from '@sanity/cli-core';
|
|
2
2
|
const CONTEXT_API_VERSION = 'v2026-08-25';
|
|
3
3
|
/**
|
|
4
|
-
* Knowledge-base-scoped methods on `client.context`
|
|
5
|
-
*
|
|
4
|
+
* Knowledge-base-scoped methods on `client.context` operate on the client's
|
|
5
|
+
* configured `resource`; collection-level methods
|
|
6
6
|
* (`client.context.knowledgeBases.*`) are addressed per call and need none.
|
|
7
|
-
|
|
7
|
+
* GROQ-backed reads also need `context.organizationId`.
|
|
8
|
+
*/ async function getContextClient(knowledgeBaseId, organizationId) {
|
|
8
9
|
return getGlobalCliClient({
|
|
9
10
|
apiVersion: CONTEXT_API_VERSION,
|
|
10
11
|
requireUser: true,
|
|
@@ -13,9 +14,22 @@ const CONTEXT_API_VERSION = 'v2026-08-25';
|
|
|
13
14
|
id: knowledgeBaseId,
|
|
14
15
|
type: 'knowledge-base'
|
|
15
16
|
}
|
|
17
|
+
},
|
|
18
|
+
...organizationId === undefined ? {} : {
|
|
19
|
+
context: {
|
|
20
|
+
organizationId
|
|
21
|
+
}
|
|
16
22
|
}
|
|
17
23
|
});
|
|
18
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* GROQ-backed reads (issues, entries) query the organization's
|
|
27
|
+
* document store, where documents are keyed by the knowledge base's public ID.
|
|
28
|
+
* Fetching the knowledge base resolves both, whichever of its IDs was passed.
|
|
29
|
+
*/ async function getContextStoreClient(knowledgeBaseId) {
|
|
30
|
+
const { organizationId, publicId } = await getKnowledgeBase(knowledgeBaseId);
|
|
31
|
+
return getContextClient(publicId, organizationId);
|
|
32
|
+
}
|
|
19
33
|
/**
|
|
20
34
|
* Create a knowledge base in an organization
|
|
21
35
|
*/ export async function createKnowledgeBase(params) {
|
|
@@ -128,5 +142,84 @@ const CONTEXT_API_VERSION = 'v2026-08-25';
|
|
|
128
142
|
jobId
|
|
129
143
|
});
|
|
130
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* List all issues for a knowledge base, optionally narrowed to one status
|
|
147
|
+
*/ export async function listIssues(knowledgeBaseId, status) {
|
|
148
|
+
const client = await getContextStoreClient(knowledgeBaseId);
|
|
149
|
+
return client.context.issues.list({
|
|
150
|
+
status
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get a single issue by ID, or `null` when it does not exist
|
|
155
|
+
*/ export async function getIssue(knowledgeBaseId, issueId) {
|
|
156
|
+
const client = await getContextStoreClient(knowledgeBaseId);
|
|
157
|
+
return client.context.issues.get({
|
|
158
|
+
issueId
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Resolve a conflict issue by choosing one of its sides (by index). Choosing
|
|
163
|
+
* a side other than the entry's current content starts a rewrite job.
|
|
164
|
+
*/ export async function resolveIssue(knowledgeBaseId, issueId, resolution) {
|
|
165
|
+
const client = await getContextClient(knowledgeBaseId);
|
|
166
|
+
return client.context.issues.resolve({
|
|
167
|
+
issueId,
|
|
168
|
+
resolution
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Dismiss an issue
|
|
173
|
+
*/ export async function dismissIssue(knowledgeBaseId, issueId) {
|
|
174
|
+
const client = await getContextClient(knowledgeBaseId);
|
|
175
|
+
return client.context.issues.dismiss({
|
|
176
|
+
issueId
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Reopen a resolved conflict issue
|
|
181
|
+
*/ export async function reopenIssue(knowledgeBaseId, issueId) {
|
|
182
|
+
const client = await getContextClient(knowledgeBaseId);
|
|
183
|
+
return client.context.issues.reopen({
|
|
184
|
+
issueId
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Apply accepted issues to a knowledge base in one batch
|
|
189
|
+
*/ export async function applyIssues(knowledgeBaseId, issueIds) {
|
|
190
|
+
const client = await getContextClient(knowledgeBaseId);
|
|
191
|
+
return client.context.issues.apply({
|
|
192
|
+
issueIds
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* List all entries for a knowledge base, path-ordered, without bodies
|
|
197
|
+
*/ export async function listEntries(knowledgeBaseId) {
|
|
198
|
+
const client = await getContextStoreClient(knowledgeBaseId);
|
|
199
|
+
return client.context.entries.list();
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get a single entry with its body and citations by outline path, or `null`
|
|
203
|
+
* when no entry sits at that path
|
|
204
|
+
*/ export async function getEntry(knowledgeBaseId, path) {
|
|
205
|
+
const client = await getContextStoreClient(knowledgeBaseId);
|
|
206
|
+
return client.context.entries.get({
|
|
207
|
+
path
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Rebuild one entry from its sources by outline path
|
|
212
|
+
*/ export async function rebuildEntry(knowledgeBaseId, path) {
|
|
213
|
+
const client = await getContextClient(knowledgeBaseId);
|
|
214
|
+
return client.context.entries.rebuild({
|
|
215
|
+
path
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Run a GROQ query against an organization's Context documents
|
|
220
|
+
*/ export async function queryContext(organizationId, query, params) {
|
|
221
|
+
const client = await getContextClient(undefined, organizationId);
|
|
222
|
+
return client.context.fetch(query, params);
|
|
223
|
+
}
|
|
131
224
|
|
|
132
225
|
//# sourceMappingURL=context.js.map
|