@sanity/cli 7.12.1 → 7.14.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/README.md +88 -0
- package/dist/actions/api/constants.js +13 -0
- package/dist/actions/api/constants.js.map +1 -0
- package/dist/actions/api/distillApiRoutes.js +106 -0
- package/dist/actions/api/distillApiRoutes.js.map +1 -0
- package/dist/actions/api/errors.js +18 -0
- package/dist/actions/api/errors.js.map +1 -0
- package/dist/actions/api/parseFields.js +183 -0
- package/dist/actions/api/parseFields.js.map +1 -0
- package/dist/actions/api/resolveEndpoint.js +159 -0
- package/dist/actions/api/resolveEndpoint.js.map +1 -0
- package/dist/actions/api/types.js +10 -0
- package/dist/actions/api/types.js.map +1 -0
- package/dist/actions/auth/getProviderName.js +13 -0
- package/dist/actions/auth/getProviderName.js.map +1 -1
- package/dist/actions/build/buildApp.js +3 -1
- package/dist/actions/build/buildApp.js.map +1 -1
- package/dist/actions/build/buildStudio.js +104 -13
- package/dist/actions/build/buildStudio.js.map +1 -1
- package/dist/actions/build/eventListenerFactory.js +70 -0
- package/dist/actions/build/eventListenerFactory.js.map +1 -0
- package/dist/actions/debug/types.js.map +1 -1
- package/dist/actions/deploy/deployApp.js +1 -0
- package/dist/actions/deploy/deployApp.js.map +1 -1
- package/dist/actions/deploy/deployChecks.js +25 -4
- package/dist/actions/deploy/deployChecks.js.map +1 -1
- package/dist/actions/deploy/resolveDeployTarget.js +23 -4
- package/dist/actions/deploy/resolveDeployTarget.js.map +1 -1
- package/dist/actions/dev/servers/startStudioDevServer.js +5 -1
- package/dist/actions/dev/servers/startStudioDevServer.js.map +1 -1
- package/dist/actions/init/initAction.js +3 -3
- package/dist/actions/init/initAction.js.map +1 -1
- package/dist/commands/api.js +294 -0
- package/dist/commands/api.js.map +1 -0
- package/dist/commands/cors/add.js +13 -7
- package/dist/commands/cors/add.js.map +1 -1
- package/dist/commands/datasets/copy.js +6 -3
- package/dist/commands/datasets/copy.js.map +1 -1
- package/dist/commands/debug.js +1 -1
- package/dist/commands/debug.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/index.js +6 -0
- package/dist/exports/invokeSanityCli/commandPolicies/index.js.map +1 -0
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +167 -0
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -0
- package/dist/exports/invokeSanityCli/commandPolicies/policy.js +46 -0
- package/dist/exports/invokeSanityCli/commandPolicies/policy.js.map +1 -0
- package/dist/exports/invokeSanityCli/help.js +146 -0
- package/dist/exports/invokeSanityCli/help.js.map +1 -0
- package/dist/exports/invokeSanityCli/index.d.ts +60 -0
- package/dist/exports/invokeSanityCli/index.js +177 -0
- package/dist/exports/invokeSanityCli/index.js.map +1 -0
- package/dist/generated/apiRoutes.js +401 -0
- package/dist/generated/apiRoutes.js.map +1 -0
- package/dist/server/devServer.js +16 -2
- package/dist/server/devServer.js.map +1 -1
- package/dist/services/api.js +116 -0
- package/dist/services/api.js.map +1 -0
- package/dist/util/getProjectDefaults.js +4 -1
- package/dist/util/getProjectDefaults.js.map +1 -1
- package/dist/util/packageManager/preferredPm.js +45 -6
- package/dist/util/packageManager/preferredPm.js.map +1 -1
- package/oclif.manifest.json +709 -528
- package/package.json +14 -7
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { buffer } from 'node:stream/consumers';
|
|
4
|
+
import { styleText } from 'node:util';
|
|
5
|
+
import { Args, Flags } from '@oclif/core';
|
|
6
|
+
import { colorizeJson, exitCodes, SanityCommand, subdebug } from '@sanity/cli-core';
|
|
7
|
+
import { ApiUsageError, ProjectIdRequiredError } from '../actions/api/errors.js';
|
|
8
|
+
import { fieldsToQuery, parseFields } from '../actions/api/parseFields.js';
|
|
9
|
+
import { resolveEndpoint } from '../actions/api/resolveEndpoint.js';
|
|
10
|
+
import { apiRoutes } from '../generated/apiRoutes.js';
|
|
11
|
+
import { promptForProject } from '../prompts/promptForProject.js';
|
|
12
|
+
import { performApiRequest } from '../services/api.js';
|
|
13
|
+
import { getDatasetFlag, getProjectIdFlag } from '../util/sharedFlags.js';
|
|
14
|
+
const apiDebug = subdebug('api');
|
|
15
|
+
const METHOD_RE = /^[a-z]+$/i;
|
|
16
|
+
const BODYLESS_METHODS = new Set([
|
|
17
|
+
'GET',
|
|
18
|
+
'HEAD'
|
|
19
|
+
]);
|
|
20
|
+
export class ApiCommand extends SanityCommand {
|
|
21
|
+
static args = {
|
|
22
|
+
endpoint: Args.string({
|
|
23
|
+
description: 'API path (eg "projects" or "data/query/{dataset}"), optionally with placeholders, or a full https://*.api.sanity.io URL',
|
|
24
|
+
required: true
|
|
25
|
+
})
|
|
26
|
+
};
|
|
27
|
+
static description = `Make an authenticated HTTP request to a Sanity API
|
|
28
|
+
|
|
29
|
+
The endpoint argument is an API path as documented in the published OpenAPI
|
|
30
|
+
specifications - list them with "sanity openapi list" and inspect one with
|
|
31
|
+
"sanity openapi get <slug>". Paths can be copied verbatim from the specs:
|
|
32
|
+
{projectId} and {dataset} placeholders are filled in from flags or the CLI
|
|
33
|
+
configuration, and the API host (api.sanity.io or <projectId>.api.sanity.io)
|
|
34
|
+
is chosen based on the specs' routing information.
|
|
35
|
+
|
|
36
|
+
The default request method is GET, or POST when fields or --input are
|
|
37
|
+
provided. For GET/HEAD requests, fields are sent as query parameters;
|
|
38
|
+
otherwise they are combined into a JSON request body sent with
|
|
39
|
+
"Content-Type: application/json". Raw --input bodies are sent without a
|
|
40
|
+
default Content-Type - provide one with -H when the API requires it. The
|
|
41
|
+
response body is written to stdout.
|
|
42
|
+
|
|
43
|
+
Requests are authenticated with the token from "sanity login". To use a
|
|
44
|
+
specific token instead - for example in CI or when the CLI is not logged in
|
|
45
|
+
- pass --token or set the SANITY_AUTH_TOKEN environment variable. Pass
|
|
46
|
+
--anonymous to send no token at all.`;
|
|
47
|
+
static examples = [
|
|
48
|
+
{
|
|
49
|
+
command: '<%= config.bin %> <%= command.id %> users/me',
|
|
50
|
+
description: 'Get the current user'
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
command: '<%= config.bin %> <%= command.id %> projects/{projectId}',
|
|
54
|
+
description: 'Get the current project (placeholder filled from CLI config)'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
command: `<%= config.bin %> <%= command.id %> 'data/query/{dataset}' -f query='*[_type == "movie"][0..2]'`,
|
|
58
|
+
description: 'Run a GROQ query against the project host'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
command: '<%= config.bin %> <%= command.id %> projects/{projectId} -X PATCH -F displayName="My project"',
|
|
62
|
+
description: 'Send a JSON body built from typed fields'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
command: `echo '{"mutations": []}' | <%= config.bin %> <%= command.id %> 'data/mutate/{dataset}' --input - -H 'Content-Type: application/json'`,
|
|
66
|
+
description: 'Send a raw request body from stdin'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
command: '<%= config.bin %> <%= command.id %> jobs/123 --include --api-version v2025-02-19',
|
|
70
|
+
description: 'Include the response status and headers, pinning the API version'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
command: 'SANITY_AUTH_TOKEN=<token> <%= config.bin %> <%= command.id %> users/me',
|
|
74
|
+
description: 'Authenticate with a specific token instead of the logged-in session'
|
|
75
|
+
}
|
|
76
|
+
];
|
|
77
|
+
static flags = {
|
|
78
|
+
...getProjectIdFlag({
|
|
79
|
+
description: 'Project ID for {projectId} placeholders and project-hosted APIs',
|
|
80
|
+
semantics: 'override'
|
|
81
|
+
}),
|
|
82
|
+
...getDatasetFlag({
|
|
83
|
+
description: 'Dataset for {dataset} placeholders',
|
|
84
|
+
semantics: 'override'
|
|
85
|
+
}),
|
|
86
|
+
anonymous: Flags.boolean({
|
|
87
|
+
default: false,
|
|
88
|
+
description: 'Send the request without an authorization token'
|
|
89
|
+
}),
|
|
90
|
+
'api-version': Flags.string({
|
|
91
|
+
description: 'API version to use (eg v2025-02-19). Defaults to a version embedded in the endpoint path, or the version from the matching OpenAPI spec',
|
|
92
|
+
helpValue: '<version>'
|
|
93
|
+
}),
|
|
94
|
+
field: Flags.string({
|
|
95
|
+
char: 'F',
|
|
96
|
+
description: 'Add a typed parameter (key=value): true/false/null and numbers are converted, @file reads the value from a file, @- from stdin',
|
|
97
|
+
helpValue: '<key=value>',
|
|
98
|
+
multiple: true
|
|
99
|
+
}),
|
|
100
|
+
global: Flags.boolean({
|
|
101
|
+
description: 'Force the request to the global API host (api.sanity.io)',
|
|
102
|
+
exclusive: [
|
|
103
|
+
'project-hosted'
|
|
104
|
+
]
|
|
105
|
+
}),
|
|
106
|
+
header: Flags.string({
|
|
107
|
+
char: 'H',
|
|
108
|
+
description: 'Add an HTTP request header (key: value)',
|
|
109
|
+
helpValue: '<key:value>',
|
|
110
|
+
multiple: true
|
|
111
|
+
}),
|
|
112
|
+
include: Flags.boolean({
|
|
113
|
+
char: 'i',
|
|
114
|
+
default: false,
|
|
115
|
+
description: 'Include the HTTP response status and headers in the output'
|
|
116
|
+
}),
|
|
117
|
+
input: Flags.string({
|
|
118
|
+
description: 'Read the raw request body from a file (use "-" for stdin). Sent without a default Content-Type - provide one with -H when the API requires it',
|
|
119
|
+
exclusive: [
|
|
120
|
+
'field',
|
|
121
|
+
'raw-field'
|
|
122
|
+
],
|
|
123
|
+
helpValue: '<file>'
|
|
124
|
+
}),
|
|
125
|
+
method: Flags.string({
|
|
126
|
+
char: 'X',
|
|
127
|
+
description: 'HTTP method to use (default GET, or POST when fields or --input are provided)',
|
|
128
|
+
helpValue: '<method>'
|
|
129
|
+
}),
|
|
130
|
+
pretty: Flags.boolean({
|
|
131
|
+
default: false,
|
|
132
|
+
description: 'Colorize JSON output'
|
|
133
|
+
}),
|
|
134
|
+
'project-hosted': Flags.boolean({
|
|
135
|
+
description: 'Force the request to the project API host (<projectId>.api.sanity.io)',
|
|
136
|
+
exclusive: [
|
|
137
|
+
'global'
|
|
138
|
+
]
|
|
139
|
+
}),
|
|
140
|
+
'raw-field': Flags.string({
|
|
141
|
+
char: 'f',
|
|
142
|
+
description: 'Add a string parameter (key=value)',
|
|
143
|
+
helpValue: '<key=value>',
|
|
144
|
+
multiple: true
|
|
145
|
+
}),
|
|
146
|
+
token: Flags.string({
|
|
147
|
+
char: 't',
|
|
148
|
+
description: 'API token to authenticate with, instead of the logged-in user token',
|
|
149
|
+
exclusive: [
|
|
150
|
+
'anonymous'
|
|
151
|
+
],
|
|
152
|
+
helpValue: '<token>'
|
|
153
|
+
})
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Read the raw body for `--input`, or the content referenced by a `@-`
|
|
157
|
+
* field value. Returns raw bytes so binary bodies survive unmodified.
|
|
158
|
+
* Extracted so tests can substitute stdin.
|
|
159
|
+
*/ readStdin() {
|
|
160
|
+
return buffer(process.stdin);
|
|
161
|
+
}
|
|
162
|
+
async run() {
|
|
163
|
+
const { args, flags } = await this.parse(ApiCommand);
|
|
164
|
+
try {
|
|
165
|
+
await this.performRequest(args.endpoint, flags);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof ApiUsageError) {
|
|
168
|
+
this.error(error.message, {
|
|
169
|
+
exit: exitCodes.USAGE_ERROR
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async performRequest(endpoint, flags) {
|
|
176
|
+
const stdin = await this.resolveStdin(flags);
|
|
177
|
+
const fields = parseFields({
|
|
178
|
+
fields: flags.field,
|
|
179
|
+
rawFields: flags['raw-field'],
|
|
180
|
+
readFile: (path)=>readFileSync(path, 'utf8'),
|
|
181
|
+
stdin: stdin?.toString('utf8')
|
|
182
|
+
});
|
|
183
|
+
const hasFields = Object.keys(fields).length > 0;
|
|
184
|
+
const method = this.resolveMethod(flags.method, hasFields || flags.input !== undefined);
|
|
185
|
+
const useQueryFields = BODYLESS_METHODS.has(method);
|
|
186
|
+
const body = await this.resolveBody(flags, fields, hasFields, useQueryFields, stdin);
|
|
187
|
+
const query = useQueryFields && hasFields ? fieldsToQuery(fields) : {};
|
|
188
|
+
const resolved = await this.resolveTarget(endpoint, flags);
|
|
189
|
+
apiDebug('Resolved endpoint', resolved);
|
|
190
|
+
const response = await performApiRequest({
|
|
191
|
+
...body === undefined ? {} : {
|
|
192
|
+
body
|
|
193
|
+
},
|
|
194
|
+
headers: parseHeaders(flags.header ?? []),
|
|
195
|
+
method,
|
|
196
|
+
query,
|
|
197
|
+
resolved,
|
|
198
|
+
token: flags.token,
|
|
199
|
+
unauthenticated: flags.anonymous
|
|
200
|
+
});
|
|
201
|
+
this.printResponse(response, flags);
|
|
202
|
+
if (response.statusCode >= 400) {
|
|
203
|
+
const statusLine = `HTTP ${response.statusCode}${response.statusMessage ? ` ${response.statusMessage}` : ''}`;
|
|
204
|
+
const loginHint = response.statusCode === 401 && !flags.anonymous ? `. You may need to login again with ${styleText('cyan', 'sanity login')}` : '';
|
|
205
|
+
this.error(`${statusLine}${loginHint}`, {
|
|
206
|
+
exit: exitCodes.RUNTIME_ERROR
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
printResponse(response, flags) {
|
|
211
|
+
if (flags.include) {
|
|
212
|
+
this.log(`HTTP ${response.statusCode}${response.statusMessage ? ` ${response.statusMessage}` : ''}`);
|
|
213
|
+
for (const [key, value] of Object.entries(response.headers)){
|
|
214
|
+
this.log(`${key}: ${value}`);
|
|
215
|
+
}
|
|
216
|
+
this.log('');
|
|
217
|
+
}
|
|
218
|
+
if (!response.jsonBody) {
|
|
219
|
+
if (response.rawBody !== '') this.log(response.rawBody);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
this.log(flags.pretty ? colorizeJson(response.body) : JSON.stringify(response.body, null, 2));
|
|
223
|
+
}
|
|
224
|
+
async resolveBody(flags, fields, hasFields, useQueryFields, stdin) {
|
|
225
|
+
if (flags.input !== undefined) {
|
|
226
|
+
if (flags.input === '-') return stdin;
|
|
227
|
+
try {
|
|
228
|
+
// Read raw bytes - `--input` bodies may be binary (eg asset uploads)
|
|
229
|
+
return await readFile(flags.input);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
232
|
+
throw new ApiUsageError(`Failed to read --input file "${flags.input}": ${message}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return hasFields && !useQueryFields ? fields : undefined;
|
|
236
|
+
}
|
|
237
|
+
resolveMethod(methodFlag, hasBody) {
|
|
238
|
+
if (methodFlag === undefined) {
|
|
239
|
+
return hasBody ? 'POST' : 'GET';
|
|
240
|
+
}
|
|
241
|
+
if (!METHOD_RE.test(methodFlag)) {
|
|
242
|
+
throw new ApiUsageError(`Invalid HTTP method "${methodFlag}"`);
|
|
243
|
+
}
|
|
244
|
+
return methodFlag.toUpperCase();
|
|
245
|
+
}
|
|
246
|
+
async resolveStdin(flags) {
|
|
247
|
+
const needsStdin = flags.input === '-' || (flags.field ?? []).some((field)=>field.endsWith('=@-'));
|
|
248
|
+
return needsStdin ? this.readStdin() : undefined;
|
|
249
|
+
}
|
|
250
|
+
async resolveTarget(endpoint, flags) {
|
|
251
|
+
const cliConfig = await this.tryGetCliConfig();
|
|
252
|
+
const baseOptions = {
|
|
253
|
+
apiVersion: flags['api-version'],
|
|
254
|
+
dataset: flags.dataset ?? cliConfig.api?.dataset,
|
|
255
|
+
endpoint,
|
|
256
|
+
forceHost: flags.global ? 'global' : undefined,
|
|
257
|
+
routes: apiRoutes
|
|
258
|
+
};
|
|
259
|
+
const forceHost = flags['project-hosted'] ? 'project' : baseOptions.forceHost;
|
|
260
|
+
try {
|
|
261
|
+
return resolveEndpoint({
|
|
262
|
+
...baseOptions,
|
|
263
|
+
forceHost,
|
|
264
|
+
projectId: flags['project-id'] ?? cliConfig.api?.projectId
|
|
265
|
+
});
|
|
266
|
+
} catch (error) {
|
|
267
|
+
if (!(error instanceof ProjectIdRequiredError)) throw error;
|
|
268
|
+
// The request needs a project ID that flags/config didn't provide -
|
|
269
|
+
// resolve one interactively (or fail with actionable suggestions).
|
|
270
|
+
const projectId = await this.getProjectId({
|
|
271
|
+
fallback: ()=>promptForProject({})
|
|
272
|
+
});
|
|
273
|
+
return resolveEndpoint({
|
|
274
|
+
...baseOptions,
|
|
275
|
+
forceHost,
|
|
276
|
+
projectId
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function parseHeaders(headerFlags) {
|
|
282
|
+
// Null-prototype for the same reason as the field containers in parseFields
|
|
283
|
+
const headers = Object.create(null);
|
|
284
|
+
for (const header of headerFlags){
|
|
285
|
+
const separatorIndex = header.indexOf(':');
|
|
286
|
+
if (separatorIndex < 1) {
|
|
287
|
+
throw new ApiUsageError(`Invalid --header "${header}": expected "key: value" format`);
|
|
288
|
+
}
|
|
289
|
+
headers[header.slice(0, separatorIndex).trim()] = header.slice(separatorIndex + 1).trim();
|
|
290
|
+
}
|
|
291
|
+
return headers;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/commands/api.ts"],"sourcesContent":["import {readFileSync} from 'node:fs'\nimport {readFile} from 'node:fs/promises'\nimport {buffer} from 'node:stream/consumers'\nimport {styleText} from 'node:util'\n\nimport {Args, Flags} from '@oclif/core'\nimport {colorizeJson, exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'\n\nimport {ApiUsageError, ProjectIdRequiredError} from '../actions/api/errors.js'\nimport {fieldsToQuery, type FieldValue, parseFields} from '../actions/api/parseFields.js'\nimport {type ResolvedEndpoint, resolveEndpoint} from '../actions/api/resolveEndpoint.js'\nimport {apiRoutes} from '../generated/apiRoutes.js'\nimport {promptForProject} from '../prompts/promptForProject.js'\nimport {type ApiResponse, performApiRequest} from '../services/api.js'\nimport {getDatasetFlag, getProjectIdFlag} from '../util/sharedFlags.js'\n\nconst apiDebug = subdebug('api')\n\nconst METHOD_RE = /^[a-z]+$/i\n\nconst BODYLESS_METHODS = new Set(['GET', 'HEAD'])\n\nexport class ApiCommand extends SanityCommand<typeof ApiCommand> {\n static override args = {\n endpoint: Args.string({\n description:\n 'API path (eg \"projects\" or \"data/query/{dataset}\"), optionally with placeholders, or a full https://*.api.sanity.io URL',\n required: true,\n }),\n }\n\n static override description = `Make an authenticated HTTP request to a Sanity API\n\nThe endpoint argument is an API path as documented in the published OpenAPI\nspecifications - list them with \"sanity openapi list\" and inspect one with\n\"sanity openapi get <slug>\". Paths can be copied verbatim from the specs:\n{projectId} and {dataset} placeholders are filled in from flags or the CLI\nconfiguration, and the API host (api.sanity.io or <projectId>.api.sanity.io)\nis chosen based on the specs' routing information.\n\nThe default request method is GET, or POST when fields or --input are\nprovided. For GET/HEAD requests, fields are sent as query parameters;\notherwise they are combined into a JSON request body sent with\n\"Content-Type: application/json\". Raw --input bodies are sent without a\ndefault Content-Type - provide one with -H when the API requires it. The\nresponse body is written to stdout.\n\nRequests are authenticated with the token from \"sanity login\". To use a\nspecific token instead - for example in CI or when the CLI is not logged in\n- pass --token or set the SANITY_AUTH_TOKEN environment variable. Pass\n--anonymous to send no token at all.`\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %> users/me',\n description: 'Get the current user',\n },\n {\n command: '<%= config.bin %> <%= command.id %> projects/{projectId}',\n description: 'Get the current project (placeholder filled from CLI config)',\n },\n {\n command: `<%= config.bin %> <%= command.id %> 'data/query/{dataset}' -f query='*[_type == \"movie\"][0..2]'`,\n description: 'Run a GROQ query against the project host',\n },\n {\n command:\n '<%= config.bin %> <%= command.id %> projects/{projectId} -X PATCH -F displayName=\"My project\"',\n description: 'Send a JSON body built from typed fields',\n },\n {\n command: `echo '{\"mutations\": []}' | <%= config.bin %> <%= command.id %> 'data/mutate/{dataset}' --input - -H 'Content-Type: application/json'`,\n description: 'Send a raw request body from stdin',\n },\n {\n command: '<%= config.bin %> <%= command.id %> jobs/123 --include --api-version v2025-02-19',\n description: 'Include the response status and headers, pinning the API version',\n },\n {\n command: 'SANITY_AUTH_TOKEN=<token> <%= config.bin %> <%= command.id %> users/me',\n description: 'Authenticate with a specific token instead of the logged-in session',\n },\n ]\n\n static override flags = {\n ...getProjectIdFlag({\n description: 'Project ID for {projectId} placeholders and project-hosted APIs',\n semantics: 'override',\n }),\n ...getDatasetFlag({\n description: 'Dataset for {dataset} placeholders',\n semantics: 'override',\n }),\n anonymous: Flags.boolean({\n default: false,\n description: 'Send the request without an authorization token',\n }),\n 'api-version': Flags.string({\n description:\n 'API version to use (eg v2025-02-19). Defaults to a version embedded in the endpoint path, or the version from the matching OpenAPI spec',\n helpValue: '<version>',\n }),\n field: Flags.string({\n char: 'F',\n description:\n 'Add a typed parameter (key=value): true/false/null and numbers are converted, @file reads the value from a file, @- from stdin',\n helpValue: '<key=value>',\n multiple: true,\n }),\n global: Flags.boolean({\n description: 'Force the request to the global API host (api.sanity.io)',\n exclusive: ['project-hosted'],\n }),\n header: Flags.string({\n char: 'H',\n description: 'Add an HTTP request header (key: value)',\n helpValue: '<key:value>',\n multiple: true,\n }),\n include: Flags.boolean({\n char: 'i',\n default: false,\n description: 'Include the HTTP response status and headers in the output',\n }),\n input: Flags.string({\n description:\n 'Read the raw request body from a file (use \"-\" for stdin). Sent without a default Content-Type - provide one with -H when the API requires it',\n exclusive: ['field', 'raw-field'],\n helpValue: '<file>',\n }),\n method: Flags.string({\n char: 'X',\n description: 'HTTP method to use (default GET, or POST when fields or --input are provided)',\n helpValue: '<method>',\n }),\n pretty: Flags.boolean({\n default: false,\n description: 'Colorize JSON output',\n }),\n 'project-hosted': Flags.boolean({\n description: 'Force the request to the project API host (<projectId>.api.sanity.io)',\n exclusive: ['global'],\n }),\n 'raw-field': Flags.string({\n char: 'f',\n description: 'Add a string parameter (key=value)',\n helpValue: '<key=value>',\n multiple: true,\n }),\n token: Flags.string({\n char: 't',\n description: 'API token to authenticate with, instead of the logged-in user token',\n exclusive: ['anonymous'],\n helpValue: '<token>',\n }),\n }\n\n /**\n * Read the raw body for `--input`, or the content referenced by a `@-`\n * field value. Returns raw bytes so binary bodies survive unmodified.\n * Extracted so tests can substitute stdin.\n */\n protected readStdin(): Promise<Buffer> {\n return buffer(process.stdin)\n }\n\n public async run(): Promise<void> {\n const {args, flags} = await this.parse(ApiCommand)\n\n try {\n await this.performRequest(args.endpoint, flags)\n } catch (error) {\n if (error instanceof ApiUsageError) {\n this.error(error.message, {exit: exitCodes.USAGE_ERROR})\n }\n throw error\n }\n }\n\n private async performRequest(\n endpoint: string,\n flags: (typeof ApiCommand.prototype)['flags'],\n ): Promise<void> {\n const stdin = await this.resolveStdin(flags)\n\n const fields = parseFields({\n fields: flags.field,\n rawFields: flags['raw-field'],\n readFile: (path) => readFileSync(path, 'utf8'),\n stdin: stdin?.toString('utf8'),\n })\n const hasFields = Object.keys(fields).length > 0\n\n const method = this.resolveMethod(flags.method, hasFields || flags.input !== undefined)\n const useQueryFields = BODYLESS_METHODS.has(method)\n\n const body = await this.resolveBody(flags, fields, hasFields, useQueryFields, stdin)\n const query = useQueryFields && hasFields ? fieldsToQuery(fields) : {}\n\n const resolved = await this.resolveTarget(endpoint, flags)\n apiDebug('Resolved endpoint', resolved)\n\n const response = await performApiRequest({\n ...(body === undefined ? {} : {body}),\n headers: parseHeaders(flags.header ?? []),\n method,\n query,\n resolved,\n token: flags.token,\n unauthenticated: flags.anonymous,\n })\n\n this.printResponse(response, flags)\n\n if (response.statusCode >= 400) {\n const statusLine = `HTTP ${response.statusCode}${\n response.statusMessage ? ` ${response.statusMessage}` : ''\n }`\n const loginHint =\n response.statusCode === 401 && !flags.anonymous\n ? `. You may need to login again with ${styleText('cyan', 'sanity login')}`\n : ''\n this.error(`${statusLine}${loginHint}`, {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n\n private printResponse(response: ApiResponse, flags: {include: boolean; pretty: boolean}): void {\n if (flags.include) {\n this.log(\n `HTTP ${response.statusCode}${response.statusMessage ? ` ${response.statusMessage}` : ''}`,\n )\n for (const [key, value] of Object.entries(response.headers)) {\n this.log(`${key}: ${value}`)\n }\n this.log('')\n }\n\n if (!response.jsonBody) {\n if (response.rawBody !== '') this.log(response.rawBody)\n return\n }\n\n this.log(flags.pretty ? colorizeJson(response.body) : JSON.stringify(response.body, null, 2))\n }\n\n private async resolveBody(\n flags: {input?: string},\n fields: Record<string, FieldValue>,\n hasFields: boolean,\n useQueryFields: boolean,\n stdin: Buffer | undefined,\n ): Promise<Buffer | unknown | undefined> {\n if (flags.input !== undefined) {\n if (flags.input === '-') return stdin\n try {\n // Read raw bytes - `--input` bodies may be binary (eg asset uploads)\n return await readFile(flags.input)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new ApiUsageError(`Failed to read --input file \"${flags.input}\": ${message}`)\n }\n }\n\n return hasFields && !useQueryFields ? fields : undefined\n }\n\n private resolveMethod(methodFlag: string | undefined, hasBody: boolean): string {\n if (methodFlag === undefined) {\n return hasBody ? 'POST' : 'GET'\n }\n if (!METHOD_RE.test(methodFlag)) {\n throw new ApiUsageError(`Invalid HTTP method \"${methodFlag}\"`)\n }\n return methodFlag.toUpperCase()\n }\n\n private async resolveStdin(flags: {\n field?: string[]\n input?: string\n }): Promise<Buffer | undefined> {\n const needsStdin =\n flags.input === '-' || (flags.field ?? []).some((field) => field.endsWith('=@-'))\n return needsStdin ? this.readStdin() : undefined\n }\n\n private async resolveTarget(\n endpoint: string,\n flags: {\n 'api-version'?: string\n dataset?: string\n global?: boolean\n 'project-hosted'?: boolean\n 'project-id'?: string\n },\n ): Promise<ResolvedEndpoint> {\n const cliConfig = await this.tryGetCliConfig()\n\n const baseOptions = {\n apiVersion: flags['api-version'],\n dataset: flags.dataset ?? cliConfig.api?.dataset,\n endpoint,\n forceHost: flags.global ? ('global' as const) : undefined,\n routes: apiRoutes,\n }\n const forceHost = flags['project-hosted'] ? ('project' as const) : baseOptions.forceHost\n\n try {\n return resolveEndpoint({\n ...baseOptions,\n forceHost,\n projectId: flags['project-id'] ?? cliConfig.api?.projectId,\n })\n } catch (error) {\n if (!(error instanceof ProjectIdRequiredError)) throw error\n\n // The request needs a project ID that flags/config didn't provide -\n // resolve one interactively (or fail with actionable suggestions).\n const projectId = await this.getProjectId({fallback: () => promptForProject({})})\n return resolveEndpoint({...baseOptions, forceHost, projectId})\n }\n }\n}\n\nfunction parseHeaders(headerFlags: string[]): Record<string, string> {\n // Null-prototype for the same reason as the field containers in parseFields\n const headers: Record<string, string> = Object.create(null)\n\n for (const header of headerFlags) {\n const separatorIndex = header.indexOf(':')\n if (separatorIndex < 1) {\n throw new ApiUsageError(`Invalid --header \"${header}\": expected \"key: value\" format`)\n }\n headers[header.slice(0, separatorIndex).trim()] = header.slice(separatorIndex + 1).trim()\n }\n\n return headers\n}\n"],"names":["readFileSync","readFile","buffer","styleText","Args","Flags","colorizeJson","exitCodes","SanityCommand","subdebug","ApiUsageError","ProjectIdRequiredError","fieldsToQuery","parseFields","resolveEndpoint","apiRoutes","promptForProject","performApiRequest","getDatasetFlag","getProjectIdFlag","apiDebug","METHOD_RE","BODYLESS_METHODS","Set","ApiCommand","args","endpoint","string","description","required","examples","command","flags","semantics","anonymous","boolean","default","helpValue","field","char","multiple","global","exclusive","header","include","input","method","pretty","token","readStdin","process","stdin","run","parse","performRequest","error","message","exit","USAGE_ERROR","resolveStdin","fields","rawFields","path","toString","hasFields","Object","keys","length","resolveMethod","undefined","useQueryFields","has","body","resolveBody","query","resolved","resolveTarget","response","headers","parseHeaders","unauthenticated","printResponse","statusCode","statusLine","statusMessage","loginHint","RUNTIME_ERROR","log","key","value","entries","jsonBody","rawBody","JSON","stringify","Error","String","methodFlag","hasBody","test","toUpperCase","needsStdin","some","endsWith","cliConfig","tryGetCliConfig","baseOptions","apiVersion","dataset","api","forceHost","routes","projectId","getProjectId","fallback","headerFlags","create","separatorIndex","indexOf","slice","trim"],"mappings":"AAAA,SAAQA,YAAY,QAAO,UAAS;AACpC,SAAQC,QAAQ,QAAO,mBAAkB;AACzC,SAAQC,MAAM,QAAO,wBAAuB;AAC5C,SAAQC,SAAS,QAAO,YAAW;AAEnC,SAAQC,IAAI,EAAEC,KAAK,QAAO,cAAa;AACvC,SAAQC,YAAY,EAAEC,SAAS,EAAEC,aAAa,EAAEC,QAAQ,QAAO,mBAAkB;AAEjF,SAAQC,aAAa,EAAEC,sBAAsB,QAAO,2BAA0B;AAC9E,SAAQC,aAAa,EAAmBC,WAAW,QAAO,gCAA+B;AACzF,SAA+BC,eAAe,QAAO,oCAAmC;AACxF,SAAQC,SAAS,QAAO,4BAA2B;AACnD,SAAQC,gBAAgB,QAAO,iCAAgC;AAC/D,SAA0BC,iBAAiB,QAAO,qBAAoB;AACtE,SAAQC,cAAc,EAAEC,gBAAgB,QAAO,yBAAwB;AAEvE,MAAMC,WAAWX,SAAS;AAE1B,MAAMY,YAAY;AAElB,MAAMC,mBAAmB,IAAIC,IAAI;IAAC;IAAO;CAAO;AAEhD,OAAO,MAAMC,mBAAmBhB;IAC9B,OAAgBiB,OAAO;QACrBC,UAAUtB,KAAKuB,MAAM,CAAC;YACpBC,aACE;YACFC,UAAU;QACZ;IACF,EAAC;IAED,OAAgBD,cAAc,CAAC;;;;;;;;;;;;;;;;;;;oCAmBG,CAAC,CAAA;IAEnC,OAAgBE,WAAW;QACzB;YACEC,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS,CAAC,+FAA+F,CAAC;YAC1GH,aAAa;QACf;QACA;YACEG,SACE;YACFH,aAAa;QACf;QACA;YACEG,SAAS,CAAC,oIAAoI,CAAC;YAC/IH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aAAa;QACf;KACD,CAAA;IAED,OAAgBI,QAAQ;QACtB,GAAGb,iBAAiB;YAClBS,aAAa;YACbK,WAAW;QACb,EAAE;QACF,GAAGf,eAAe;YAChBU,aAAa;YACbK,WAAW;QACb,EAAE;QACFC,WAAW7B,MAAM8B,OAAO,CAAC;YACvBC,SAAS;YACTR,aAAa;QACf;QACA,eAAevB,MAAMsB,MAAM,CAAC;YAC1BC,aACE;YACFS,WAAW;QACb;QACAC,OAAOjC,MAAMsB,MAAM,CAAC;YAClBY,MAAM;YACNX,aACE;YACFS,WAAW;YACXG,UAAU;QACZ;QACAC,QAAQpC,MAAM8B,OAAO,CAAC;YACpBP,aAAa;YACbc,WAAW;gBAAC;aAAiB;QAC/B;QACAC,QAAQtC,MAAMsB,MAAM,CAAC;YACnBY,MAAM;YACNX,aAAa;YACbS,WAAW;YACXG,UAAU;QACZ;QACAI,SAASvC,MAAM8B,OAAO,CAAC;YACrBI,MAAM;YACNH,SAAS;YACTR,aAAa;QACf;QACAiB,OAAOxC,MAAMsB,MAAM,CAAC;YAClBC,aACE;YACFc,WAAW;gBAAC;gBAAS;aAAY;YACjCL,WAAW;QACb;QACAS,QAAQzC,MAAMsB,MAAM,CAAC;YACnBY,MAAM;YACNX,aAAa;YACbS,WAAW;QACb;QACAU,QAAQ1C,MAAM8B,OAAO,CAAC;YACpBC,SAAS;YACTR,aAAa;QACf;QACA,kBAAkBvB,MAAM8B,OAAO,CAAC;YAC9BP,aAAa;YACbc,WAAW;gBAAC;aAAS;QACvB;QACA,aAAarC,MAAMsB,MAAM,CAAC;YACxBY,MAAM;YACNX,aAAa;YACbS,WAAW;YACXG,UAAU;QACZ;QACAQ,OAAO3C,MAAMsB,MAAM,CAAC;YAClBY,MAAM;YACNX,aAAa;YACbc,WAAW;gBAAC;aAAY;YACxBL,WAAW;QACb;IACF,EAAC;IAED;;;;GAIC,GACD,AAAUY,YAA6B;QACrC,OAAO/C,OAAOgD,QAAQC,KAAK;IAC7B;IAEA,MAAaC,MAAqB;QAChC,MAAM,EAAC3B,IAAI,EAAEO,KAAK,EAAC,GAAG,MAAM,IAAI,CAACqB,KAAK,CAAC7B;QAEvC,IAAI;YACF,MAAM,IAAI,CAAC8B,cAAc,CAAC7B,KAAKC,QAAQ,EAAEM;QAC3C,EAAE,OAAOuB,OAAO;YACd,IAAIA,iBAAiB7C,eAAe;gBAClC,IAAI,CAAC6C,KAAK,CAACA,MAAMC,OAAO,EAAE;oBAACC,MAAMlD,UAAUmD,WAAW;gBAAA;YACxD;YACA,MAAMH;QACR;IACF;IAEA,MAAcD,eACZ5B,QAAgB,EAChBM,KAA6C,EAC9B;QACf,MAAMmB,QAAQ,MAAM,IAAI,CAACQ,YAAY,CAAC3B;QAEtC,MAAM4B,SAAS/C,YAAY;YACzB+C,QAAQ5B,MAAMM,KAAK;YACnBuB,WAAW7B,KAAK,CAAC,YAAY;YAC7B/B,UAAU,CAAC6D,OAAS9D,aAAa8D,MAAM;YACvCX,OAAOA,OAAOY,SAAS;QACzB;QACA,MAAMC,YAAYC,OAAOC,IAAI,CAACN,QAAQO,MAAM,GAAG;QAE/C,MAAMrB,SAAS,IAAI,CAACsB,aAAa,CAACpC,MAAMc,MAAM,EAAEkB,aAAahC,MAAMa,KAAK,KAAKwB;QAC7E,MAAMC,iBAAiBhD,iBAAiBiD,GAAG,CAACzB;QAE5C,MAAM0B,OAAO,MAAM,IAAI,CAACC,WAAW,CAACzC,OAAO4B,QAAQI,WAAWM,gBAAgBnB;QAC9E,MAAMuB,QAAQJ,kBAAkBN,YAAYpD,cAAcgD,UAAU,CAAC;QAErE,MAAMe,WAAW,MAAM,IAAI,CAACC,aAAa,CAAClD,UAAUM;QACpDZ,SAAS,qBAAqBuD;QAE9B,MAAME,WAAW,MAAM5D,kBAAkB;YACvC,GAAIuD,SAASH,YAAY,CAAC,IAAI;gBAACG;YAAI,CAAC;YACpCM,SAASC,aAAa/C,MAAMW,MAAM,IAAI,EAAE;YACxCG;YACA4B;YACAC;YACA3B,OAAOhB,MAAMgB,KAAK;YAClBgC,iBAAiBhD,MAAME,SAAS;QAClC;QAEA,IAAI,CAAC+C,aAAa,CAACJ,UAAU7C;QAE7B,IAAI6C,SAASK,UAAU,IAAI,KAAK;YAC9B,MAAMC,aAAa,CAAC,KAAK,EAAEN,SAASK,UAAU,GAC5CL,SAASO,aAAa,GAAG,CAAC,CAAC,EAAEP,SAASO,aAAa,EAAE,GAAG,IACxD;YACF,MAAMC,YACJR,SAASK,UAAU,KAAK,OAAO,CAAClD,MAAME,SAAS,GAC3C,CAAC,mCAAmC,EAAE/B,UAAU,QAAQ,iBAAiB,GACzE;YACN,IAAI,CAACoD,KAAK,CAAC,GAAG4B,aAAaE,WAAW,EAAE;gBAAC5B,MAAMlD,UAAU+E,aAAa;YAAA;QACxE;IACF;IAEQL,cAAcJ,QAAqB,EAAE7C,KAA0C,EAAQ;QAC7F,IAAIA,MAAMY,OAAO,EAAE;YACjB,IAAI,CAAC2C,GAAG,CACN,CAAC,KAAK,EAAEV,SAASK,UAAU,GAAGL,SAASO,aAAa,GAAG,CAAC,CAAC,EAAEP,SAASO,aAAa,EAAE,GAAG,IAAI;YAE5F,KAAK,MAAM,CAACI,KAAKC,MAAM,IAAIxB,OAAOyB,OAAO,CAACb,SAASC,OAAO,EAAG;gBAC3D,IAAI,CAACS,GAAG,CAAC,GAAGC,IAAI,EAAE,EAAEC,OAAO;YAC7B;YACA,IAAI,CAACF,GAAG,CAAC;QACX;QAEA,IAAI,CAACV,SAASc,QAAQ,EAAE;YACtB,IAAId,SAASe,OAAO,KAAK,IAAI,IAAI,CAACL,GAAG,CAACV,SAASe,OAAO;YACtD;QACF;QAEA,IAAI,CAACL,GAAG,CAACvD,MAAMe,MAAM,GAAGzC,aAAauE,SAASL,IAAI,IAAIqB,KAAKC,SAAS,CAACjB,SAASL,IAAI,EAAE,MAAM;IAC5F;IAEA,MAAcC,YACZzC,KAAuB,EACvB4B,MAAkC,EAClCI,SAAkB,EAClBM,cAAuB,EACvBnB,KAAyB,EACc;QACvC,IAAInB,MAAMa,KAAK,KAAKwB,WAAW;YAC7B,IAAIrC,MAAMa,KAAK,KAAK,KAAK,OAAOM;YAChC,IAAI;gBACF,qEAAqE;gBACrE,OAAO,MAAMlD,SAAS+B,MAAMa,KAAK;YACnC,EAAE,OAAOU,OAAO;gBACd,MAAMC,UAAUD,iBAAiBwC,QAAQxC,MAAMC,OAAO,GAAGwC,OAAOzC;gBAChE,MAAM,IAAI7C,cAAc,CAAC,6BAA6B,EAAEsB,MAAMa,KAAK,CAAC,GAAG,EAAEW,SAAS;YACpF;QACF;QAEA,OAAOQ,aAAa,CAACM,iBAAiBV,SAASS;IACjD;IAEQD,cAAc6B,UAA8B,EAAEC,OAAgB,EAAU;QAC9E,IAAID,eAAe5B,WAAW;YAC5B,OAAO6B,UAAU,SAAS;QAC5B;QACA,IAAI,CAAC7E,UAAU8E,IAAI,CAACF,aAAa;YAC/B,MAAM,IAAIvF,cAAc,CAAC,qBAAqB,EAAEuF,WAAW,CAAC,CAAC;QAC/D;QACA,OAAOA,WAAWG,WAAW;IAC/B;IAEA,MAAczC,aAAa3B,KAG1B,EAA+B;QAC9B,MAAMqE,aACJrE,MAAMa,KAAK,KAAK,OAAO,AAACb,CAAAA,MAAMM,KAAK,IAAI,EAAE,AAAD,EAAGgE,IAAI,CAAC,CAAChE,QAAUA,MAAMiE,QAAQ,CAAC;QAC5E,OAAOF,aAAa,IAAI,CAACpD,SAAS,KAAKoB;IACzC;IAEA,MAAcO,cACZlD,QAAgB,EAChBM,KAMC,EAC0B;QAC3B,MAAMwE,YAAY,MAAM,IAAI,CAACC,eAAe;QAE5C,MAAMC,cAAc;YAClBC,YAAY3E,KAAK,CAAC,cAAc;YAChC4E,SAAS5E,MAAM4E,OAAO,IAAIJ,UAAUK,GAAG,EAAED;YACzClF;YACAoF,WAAW9E,MAAMS,MAAM,GAAI,WAAqB4B;YAChD0C,QAAQhG;QACV;QACA,MAAM+F,YAAY9E,KAAK,CAAC,iBAAiB,GAAI,YAAsB0E,YAAYI,SAAS;QAExF,IAAI;YACF,OAAOhG,gBAAgB;gBACrB,GAAG4F,WAAW;gBACdI;gBACAE,WAAWhF,KAAK,CAAC,aAAa,IAAIwE,UAAUK,GAAG,EAAEG;YACnD;QACF,EAAE,OAAOzD,OAAO;YACd,IAAI,CAAEA,CAAAA,iBAAiB5C,sBAAqB,GAAI,MAAM4C;YAEtD,oEAAoE;YACpE,mEAAmE;YACnE,MAAMyD,YAAY,MAAM,IAAI,CAACC,YAAY,CAAC;gBAACC,UAAU,IAAMlG,iBAAiB,CAAC;YAAE;YAC/E,OAAOF,gBAAgB;gBAAC,GAAG4F,WAAW;gBAAEI;gBAAWE;YAAS;QAC9D;IACF;AACF;AAEA,SAASjC,aAAaoC,WAAqB;IACzC,4EAA4E;IAC5E,MAAMrC,UAAkCb,OAAOmD,MAAM,CAAC;IAEtD,KAAK,MAAMzE,UAAUwE,YAAa;QAChC,MAAME,iBAAiB1E,OAAO2E,OAAO,CAAC;QACtC,IAAID,iBAAiB,GAAG;YACtB,MAAM,IAAI3G,cAAc,CAAC,kBAAkB,EAAEiC,OAAO,+BAA+B,CAAC;QACtF;QACAmC,OAAO,CAACnC,OAAO4E,KAAK,CAAC,GAAGF,gBAAgBG,IAAI,GAAG,GAAG7E,OAAO4E,KAAK,CAACF,iBAAiB,GAAGG,IAAI;IACzF;IAEA,OAAO1C;AACT"}
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { styleText } from 'node:util';
|
|
4
4
|
import { Args, Flags } from '@oclif/core';
|
|
5
5
|
import { exitCodes, SanityCommand, subdebug } from '@sanity/cli-core';
|
|
6
|
+
import { getCliExecutionContext } from '@sanity/cli-core/executionContext';
|
|
6
7
|
import { confirm, logSymbols } from '@sanity/cli-core/ux';
|
|
7
8
|
import { oneline } from 'oneline';
|
|
8
9
|
import { filterAndValidateOrigin } from '../../actions/cors/filterAndValidateOrigin.js';
|
|
@@ -66,14 +67,19 @@ export class Add extends SanityCommand {
|
|
|
66
67
|
]
|
|
67
68
|
})
|
|
68
69
|
});
|
|
69
|
-
// Check if the origin argument looks like a file path and warn
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
// Check if the origin argument looks like a file path and warn. This only
|
|
71
|
+
// makes sense for shell usage (unquoted globs expanding to filenames), so
|
|
72
|
+
// programmatic invocations skip it — they must never probe the host's
|
|
73
|
+
// filesystem.
|
|
74
|
+
if (!getCliExecutionContext()) {
|
|
75
|
+
try {
|
|
76
|
+
const isFile = fs.existsSync(path.join(process.cwd(), args.origin));
|
|
77
|
+
if (isFile) {
|
|
78
|
+
this.warn(`Origin "${args.origin}?" Remember to quote values (sanity cors add "*")`);
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore errors checking if it's a file
|
|
74
82
|
}
|
|
75
|
-
} catch {
|
|
76
|
-
// Ignore errors checking if it's a file
|
|
77
83
|
}
|
|
78
84
|
const filteredOrigin = await filterAndValidateOrigin(origin, this.output);
|
|
79
85
|
const hasWildcard = origin.includes('*');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/commands/cors/add.ts"],"sourcesContent":["import fs from 'node:fs'\nimport path from 'node:path'\nimport {styleText} from 'node:util'\n\nimport {Args, Flags} from '@oclif/core'\nimport {exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'\nimport {confirm, logSymbols} from '@sanity/cli-core/ux'\nimport {oneline} from 'oneline'\n\nimport {filterAndValidateOrigin} from '../../actions/cors/filterAndValidateOrigin.js'\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {createCorsOrigin} from '../../services/cors.js'\nimport {getProjectIdFlag} from '../../util/sharedFlags.js'\n\nconst addCorsDebug = subdebug('cors:add')\n\nexport class Add extends SanityCommand<typeof Add> {\n static override args = {\n origin: Args.string({\n description: 'Origin to allow (e.g., https://example.com)',\n required: true,\n }),\n }\n\n static override description = 'Add a CORS origin to the project'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Interactively add a CORS origin',\n },\n {\n command: '<%= config.bin %> <%= command.id %> http://localhost:3000 --no-credentials',\n description: 'Add a localhost origin without credentials',\n },\n {\n command: '<%= config.bin %> <%= command.id %> https://myapp.com --credentials',\n description: 'Add a production origin with credentials allowed',\n },\n {\n command: '<%= config.bin %> <%= command.id %> https://myapp.com --project-id abc123',\n description: 'Add a CORS origin for a specific project',\n },\n ]\n\n static override flags = {\n ...getProjectIdFlag({\n description: 'Project ID to add CORS origin to',\n semantics: 'override',\n }),\n credentials: Flags.boolean({\n allowNo: true,\n default: undefined,\n description: 'Allow credentials (token/cookie) to be sent from this origin',\n required: false,\n }),\n yes: Flags.boolean({\n char: 'y',\n description: 'Confirm risky wildcard origins without prompting',\n }),\n }\n\n public async run(): Promise<void> {\n const {args, flags} = await this.parse(Add)\n const {origin} = args\n\n // Ensure we have project context\n const projectId = await this.getProjectId({\n fallback: () =>\n promptForProject({\n requiredPermissions: [{grant: 'create', permission: 'sanity.project.cors'}],\n }),\n })\n\n // Check if the origin argument looks like a file path and warn\n try {\n const isFile = fs.existsSync(path.join(process.cwd(), args.origin))\n if (isFile) {\n this.warn(`Origin \"${args.origin}?\" Remember to quote values (sanity cors add \"*\")`)\n }\n } catch {\n // Ignore errors checking if it's a file\n }\n\n const filteredOrigin = await filterAndValidateOrigin(origin, this.output)\n const hasWildcard = origin.includes('*')\n\n if (hasWildcard) {\n if (this.isUnattended() && !flags.yes) {\n this.error('Wildcard origins require confirmation. Pass `--yes` to continue.', {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n const confirmed = flags.yes || (await this.promptForWildcardConfirmation(origin))\n if (!confirmed) {\n this.log('CORS origin not added')\n this.exit(exitCodes.USER_ABORT)\n }\n }\n\n const allowCredentials =\n flags.credentials === undefined\n ? this.isUnattended()\n ? false\n : await this.promptForCredentials(hasWildcard)\n : Boolean(flags.credentials)\n\n if (filteredOrigin !== origin) {\n this.log(`Normalized origin to: ${filteredOrigin}`)\n }\n\n try {\n const response = await createCorsOrigin({\n allowCredentials,\n origin: filteredOrigin,\n projectId,\n })\n\n addCorsDebug(`CORS origin added successfully`, response)\n\n this.log('CORS origin added')\n } catch (error) {\n const err = error as Error\n\n addCorsDebug(`Error adding CORS origin`, err)\n this.error(`CORS origin addition failed:\\n${err.message}`, {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n\n /**\n * Prompt the user for credentials\n *\n * @param hasWildcard - Whether the origin contains a wildcard\n * @returns - Whether to allow credentials\n */\n private async promptForCredentials(hasWildcard: boolean) {\n this.log('')\n if (hasWildcard) {\n this.log(oneline`\n ${styleText('yellow', `${logSymbols.warning} Warning:`)}\n We ${styleText(['red', 'underline'], 'HIGHLY')} recommend NOT allowing credentials\n on origins containing wildcards. If you are logged in to a Sanity studio or an app built with\n the Sanity App SDK, people will be able to send requests ${styleText('underline', 'on your behalf')}\n to read and modify data, from any matching origin. Please tread carefully!\n `)\n } else {\n this.log(oneline`\n ${styleText('yellow', `${logSymbols.warning} Warning:`)}\n Should this origin be allowed to send requests using authentication tokens or\n session cookies? Be aware that any script on this origin will be able to send\n requests ${styleText('underline', 'on your behalf')} to read and modify data if you\n are logged in to a Sanity studio or app. If this origin hosts a studio or an app built with\n the Sanity App SDK, you will need this, otherwise you should probably answer \"No\" (n).\n `)\n }\n\n this.log('')\n\n return confirm({\n default: false,\n message: oneline`\n Allow credentials to be sent from this origin? Please read the warning above.\n `,\n })\n }\n\n /**\n * Prompt the user for wildcard confirmation\n *\n * @param origin - The origin to check for wildcards\n * @returns - Whether to allow the origin\n */\n private async promptForWildcardConfirmation(origin: string) {\n this.log('')\n this.log(styleText('yellow', `${logSymbols.warning} Warning: Examples of allowed origins:`))\n\n if (origin === '*') {\n this.log('- http://www.some-malicious.site')\n this.log('- https://not.what-you-were-expecting.com')\n this.log('- https://high-traffic-site.com')\n this.log('- http://192.168.1.1:8080')\n } else {\n this.log(`- ${origin.replace(/:\\*/, ':1234').replaceAll('*', 'foo')}`)\n this.log(`- ${origin.replace(/:\\*/, ':3030').replaceAll('*', 'foo.bar')}`)\n }\n\n this.log('')\n\n return confirm({\n default: false,\n message: oneline`\n Using wildcards can be ${styleText('red', 'risky')}.\n Are you ${styleText('underline', 'absolutely sure')} you want to allow this origin?`,\n })\n }\n}\n"],"names":["fs","path","styleText","Args","Flags","exitCodes","SanityCommand","subdebug","confirm","logSymbols","oneline","filterAndValidateOrigin","promptForProject","createCorsOrigin","getProjectIdFlag","addCorsDebug","Add","args","origin","string","description","required","examples","command","flags","semantics","credentials","boolean","allowNo","default","undefined","yes","char","run","parse","projectId","getProjectId","fallback","requiredPermissions","grant","permission","isFile","existsSync","join","process","cwd","warn","filteredOrigin","output","hasWildcard","includes","isUnattended","error","exit","USAGE_ERROR","confirmed","promptForWildcardConfirmation","log","USER_ABORT","allowCredentials","promptForCredentials","Boolean","response","err","message","RUNTIME_ERROR","warning","replace","replaceAll"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/cors/add.ts"],"sourcesContent":["import fs from 'node:fs'\nimport path from 'node:path'\nimport {styleText} from 'node:util'\n\nimport {Args, Flags} from '@oclif/core'\nimport {exitCodes, SanityCommand, subdebug} from '@sanity/cli-core'\nimport {getCliExecutionContext} from '@sanity/cli-core/executionContext'\nimport {confirm, logSymbols} from '@sanity/cli-core/ux'\nimport {oneline} from 'oneline'\n\nimport {filterAndValidateOrigin} from '../../actions/cors/filterAndValidateOrigin.js'\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {createCorsOrigin} from '../../services/cors.js'\nimport {getProjectIdFlag} from '../../util/sharedFlags.js'\n\nconst addCorsDebug = subdebug('cors:add')\n\nexport class Add extends SanityCommand<typeof Add> {\n static override args = {\n origin: Args.string({\n description: 'Origin to allow (e.g., https://example.com)',\n required: true,\n }),\n }\n\n static override description = 'Add a CORS origin to the project'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Interactively add a CORS origin',\n },\n {\n command: '<%= config.bin %> <%= command.id %> http://localhost:3000 --no-credentials',\n description: 'Add a localhost origin without credentials',\n },\n {\n command: '<%= config.bin %> <%= command.id %> https://myapp.com --credentials',\n description: 'Add a production origin with credentials allowed',\n },\n {\n command: '<%= config.bin %> <%= command.id %> https://myapp.com --project-id abc123',\n description: 'Add a CORS origin for a specific project',\n },\n ]\n\n static override flags = {\n ...getProjectIdFlag({\n description: 'Project ID to add CORS origin to',\n semantics: 'override',\n }),\n credentials: Flags.boolean({\n allowNo: true,\n default: undefined,\n description: 'Allow credentials (token/cookie) to be sent from this origin',\n required: false,\n }),\n yes: Flags.boolean({\n char: 'y',\n description: 'Confirm risky wildcard origins without prompting',\n }),\n }\n\n public async run(): Promise<void> {\n const {args, flags} = await this.parse(Add)\n const {origin} = args\n\n // Ensure we have project context\n const projectId = await this.getProjectId({\n fallback: () =>\n promptForProject({\n requiredPermissions: [{grant: 'create', permission: 'sanity.project.cors'}],\n }),\n })\n\n // Check if the origin argument looks like a file path and warn. This only\n // makes sense for shell usage (unquoted globs expanding to filenames), so\n // programmatic invocations skip it — they must never probe the host's\n // filesystem.\n if (!getCliExecutionContext()) {\n try {\n const isFile = fs.existsSync(path.join(process.cwd(), args.origin))\n if (isFile) {\n this.warn(`Origin \"${args.origin}?\" Remember to quote values (sanity cors add \"*\")`)\n }\n } catch {\n // Ignore errors checking if it's a file\n }\n }\n\n const filteredOrigin = await filterAndValidateOrigin(origin, this.output)\n const hasWildcard = origin.includes('*')\n\n if (hasWildcard) {\n if (this.isUnattended() && !flags.yes) {\n this.error('Wildcard origins require confirmation. Pass `--yes` to continue.', {\n exit: exitCodes.USAGE_ERROR,\n })\n }\n const confirmed = flags.yes || (await this.promptForWildcardConfirmation(origin))\n if (!confirmed) {\n this.log('CORS origin not added')\n this.exit(exitCodes.USER_ABORT)\n }\n }\n\n const allowCredentials =\n flags.credentials === undefined\n ? this.isUnattended()\n ? false\n : await this.promptForCredentials(hasWildcard)\n : Boolean(flags.credentials)\n\n if (filteredOrigin !== origin) {\n this.log(`Normalized origin to: ${filteredOrigin}`)\n }\n\n try {\n const response = await createCorsOrigin({\n allowCredentials,\n origin: filteredOrigin,\n projectId,\n })\n\n addCorsDebug(`CORS origin added successfully`, response)\n\n this.log('CORS origin added')\n } catch (error) {\n const err = error as Error\n\n addCorsDebug(`Error adding CORS origin`, err)\n this.error(`CORS origin addition failed:\\n${err.message}`, {exit: exitCodes.RUNTIME_ERROR})\n }\n }\n\n /**\n * Prompt the user for credentials\n *\n * @param hasWildcard - Whether the origin contains a wildcard\n * @returns - Whether to allow credentials\n */\n private async promptForCredentials(hasWildcard: boolean) {\n this.log('')\n if (hasWildcard) {\n this.log(oneline`\n ${styleText('yellow', `${logSymbols.warning} Warning:`)}\n We ${styleText(['red', 'underline'], 'HIGHLY')} recommend NOT allowing credentials\n on origins containing wildcards. If you are logged in to a Sanity studio or an app built with\n the Sanity App SDK, people will be able to send requests ${styleText('underline', 'on your behalf')}\n to read and modify data, from any matching origin. Please tread carefully!\n `)\n } else {\n this.log(oneline`\n ${styleText('yellow', `${logSymbols.warning} Warning:`)}\n Should this origin be allowed to send requests using authentication tokens or\n session cookies? Be aware that any script on this origin will be able to send\n requests ${styleText('underline', 'on your behalf')} to read and modify data if you\n are logged in to a Sanity studio or app. If this origin hosts a studio or an app built with\n the Sanity App SDK, you will need this, otherwise you should probably answer \"No\" (n).\n `)\n }\n\n this.log('')\n\n return confirm({\n default: false,\n message: oneline`\n Allow credentials to be sent from this origin? Please read the warning above.\n `,\n })\n }\n\n /**\n * Prompt the user for wildcard confirmation\n *\n * @param origin - The origin to check for wildcards\n * @returns - Whether to allow the origin\n */\n private async promptForWildcardConfirmation(origin: string) {\n this.log('')\n this.log(styleText('yellow', `${logSymbols.warning} Warning: Examples of allowed origins:`))\n\n if (origin === '*') {\n this.log('- http://www.some-malicious.site')\n this.log('- https://not.what-you-were-expecting.com')\n this.log('- https://high-traffic-site.com')\n this.log('- http://192.168.1.1:8080')\n } else {\n this.log(`- ${origin.replace(/:\\*/, ':1234').replaceAll('*', 'foo')}`)\n this.log(`- ${origin.replace(/:\\*/, ':3030').replaceAll('*', 'foo.bar')}`)\n }\n\n this.log('')\n\n return confirm({\n default: false,\n message: oneline`\n Using wildcards can be ${styleText('red', 'risky')}.\n Are you ${styleText('underline', 'absolutely sure')} you want to allow this origin?`,\n })\n }\n}\n"],"names":["fs","path","styleText","Args","Flags","exitCodes","SanityCommand","subdebug","getCliExecutionContext","confirm","logSymbols","oneline","filterAndValidateOrigin","promptForProject","createCorsOrigin","getProjectIdFlag","addCorsDebug","Add","args","origin","string","description","required","examples","command","flags","semantics","credentials","boolean","allowNo","default","undefined","yes","char","run","parse","projectId","getProjectId","fallback","requiredPermissions","grant","permission","isFile","existsSync","join","process","cwd","warn","filteredOrigin","output","hasWildcard","includes","isUnattended","error","exit","USAGE_ERROR","confirmed","promptForWildcardConfirmation","log","USER_ABORT","allowCredentials","promptForCredentials","Boolean","response","err","message","RUNTIME_ERROR","warning","replace","replaceAll"],"mappings":"AAAA,OAAOA,QAAQ,UAAS;AACxB,OAAOC,UAAU,YAAW;AAC5B,SAAQC,SAAS,QAAO,YAAW;AAEnC,SAAQC,IAAI,EAAEC,KAAK,QAAO,cAAa;AACvC,SAAQC,SAAS,EAAEC,aAAa,EAAEC,QAAQ,QAAO,mBAAkB;AACnE,SAAQC,sBAAsB,QAAO,oCAAmC;AACxE,SAAQC,OAAO,EAAEC,UAAU,QAAO,sBAAqB;AACvD,SAAQC,OAAO,QAAO,UAAS;AAE/B,SAAQC,uBAAuB,QAAO,gDAA+C;AACrF,SAAQC,gBAAgB,QAAO,oCAAmC;AAClE,SAAQC,gBAAgB,QAAO,yBAAwB;AACvD,SAAQC,gBAAgB,QAAO,4BAA2B;AAE1D,MAAMC,eAAeT,SAAS;AAE9B,OAAO,MAAMU,YAAYX;IACvB,OAAgBY,OAAO;QACrBC,QAAQhB,KAAKiB,MAAM,CAAC;YAClBC,aAAa;YACbC,UAAU;QACZ;IACF,EAAC;IAED,OAAgBD,cAAc,mCAAkC;IAEhE,OAAgBE,WAAW;QACzB;YACEC,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aAAa;QACf;QACA;YACEG,SAAS;YACTH,aAAa;QACf;KACD,CAAA;IAED,OAAgBI,QAAQ;QACtB,GAAGV,iBAAiB;YAClBM,aAAa;YACbK,WAAW;QACb,EAAE;QACFC,aAAavB,MAAMwB,OAAO,CAAC;YACzBC,SAAS;YACTC,SAASC;YACTV,aAAa;YACbC,UAAU;QACZ;QACAU,KAAK5B,MAAMwB,OAAO,CAAC;YACjBK,MAAM;YACNZ,aAAa;QACf;IACF,EAAC;IAED,MAAaa,MAAqB;QAChC,MAAM,EAAChB,IAAI,EAAEO,KAAK,EAAC,GAAG,MAAM,IAAI,CAACU,KAAK,CAAClB;QACvC,MAAM,EAACE,MAAM,EAAC,GAAGD;QAEjB,iCAAiC;QACjC,MAAMkB,YAAY,MAAM,IAAI,CAACC,YAAY,CAAC;YACxCC,UAAU,IACRzB,iBAAiB;oBACf0B,qBAAqB;wBAAC;4BAACC,OAAO;4BAAUC,YAAY;wBAAqB;qBAAE;gBAC7E;QACJ;QAEA,0EAA0E;QAC1E,0EAA0E;QAC1E,sEAAsE;QACtE,cAAc;QACd,IAAI,CAACjC,0BAA0B;YAC7B,IAAI;gBACF,MAAMkC,SAAS1C,GAAG2C,UAAU,CAAC1C,KAAK2C,IAAI,CAACC,QAAQC,GAAG,IAAI5B,KAAKC,MAAM;gBACjE,IAAIuB,QAAQ;oBACV,IAAI,CAACK,IAAI,CAAC,CAAC,QAAQ,EAAE7B,KAAKC,MAAM,CAAC,iDAAiD,CAAC;gBACrF;YACF,EAAE,OAAM;YACN,wCAAwC;YAC1C;QACF;QAEA,MAAM6B,iBAAiB,MAAMpC,wBAAwBO,QAAQ,IAAI,CAAC8B,MAAM;QACxE,MAAMC,cAAc/B,OAAOgC,QAAQ,CAAC;QAEpC,IAAID,aAAa;YACf,IAAI,IAAI,CAACE,YAAY,MAAM,CAAC3B,MAAMO,GAAG,EAAE;gBACrC,IAAI,CAACqB,KAAK,CAAC,oEAAoE;oBAC7EC,MAAMjD,UAAUkD,WAAW;gBAC7B;YACF;YACA,MAAMC,YAAY/B,MAAMO,GAAG,IAAK,MAAM,IAAI,CAACyB,6BAA6B,CAACtC;YACzE,IAAI,CAACqC,WAAW;gBACd,IAAI,CAACE,GAAG,CAAC;gBACT,IAAI,CAACJ,IAAI,CAACjD,UAAUsD,UAAU;YAChC;QACF;QAEA,MAAMC,mBACJnC,MAAME,WAAW,KAAKI,YAClB,IAAI,CAACqB,YAAY,KACf,QACA,MAAM,IAAI,CAACS,oBAAoB,CAACX,eAClCY,QAAQrC,MAAME,WAAW;QAE/B,IAAIqB,mBAAmB7B,QAAQ;YAC7B,IAAI,CAACuC,GAAG,CAAC,CAAC,sBAAsB,EAAEV,gBAAgB;QACpD;QAEA,IAAI;YACF,MAAMe,WAAW,MAAMjD,iBAAiB;gBACtC8C;gBACAzC,QAAQ6B;gBACRZ;YACF;YAEApB,aAAa,CAAC,8BAA8B,CAAC,EAAE+C;YAE/C,IAAI,CAACL,GAAG,CAAC;QACX,EAAE,OAAOL,OAAO;YACd,MAAMW,MAAMX;YAEZrC,aAAa,CAAC,wBAAwB,CAAC,EAAEgD;YACzC,IAAI,CAACX,KAAK,CAAC,CAAC,8BAA8B,EAAEW,IAAIC,OAAO,EAAE,EAAE;gBAACX,MAAMjD,UAAU6D,aAAa;YAAA;QAC3F;IACF;IAEA;;;;;GAKC,GACD,MAAcL,qBAAqBX,WAAoB,EAAE;QACvD,IAAI,CAACQ,GAAG,CAAC;QACT,IAAIR,aAAa;YACf,IAAI,CAACQ,GAAG,CAAC/C,OAAO,CAAC;MACjB,EAAET,UAAU,UAAU,GAAGQ,WAAWyD,OAAO,CAAC,SAAS,CAAC,EAAE;SACrD,EAAEjE,UAAU;gBAAC;gBAAO;aAAY,EAAE,UAAU;;+DAEU,EAAEA,UAAU,aAAa,kBAAkB;;IAEtG,CAAC;QACD,OAAO;YACL,IAAI,CAACwD,GAAG,CAAC/C,OAAO,CAAC;MACjB,EAAET,UAAU,UAAU,GAAGQ,WAAWyD,OAAO,CAAC,SAAS,CAAC,EAAE;;;eAG/C,EAAEjE,UAAU,aAAa,kBAAkB;;;IAGtD,CAAC;QACD;QAEA,IAAI,CAACwD,GAAG,CAAC;QAET,OAAOjD,QAAQ;YACbqB,SAAS;YACTmC,SAAStD,OAAO,CAAC;;IAEnB,CAAC;QACD;IACF;IAEA;;;;;GAKC,GACD,MAAc8C,8BAA8BtC,MAAc,EAAE;QAC1D,IAAI,CAACuC,GAAG,CAAC;QACT,IAAI,CAACA,GAAG,CAACxD,UAAU,UAAU,GAAGQ,WAAWyD,OAAO,CAAC,sCAAsC,CAAC;QAE1F,IAAIhD,WAAW,KAAK;YAClB,IAAI,CAACuC,GAAG,CAAC;YACT,IAAI,CAACA,GAAG,CAAC;YACT,IAAI,CAACA,GAAG,CAAC;YACT,IAAI,CAACA,GAAG,CAAC;QACX,OAAO;YACL,IAAI,CAACA,GAAG,CAAC,CAAC,EAAE,EAAEvC,OAAOiD,OAAO,CAAC,OAAO,SAASC,UAAU,CAAC,KAAK,QAAQ;YACrE,IAAI,CAACX,GAAG,CAAC,CAAC,EAAE,EAAEvC,OAAOiD,OAAO,CAAC,OAAO,SAASC,UAAU,CAAC,KAAK,YAAY;QAC3E;QAEA,IAAI,CAACX,GAAG,CAAC;QAET,OAAOjD,QAAQ;YACbqB,SAAS;YACTmC,SAAStD,OAAO,CAAC;6BACM,EAAET,UAAU,OAAO,SAAS;cAC3C,EAAEA,UAAU,aAAa,mBAAmB,+BAA+B,CAAC;QACtF;IACF;AACF"}
|
|
@@ -281,6 +281,12 @@ export class CopyDatasetCommand extends SanityCommand {
|
|
|
281
281
|
copyDatasetDebug('Starting copy mode');
|
|
282
282
|
const skipHistory = Boolean(flags['skip-history']);
|
|
283
283
|
const skipContentReleases = Boolean(flags['skip-content-releases']);
|
|
284
|
+
// Surfaced before any prompting so the flag is still actionable: a copy job
|
|
285
|
+
// can't be canceled once started, so mentioning it after the job kicks off
|
|
286
|
+
// leaves nothing to decide.
|
|
287
|
+
if (!skipHistory) {
|
|
288
|
+
this.output.log(`Note: You can run this command with flag '--skip-history'. The flag will reduce copy time in larger datasets.`);
|
|
289
|
+
}
|
|
284
290
|
// Get and validate source dataset
|
|
285
291
|
let sourceDataset = args.source;
|
|
286
292
|
if (sourceDataset) {
|
|
@@ -335,9 +341,6 @@ export class CopyDatasetCommand extends SanityCommand {
|
|
|
335
341
|
// Start the copy job
|
|
336
342
|
try {
|
|
337
343
|
this.output.log(`Copying dataset ${styleText('green', sourceDataset)} to ${styleText('green', targetDataset)}...`);
|
|
338
|
-
if (!skipHistory) {
|
|
339
|
-
this.output.log(`Note: You can run this command with flag '--skip-history'. The flag will reduce copy time in larger datasets.`);
|
|
340
|
-
}
|
|
341
344
|
const response = await copyDataset({
|
|
342
345
|
projectId,
|
|
343
346
|
skipContentReleases,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/commands/datasets/copy.ts"],"sourcesContent":["import {styleText} from 'node:util'\n\nimport {Args, Flags} from '@oclif/core'\nimport {exit} from '@oclif/core/errors'\nimport {exitCodes} from '@sanity/cli-core'\nimport {subdebug} from '@sanity/cli-core/debug'\nimport {SanityCommand} from '@sanity/cli-core/SanityCommand'\nimport {spinner} from '@sanity/cli-core/ux'\nimport {Table} from 'console-table-printer'\nimport {formatDistance} from 'date-fns/formatDistance'\nimport {formatDistanceToNow} from 'date-fns/formatDistanceToNow'\nimport {parseISO} from 'date-fns/parseISO'\n\nimport {validateDatasetName} from '../../actions/dataset/validateDatasetName.js'\nimport {promptForDataset} from '../../prompts/promptForDataset.js'\nimport {promptForDatasetName} from '../../prompts/promptForDatasetName.js'\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {\n copyDataset,\n type CopyJobProgressEvent,\n type DatasetCopyJob,\n followCopyJobProgress,\n listDatasetCopyJobs,\n listDatasets,\n} from '../../services/datasets.js'\nimport {formatCliErrorMessages} from '../../util/formatCliErrorMessages.js'\nimport {getProjectIdFlag} from '../../util/sharedFlags.js'\n\nconst copyDatasetDebug = subdebug('dataset:copy')\n\nexport class CopyDatasetCommand extends SanityCommand<typeof CopyDatasetCommand> {\n static override args = {\n source: Args.string({\n description: 'Name of the dataset to copy from',\n required: false,\n }),\n target: Args.string({\n description: 'Name of the dataset to copy to',\n required: false,\n }),\n }\n\n static override description = 'Copy a dataset or manage copy jobs'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Interactively copy a dataset',\n },\n {\n command: '<%= config.bin %> <%= command.id %> source-dataset',\n description: 'Copy from source-dataset (prompts for target)',\n },\n {\n command: '<%= config.bin %> <%= command.id %> source-dataset target-dataset',\n description: 'Copy from source-dataset to target-dataset',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --skip-history source target',\n description: 'Copy without preserving document history (faster for large datasets)',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --skip-content-releases source target',\n description: 'Copy without content release documents',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --detach source target',\n description: 'Start copy job without waiting for completion',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --attach <job-id>',\n description: 'Attach to a running copy job to follow progress',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --list',\n description: 'List all dataset copy jobs',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --list --offset 2 --limit 10',\n description: 'List copy jobs with pagination',\n },\n ]\n\n static override flags = {\n ...getProjectIdFlag({\n description: 'Project ID to copy dataset in',\n semantics: 'override',\n }),\n attach: Flags.string({\n description: 'Attach to the running copy process to show progress',\n exclusive: ['list', 'detach', 'skip-history'],\n required: false,\n }),\n detach: Flags.boolean({\n description: 'Start the copy without waiting for it to finish',\n exclusive: ['list', 'attach'],\n required: false,\n }),\n limit: Flags.integer({\n dependsOn: ['list'],\n description: 'Maximum number of jobs returned (default 10, max 1000)',\n max: 1000,\n required: false,\n }),\n list: Flags.boolean({\n description: 'Lists all dataset copy jobs',\n exclusive: ['attach', 'detach', 'skip-history'],\n required: false,\n }),\n offset: Flags.integer({\n dependsOn: ['list'],\n description: 'Start position in the list of jobs (default 0)',\n required: false,\n }),\n 'skip-content-releases': Flags.boolean({\n description: \"Don't copy content release documents to the target dataset\",\n exclusive: ['list', 'attach'],\n required: false,\n }),\n 'skip-history': Flags.boolean({\n description: \"Don't preserve document history on copy\",\n exclusive: ['list', 'attach'],\n required: false,\n }),\n }\n\n static override hiddenAliases: string[] = ['dataset:copy']\n\n public async run(): Promise<void> {\n const {args, flags} = await this.parse(CopyDatasetCommand)\n\n if (!flags.list && !flags.attach && this.isUnattended()) {\n const errors: string[] = []\n\n if (!args.source) {\n errors.push('Source dataset is required. Pass it as the `<source>` argument.')\n }\n if (!args.target) {\n errors.push('Target dataset is required. Pass it as the `<target>` argument.')\n }\n\n if (errors.length > 0) {\n this.output.error(formatCliErrorMessages(errors), {exit: exitCodes.USAGE_ERROR})\n }\n }\n\n const projectId = await this.getProjectId({\n fallback: () =>\n promptForProject({\n requiredPermissions: [\n {grant: 'read', permission: 'sanity.project.datasets'},\n {grant: 'create', permission: 'sanity.project.datasets'},\n ],\n }),\n })\n\n // Route to appropriate mode\n if (flags.list) {\n return this.handleListMode(projectId, flags)\n }\n\n if (flags.attach) {\n return this.handleAttachMode(projectId, flags.attach)\n }\n\n return this.handleCopyMode(projectId, args, flags)\n }\n\n private displayCopyJobsTable(jobs: DatasetCopyJob[]): void {\n const table = new Table({\n columns: [\n {alignment: 'left', name: 'id', title: 'Job ID'},\n {alignment: 'left', name: 'sourceDataset', title: 'Source Dataset'},\n {alignment: 'left', name: 'targetDataset', title: 'Target Dataset'},\n {alignment: 'left', name: 'state', title: 'State'},\n {alignment: 'left', name: 'withHistory', title: 'With history'},\n {alignment: 'left', name: 'timeStarted', title: 'Time started'},\n {alignment: 'left', name: 'timeTaken', title: 'Time taken'},\n ],\n title: 'Dataset copy jobs for this project in descending order',\n })\n\n for (const job of jobs) {\n const {createdAt, id, sourceDataset, state, targetDataset, updatedAt, withHistory} = job\n\n let timeStarted = ''\n if (createdAt !== '') {\n timeStarted = formatDistanceToNow(parseISO(createdAt))\n }\n\n let timeTaken = ''\n if (updatedAt !== '') {\n timeTaken = formatDistance(parseISO(updatedAt), parseISO(createdAt))\n }\n\n let color: '' | 'green' | 'red' | 'yellow'\n switch (state) {\n case 'completed': {\n color = 'green'\n break\n }\n case 'failed': {\n color = 'red'\n break\n }\n case 'pending': {\n color = 'yellow'\n break\n }\n default: {\n color = ''\n }\n }\n\n table.addRow(\n {\n id,\n sourceDataset,\n state,\n targetDataset,\n timeStarted: `${timeStarted} ago`,\n timeTaken,\n withHistory,\n },\n {color},\n )\n }\n\n this.output.log(table.render())\n }\n\n private async handleAttachMode(projectId: string, jobId: string): Promise<void> {\n copyDatasetDebug('Attaching to copy job %s', jobId)\n\n if (jobId.trim() === '') {\n return this.output.error('Please supply a valid jobId', {exit: exitCodes.RUNTIME_ERROR})\n }\n\n try {\n await this.subscribeToProgress(projectId, jobId)\n this.output.log(`Job ${styleText('green', jobId)} completed`)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n copyDatasetDebug('Failed to attach to copy job: %s', message, error)\n return this.output.error(`Failed to attach to copy job: ${message}`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n }\n\n private async handleCopyMode(\n projectId: string,\n args: {source?: string; target?: string},\n flags: {detach?: boolean; 'skip-content-releases'?: boolean; 'skip-history'?: boolean},\n ): Promise<void> {\n copyDatasetDebug('Starting copy mode')\n\n const skipHistory = Boolean(flags['skip-history'])\n const skipContentReleases = Boolean(flags['skip-content-releases'])\n\n // Get and validate source dataset\n let sourceDataset = args.source\n if (sourceDataset) {\n const nameError = validateDatasetName(sourceDataset)\n if (nameError) {\n return this.output.error(nameError, {exit: exitCodes.USAGE_ERROR})\n }\n }\n\n let datasetsResponse\n try {\n datasetsResponse = await listDatasets(projectId)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n copyDatasetDebug('Failed to fetch datasets: %s', message, error)\n return this.output.error(`Failed to fetch datasets: ${message}`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n const datasetNames = new Set(datasetsResponse.map((ds) => ds.name))\n\n // Prompt for source if not provided\n if (!sourceDataset) {\n sourceDataset = await promptForDataset({\n datasets: datasetsResponse,\n })\n }\n\n if (!datasetNames.has(sourceDataset)) {\n return this.output.error(`Source dataset \"${sourceDataset}\" doesn't exist`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n // Get and validate target dataset\n let targetDataset = args.target\n if (targetDataset) {\n const nameError = validateDatasetName(targetDataset)\n if (nameError) {\n return this.output.error(nameError, {exit: exitCodes.USAGE_ERROR})\n }\n } else {\n targetDataset = await promptForDatasetName({\n message: 'Target dataset name:',\n })\n }\n\n if (datasetNames.has(targetDataset)) {\n return this.output.error(`Target dataset \"${targetDataset}\" already exists`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n // Start the copy job\n try {\n this.output.log(\n `Copying dataset ${styleText('green', sourceDataset)} to ${styleText('green', targetDataset)}...`,\n )\n\n if (!skipHistory) {\n this.output.log(\n `Note: You can run this command with flag '--skip-history'. The flag will reduce copy time in larger datasets.`,\n )\n }\n\n const response = await copyDataset({\n projectId,\n skipContentReleases,\n skipHistory,\n sourceDataset,\n targetDataset,\n })\n\n this.output.log(`Job ${styleText('green', response.jobId)} started`)\n\n if (flags.detach) {\n return\n }\n\n await this.subscribeToProgress(projectId, response.jobId)\n this.output.log(`Job ${styleText('green', response.jobId)} completed`)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n copyDatasetDebug('Dataset copying failed: %s', message, error)\n return this.output.error(`Dataset copying failed: ${message}`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n }\n\n private async handleListMode(\n projectId: string,\n flags: {limit?: number; offset?: number},\n ): Promise<void> {\n copyDatasetDebug('Listing dataset copy jobs')\n\n try {\n const jobs = await listDatasetCopyJobs({\n limit: flags.limit,\n offset: flags.offset,\n projectId,\n })\n\n if (jobs.length === 0) {\n this.output.log(\"This project doesn't have any dataset copy jobs\")\n return\n }\n\n this.displayCopyJobsTable(jobs)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n copyDatasetDebug('Failed to list dataset copy jobs: %s', message, error)\n return this.output.error(`Failed to list dataset copy jobs: ${message}`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n }\n\n private async subscribeToProgress(projectId: string, jobId: string): Promise<void> {\n const spin = spinner('').start()\n\n return new Promise<void>((resolve, reject) => {\n const sigintHandler = () => {\n subscription.unsubscribe()\n spin.fail('Copy interrupted.')\n exit(130)\n }\n\n const subscription = followCopyJobProgress({jobId, projectId}).subscribe({\n complete: () => {\n process.off('SIGINT', sigintHandler)\n spin.succeed('Copy finished.')\n resolve()\n },\n error: (err) => {\n process.off('SIGINT', sigintHandler)\n spin.fail('Copy failed.')\n reject(err)\n },\n next: (event: CopyJobProgressEvent) => {\n if (typeof event.progress === 'number') {\n spin.text = `Copy in progress: ${event.progress}%`\n }\n },\n })\n\n process.once('SIGINT', sigintHandler)\n })\n }\n}\n"],"names":["styleText","Args","Flags","exit","exitCodes","subdebug","SanityCommand","spinner","Table","formatDistance","formatDistanceToNow","parseISO","validateDatasetName","promptForDataset","promptForDatasetName","promptForProject","copyDataset","followCopyJobProgress","listDatasetCopyJobs","listDatasets","formatCliErrorMessages","getProjectIdFlag","copyDatasetDebug","CopyDatasetCommand","args","source","string","description","required","target","examples","command","flags","semantics","attach","exclusive","detach","boolean","limit","integer","dependsOn","max","list","offset","hiddenAliases","run","parse","isUnattended","errors","push","length","output","error","USAGE_ERROR","projectId","getProjectId","fallback","requiredPermissions","grant","permission","handleListMode","handleAttachMode","handleCopyMode","displayCopyJobsTable","jobs","table","columns","alignment","name","title","job","createdAt","id","sourceDataset","state","targetDataset","updatedAt","withHistory","timeStarted","timeTaken","color","addRow","log","render","jobId","trim","RUNTIME_ERROR","subscribeToProgress","message","Error","String","skipHistory","Boolean","skipContentReleases","nameError","datasetsResponse","datasetNames","Set","map","ds","datasets","has","response","spin","start","Promise","resolve","reject","sigintHandler","subscription","unsubscribe","fail","subscribe","complete","process","off","succeed","err","next","event","progress","text","once"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../src/commands/datasets/copy.ts"],"sourcesContent":["import {styleText} from 'node:util'\n\nimport {Args, Flags} from '@oclif/core'\nimport {exit} from '@oclif/core/errors'\nimport {exitCodes} from '@sanity/cli-core'\nimport {subdebug} from '@sanity/cli-core/debug'\nimport {SanityCommand} from '@sanity/cli-core/SanityCommand'\nimport {spinner} from '@sanity/cli-core/ux'\nimport {Table} from 'console-table-printer'\nimport {formatDistance} from 'date-fns/formatDistance'\nimport {formatDistanceToNow} from 'date-fns/formatDistanceToNow'\nimport {parseISO} from 'date-fns/parseISO'\n\nimport {validateDatasetName} from '../../actions/dataset/validateDatasetName.js'\nimport {promptForDataset} from '../../prompts/promptForDataset.js'\nimport {promptForDatasetName} from '../../prompts/promptForDatasetName.js'\nimport {promptForProject} from '../../prompts/promptForProject.js'\nimport {\n copyDataset,\n type CopyJobProgressEvent,\n type DatasetCopyJob,\n followCopyJobProgress,\n listDatasetCopyJobs,\n listDatasets,\n} from '../../services/datasets.js'\nimport {formatCliErrorMessages} from '../../util/formatCliErrorMessages.js'\nimport {getProjectIdFlag} from '../../util/sharedFlags.js'\n\nconst copyDatasetDebug = subdebug('dataset:copy')\n\nexport class CopyDatasetCommand extends SanityCommand<typeof CopyDatasetCommand> {\n static override args = {\n source: Args.string({\n description: 'Name of the dataset to copy from',\n required: false,\n }),\n target: Args.string({\n description: 'Name of the dataset to copy to',\n required: false,\n }),\n }\n\n static override description = 'Copy a dataset or manage copy jobs'\n\n static override examples = [\n {\n command: '<%= config.bin %> <%= command.id %>',\n description: 'Interactively copy a dataset',\n },\n {\n command: '<%= config.bin %> <%= command.id %> source-dataset',\n description: 'Copy from source-dataset (prompts for target)',\n },\n {\n command: '<%= config.bin %> <%= command.id %> source-dataset target-dataset',\n description: 'Copy from source-dataset to target-dataset',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --skip-history source target',\n description: 'Copy without preserving document history (faster for large datasets)',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --skip-content-releases source target',\n description: 'Copy without content release documents',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --detach source target',\n description: 'Start copy job without waiting for completion',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --attach <job-id>',\n description: 'Attach to a running copy job to follow progress',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --list',\n description: 'List all dataset copy jobs',\n },\n {\n command: '<%= config.bin %> <%= command.id %> --list --offset 2 --limit 10',\n description: 'List copy jobs with pagination',\n },\n ]\n\n static override flags = {\n ...getProjectIdFlag({\n description: 'Project ID to copy dataset in',\n semantics: 'override',\n }),\n attach: Flags.string({\n description: 'Attach to the running copy process to show progress',\n exclusive: ['list', 'detach', 'skip-history'],\n required: false,\n }),\n detach: Flags.boolean({\n description: 'Start the copy without waiting for it to finish',\n exclusive: ['list', 'attach'],\n required: false,\n }),\n limit: Flags.integer({\n dependsOn: ['list'],\n description: 'Maximum number of jobs returned (default 10, max 1000)',\n max: 1000,\n required: false,\n }),\n list: Flags.boolean({\n description: 'Lists all dataset copy jobs',\n exclusive: ['attach', 'detach', 'skip-history'],\n required: false,\n }),\n offset: Flags.integer({\n dependsOn: ['list'],\n description: 'Start position in the list of jobs (default 0)',\n required: false,\n }),\n 'skip-content-releases': Flags.boolean({\n description: \"Don't copy content release documents to the target dataset\",\n exclusive: ['list', 'attach'],\n required: false,\n }),\n 'skip-history': Flags.boolean({\n description: \"Don't preserve document history on copy\",\n exclusive: ['list', 'attach'],\n required: false,\n }),\n }\n\n static override hiddenAliases: string[] = ['dataset:copy']\n\n public async run(): Promise<void> {\n const {args, flags} = await this.parse(CopyDatasetCommand)\n\n if (!flags.list && !flags.attach && this.isUnattended()) {\n const errors: string[] = []\n\n if (!args.source) {\n errors.push('Source dataset is required. Pass it as the `<source>` argument.')\n }\n if (!args.target) {\n errors.push('Target dataset is required. Pass it as the `<target>` argument.')\n }\n\n if (errors.length > 0) {\n this.output.error(formatCliErrorMessages(errors), {exit: exitCodes.USAGE_ERROR})\n }\n }\n\n const projectId = await this.getProjectId({\n fallback: () =>\n promptForProject({\n requiredPermissions: [\n {grant: 'read', permission: 'sanity.project.datasets'},\n {grant: 'create', permission: 'sanity.project.datasets'},\n ],\n }),\n })\n\n // Route to appropriate mode\n if (flags.list) {\n return this.handleListMode(projectId, flags)\n }\n\n if (flags.attach) {\n return this.handleAttachMode(projectId, flags.attach)\n }\n\n return this.handleCopyMode(projectId, args, flags)\n }\n\n private displayCopyJobsTable(jobs: DatasetCopyJob[]): void {\n const table = new Table({\n columns: [\n {alignment: 'left', name: 'id', title: 'Job ID'},\n {alignment: 'left', name: 'sourceDataset', title: 'Source Dataset'},\n {alignment: 'left', name: 'targetDataset', title: 'Target Dataset'},\n {alignment: 'left', name: 'state', title: 'State'},\n {alignment: 'left', name: 'withHistory', title: 'With history'},\n {alignment: 'left', name: 'timeStarted', title: 'Time started'},\n {alignment: 'left', name: 'timeTaken', title: 'Time taken'},\n ],\n title: 'Dataset copy jobs for this project in descending order',\n })\n\n for (const job of jobs) {\n const {createdAt, id, sourceDataset, state, targetDataset, updatedAt, withHistory} = job\n\n let timeStarted = ''\n if (createdAt !== '') {\n timeStarted = formatDistanceToNow(parseISO(createdAt))\n }\n\n let timeTaken = ''\n if (updatedAt !== '') {\n timeTaken = formatDistance(parseISO(updatedAt), parseISO(createdAt))\n }\n\n let color: '' | 'green' | 'red' | 'yellow'\n switch (state) {\n case 'completed': {\n color = 'green'\n break\n }\n case 'failed': {\n color = 'red'\n break\n }\n case 'pending': {\n color = 'yellow'\n break\n }\n default: {\n color = ''\n }\n }\n\n table.addRow(\n {\n id,\n sourceDataset,\n state,\n targetDataset,\n timeStarted: `${timeStarted} ago`,\n timeTaken,\n withHistory,\n },\n {color},\n )\n }\n\n this.output.log(table.render())\n }\n\n private async handleAttachMode(projectId: string, jobId: string): Promise<void> {\n copyDatasetDebug('Attaching to copy job %s', jobId)\n\n if (jobId.trim() === '') {\n return this.output.error('Please supply a valid jobId', {exit: exitCodes.RUNTIME_ERROR})\n }\n\n try {\n await this.subscribeToProgress(projectId, jobId)\n this.output.log(`Job ${styleText('green', jobId)} completed`)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n copyDatasetDebug('Failed to attach to copy job: %s', message, error)\n return this.output.error(`Failed to attach to copy job: ${message}`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n }\n\n private async handleCopyMode(\n projectId: string,\n args: {source?: string; target?: string},\n flags: {detach?: boolean; 'skip-content-releases'?: boolean; 'skip-history'?: boolean},\n ): Promise<void> {\n copyDatasetDebug('Starting copy mode')\n\n const skipHistory = Boolean(flags['skip-history'])\n const skipContentReleases = Boolean(flags['skip-content-releases'])\n\n // Surfaced before any prompting so the flag is still actionable: a copy job\n // can't be canceled once started, so mentioning it after the job kicks off\n // leaves nothing to decide.\n if (!skipHistory) {\n this.output.log(\n `Note: You can run this command with flag '--skip-history'. The flag will reduce copy time in larger datasets.`,\n )\n }\n\n // Get and validate source dataset\n let sourceDataset = args.source\n if (sourceDataset) {\n const nameError = validateDatasetName(sourceDataset)\n if (nameError) {\n return this.output.error(nameError, {exit: exitCodes.USAGE_ERROR})\n }\n }\n\n let datasetsResponse\n try {\n datasetsResponse = await listDatasets(projectId)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n copyDatasetDebug('Failed to fetch datasets: %s', message, error)\n return this.output.error(`Failed to fetch datasets: ${message}`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n const datasetNames = new Set(datasetsResponse.map((ds) => ds.name))\n\n // Prompt for source if not provided\n if (!sourceDataset) {\n sourceDataset = await promptForDataset({\n datasets: datasetsResponse,\n })\n }\n\n if (!datasetNames.has(sourceDataset)) {\n return this.output.error(`Source dataset \"${sourceDataset}\" doesn't exist`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n // Get and validate target dataset\n let targetDataset = args.target\n if (targetDataset) {\n const nameError = validateDatasetName(targetDataset)\n if (nameError) {\n return this.output.error(nameError, {exit: exitCodes.USAGE_ERROR})\n }\n } else {\n targetDataset = await promptForDatasetName({\n message: 'Target dataset name:',\n })\n }\n\n if (datasetNames.has(targetDataset)) {\n return this.output.error(`Target dataset \"${targetDataset}\" already exists`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n\n // Start the copy job\n try {\n this.output.log(\n `Copying dataset ${styleText('green', sourceDataset)} to ${styleText('green', targetDataset)}...`,\n )\n\n const response = await copyDataset({\n projectId,\n skipContentReleases,\n skipHistory,\n sourceDataset,\n targetDataset,\n })\n\n this.output.log(`Job ${styleText('green', response.jobId)} started`)\n\n if (flags.detach) {\n return\n }\n\n await this.subscribeToProgress(projectId, response.jobId)\n this.output.log(`Job ${styleText('green', response.jobId)} completed`)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n copyDatasetDebug('Dataset copying failed: %s', message, error)\n return this.output.error(`Dataset copying failed: ${message}`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n }\n\n private async handleListMode(\n projectId: string,\n flags: {limit?: number; offset?: number},\n ): Promise<void> {\n copyDatasetDebug('Listing dataset copy jobs')\n\n try {\n const jobs = await listDatasetCopyJobs({\n limit: flags.limit,\n offset: flags.offset,\n projectId,\n })\n\n if (jobs.length === 0) {\n this.output.log(\"This project doesn't have any dataset copy jobs\")\n return\n }\n\n this.displayCopyJobsTable(jobs)\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n copyDatasetDebug('Failed to list dataset copy jobs: %s', message, error)\n return this.output.error(`Failed to list dataset copy jobs: ${message}`, {\n exit: exitCodes.RUNTIME_ERROR,\n })\n }\n }\n\n private async subscribeToProgress(projectId: string, jobId: string): Promise<void> {\n const spin = spinner('').start()\n\n return new Promise<void>((resolve, reject) => {\n const sigintHandler = () => {\n subscription.unsubscribe()\n spin.fail('Copy interrupted.')\n exit(130)\n }\n\n const subscription = followCopyJobProgress({jobId, projectId}).subscribe({\n complete: () => {\n process.off('SIGINT', sigintHandler)\n spin.succeed('Copy finished.')\n resolve()\n },\n error: (err) => {\n process.off('SIGINT', sigintHandler)\n spin.fail('Copy failed.')\n reject(err)\n },\n next: (event: CopyJobProgressEvent) => {\n if (typeof event.progress === 'number') {\n spin.text = `Copy in progress: ${event.progress}%`\n }\n },\n })\n\n process.once('SIGINT', sigintHandler)\n })\n }\n}\n"],"names":["styleText","Args","Flags","exit","exitCodes","subdebug","SanityCommand","spinner","Table","formatDistance","formatDistanceToNow","parseISO","validateDatasetName","promptForDataset","promptForDatasetName","promptForProject","copyDataset","followCopyJobProgress","listDatasetCopyJobs","listDatasets","formatCliErrorMessages","getProjectIdFlag","copyDatasetDebug","CopyDatasetCommand","args","source","string","description","required","target","examples","command","flags","semantics","attach","exclusive","detach","boolean","limit","integer","dependsOn","max","list","offset","hiddenAliases","run","parse","isUnattended","errors","push","length","output","error","USAGE_ERROR","projectId","getProjectId","fallback","requiredPermissions","grant","permission","handleListMode","handleAttachMode","handleCopyMode","displayCopyJobsTable","jobs","table","columns","alignment","name","title","job","createdAt","id","sourceDataset","state","targetDataset","updatedAt","withHistory","timeStarted","timeTaken","color","addRow","log","render","jobId","trim","RUNTIME_ERROR","subscribeToProgress","message","Error","String","skipHistory","Boolean","skipContentReleases","nameError","datasetsResponse","datasetNames","Set","map","ds","datasets","has","response","spin","start","Promise","resolve","reject","sigintHandler","subscription","unsubscribe","fail","subscribe","complete","process","off","succeed","err","next","event","progress","text","once"],"mappings":"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"}
|
package/dist/commands/debug.js
CHANGED
|
@@ -198,7 +198,7 @@ export class Debug extends SanityCommand {
|
|
|
198
198
|
this.log(formatKeyValue('Name', user.name, {
|
|
199
199
|
padTo
|
|
200
200
|
}));
|
|
201
|
-
this.log(formatKeyValue('Email', user.email, {
|
|
201
|
+
this.log(formatKeyValue('Email', user.email ?? '(none)', {
|
|
202
202
|
padTo
|
|
203
203
|
}));
|
|
204
204
|
this.log(formatKeyValue('ID', user.id, {
|