@sanity/cli 8.7.0 → 8.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +396 -0
  2. package/dist/actions/api/distillApiRoutes.js +1 -1
  3. package/dist/actions/api/distillApiRoutes.js.map +1 -1
  4. package/dist/actions/api/resolveEndpoint.js +1 -1
  5. package/dist/actions/api/resolveEndpoint.js.map +1 -1
  6. package/dist/actions/backup/archiveDir.js +7 -3
  7. package/dist/actions/backup/archiveDir.js.map +1 -1
  8. package/dist/actions/context/resolveOrganizationId.js +17 -0
  9. package/dist/actions/context/resolveOrganizationId.js.map +1 -0
  10. package/dist/actions/context/waitForJob.js +24 -0
  11. package/dist/actions/context/waitForJob.js.map +1 -0
  12. package/dist/actions/context/watchJob.js +32 -0
  13. package/dist/actions/context/watchJob.js.map +1 -0
  14. package/dist/actions/deploy/deployApp.js +6 -6
  15. package/dist/actions/deploy/deployApp.js.map +1 -1
  16. package/dist/actions/deploy/deployStudio.js +6 -6
  17. package/dist/actions/deploy/deployStudio.js.map +1 -1
  18. package/dist/actions/exec/execScript.js +6 -18
  19. package/dist/actions/exec/execScript.js.map +1 -1
  20. package/dist/actions/exec/runScript.worker.js +21 -0
  21. package/dist/actions/exec/runScript.worker.js.map +1 -0
  22. package/dist/actions/init/initAction.js +1 -1
  23. package/dist/actions/init/initAction.js.map +1 -1
  24. package/dist/actions/media/importMedia.js +4 -2
  25. package/dist/actions/media/importMedia.js.map +1 -1
  26. package/dist/commands/backups/download.js +8 -17
  27. package/dist/commands/backups/download.js.map +1 -1
  28. package/dist/commands/context/build.js +111 -0
  29. package/dist/commands/context/build.js.map +1 -0
  30. package/dist/commands/context/create.js +106 -0
  31. package/dist/commands/context/create.js.map +1 -0
  32. package/dist/commands/context/delete.js +68 -0
  33. package/dist/commands/context/delete.js.map +1 -0
  34. package/dist/commands/context/get.js +95 -0
  35. package/dist/commands/context/get.js.map +1 -0
  36. package/dist/commands/context/imports/create.js +270 -0
  37. package/dist/commands/context/imports/create.js.map +1 -0
  38. package/dist/commands/context/imports/delete.js +73 -0
  39. package/dist/commands/context/imports/delete.js.map +1 -0
  40. package/dist/commands/context/imports/download.js +65 -0
  41. package/dist/commands/context/imports/download.js.map +1 -0
  42. package/dist/commands/context/imports/get.js +93 -0
  43. package/dist/commands/context/imports/get.js.map +1 -0
  44. package/dist/commands/context/imports/list.js +105 -0
  45. package/dist/commands/context/imports/list.js.map +1 -0
  46. package/dist/commands/context/jobs/get.js +90 -0
  47. package/dist/commands/context/jobs/get.js.map +1 -0
  48. package/dist/commands/context/list.js +109 -0
  49. package/dist/commands/context/list.js.map +1 -0
  50. package/dist/commands/context/refresh.js +46 -0
  51. package/dist/commands/context/refresh.js.map +1 -0
  52. package/dist/commands/context/update.js +127 -0
  53. package/dist/commands/context/update.js.map +1 -0
  54. package/dist/commands/datasets/alias/delete.js +4 -9
  55. package/dist/commands/datasets/alias/delete.js.map +1 -1
  56. package/dist/commands/datasets/alias/unlink.js +4 -9
  57. package/dist/commands/datasets/alias/unlink.js.map +1 -1
  58. package/dist/commands/datasets/delete.js +4 -9
  59. package/dist/commands/datasets/delete.js.map +1 -1
  60. package/dist/commands/graphql/undeploy.js +4 -9
  61. package/dist/commands/graphql/undeploy.js.map +1 -1
  62. package/dist/commands/init.js +1 -1
  63. package/dist/commands/init.js.map +1 -1
  64. package/dist/commands/media/create-aspect.js +5 -6
  65. package/dist/commands/media/create-aspect.js.map +1 -1
  66. package/dist/commands/media/delete-aspect.js +7 -23
  67. package/dist/commands/media/delete-aspect.js.map +1 -1
  68. package/dist/commands/media/deploy-aspect.js +4 -9
  69. package/dist/commands/media/deploy-aspect.js.map +1 -1
  70. package/dist/commands/media/export.js +3 -7
  71. package/dist/commands/media/export.js.map +1 -1
  72. package/dist/commands/media/import.js +3 -7
  73. package/dist/commands/media/import.js.map +1 -1
  74. package/dist/commands/organizations/create.js +4 -9
  75. package/dist/commands/organizations/create.js.map +1 -1
  76. package/dist/commands/schemas/delete.js +3 -8
  77. package/dist/commands/schemas/delete.js.map +1 -1
  78. package/dist/commands/tokens/delete.js +4 -7
  79. package/dist/commands/tokens/delete.js.map +1 -1
  80. package/dist/commands/users/invite.js +8 -18
  81. package/dist/commands/users/invite.js.map +1 -1
  82. package/dist/exports/index.js +1 -1
  83. package/dist/exports/index.js.map +1 -1
  84. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +18 -0
  85. package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
  86. package/dist/generated/apiRoutes.js +11 -9
  87. package/dist/generated/apiRoutes.js.map +1 -1
  88. package/dist/prompts/promptForOrganization.js +19 -0
  89. package/dist/prompts/promptForOrganization.js.map +1 -0
  90. package/dist/services/context.js +132 -0
  91. package/dist/services/context.js.map +1 -0
  92. package/dist/util/sharedFlags.js +24 -0
  93. package/dist/util/sharedFlags.js.map +1 -1
  94. package/oclif.config.js +1 -0
  95. package/oclif.manifest.json +1931 -1133
  96. package/package.json +16 -22
package/README.md CHANGED
@@ -33,6 +33,19 @@ Code for sanity cli
33
33
  - [`sanity blueprints stacks`](#sanity-blueprints-stacks)
34
34
  - [`sanity build [OUTPUTDIR]`](#sanity-build-outputdir)
35
35
  - [`sanity codemod [CODEMODNAME]`](#sanity-codemod-codemodname)
36
+ - [`sanity context build KNOWLEDGEBASEID`](#sanity-context-build-knowledgebaseid)
37
+ - [`sanity context create`](#sanity-context-create)
38
+ - [`sanity context delete KNOWLEDGEBASEID`](#sanity-context-delete-knowledgebaseid)
39
+ - [`sanity context get KNOWLEDGEBASEID`](#sanity-context-get-knowledgebaseid)
40
+ - [`sanity context imports create KNOWLEDGEBASEID`](#sanity-context-imports-create-knowledgebaseid)
41
+ - [`sanity context imports delete KNOWLEDGEBASEID IMPORTID`](#sanity-context-imports-delete-knowledgebaseid-importid)
42
+ - [`sanity context imports download KNOWLEDGEBASEID IMPORTID`](#sanity-context-imports-download-knowledgebaseid-importid)
43
+ - [`sanity context imports get KNOWLEDGEBASEID IMPORTID`](#sanity-context-imports-get-knowledgebaseid-importid)
44
+ - [`sanity context imports list KNOWLEDGEBASEID`](#sanity-context-imports-list-knowledgebaseid)
45
+ - [`sanity context jobs get KNOWLEDGEBASEID JOBID`](#sanity-context-jobs-get-knowledgebaseid-jobid)
46
+ - [`sanity context list`](#sanity-context-list)
47
+ - [`sanity context refresh KNOWLEDGEBASEID`](#sanity-context-refresh-knowledgebaseid)
48
+ - [`sanity context update KNOWLEDGEBASEID`](#sanity-context-update-knowledgebaseid)
36
49
  - [`sanity cors add ORIGIN`](#sanity-cors-add-origin)
37
50
  - [`sanity cors delete [ORIGIN]`](#sanity-cors-delete-origin)
38
51
  - [`sanity cors list`](#sanity-cors-list)
@@ -950,6 +963,389 @@ EXAMPLES
950
963
  $ sanity codemod reactIconsV3 --dry
951
964
  ```
952
965
 
966
+ ## `sanity context build KNOWLEDGEBASEID`
967
+
968
+ Build a knowledge base from its imported content
969
+
970
+ ```
971
+ USAGE
972
+ $ sanity context build KNOWLEDGEBASEID [--cancel | --watch]
973
+
974
+ ARGUMENTS
975
+ KNOWLEDGEBASEID Knowledge base ID
976
+
977
+ FLAGS
978
+ --cancel Cancel the running build instead of starting one
979
+ --watch Wait for the build to finish, exiting non-zero if it fails
980
+
981
+ DESCRIPTION
982
+ Build a knowledge base from its imported content
983
+
984
+ EXAMPLES
985
+ Start a build and return the job ID
986
+
987
+ $ sanity context build kb-abc123
988
+
989
+ Start a build and wait for it to finish (non-zero exit on failure)
990
+
991
+ $ sanity context build kb-abc123 --watch
992
+
993
+ Cancel the running build, if any
994
+
995
+ $ sanity context build kb-abc123 --cancel
996
+ ```
997
+
998
+ ## `sanity context create`
999
+
1000
+ Create a knowledge base
1001
+
1002
+ ```
1003
+ USAGE
1004
+ $ sanity context create [--organization <id>] [--description <value>] [--title <value>]
1005
+
1006
+ FLAGS
1007
+ --description=<value> Knowledge base description
1008
+ --title=<value> Knowledge base title
1009
+
1010
+ OVERRIDE FLAGS
1011
+ --organization=<id> Organization to create the knowledge base in (overrides CLI configuration)
1012
+
1013
+ DESCRIPTION
1014
+ Create a knowledge base
1015
+
1016
+ EXAMPLES
1017
+ Interactively create a knowledge base
1018
+
1019
+ $ sanity context create
1020
+
1021
+ Create a knowledge base in a specific organization
1022
+
1023
+ $ sanity context create --organization org-abc123 --title "Support docs" --description "Product docs and \
1024
+ troubleshooting guides"
1025
+ ```
1026
+
1027
+ ## `sanity context delete KNOWLEDGEBASEID`
1028
+
1029
+ Delete a knowledge base and its generated content
1030
+
1031
+ ```
1032
+ USAGE
1033
+ $ sanity context delete KNOWLEDGEBASEID [-y]
1034
+
1035
+ ARGUMENTS
1036
+ KNOWLEDGEBASEID Knowledge base ID
1037
+
1038
+ FLAGS
1039
+ -y, --yes Skip confirmation prompt (unattended mode)
1040
+
1041
+ DESCRIPTION
1042
+ Delete a knowledge base and its generated content
1043
+
1044
+ EXAMPLES
1045
+ Delete a knowledge base after confirmation
1046
+
1047
+ $ sanity context delete kb-abc123
1048
+
1049
+ Delete a knowledge base without confirmation prompt
1050
+
1051
+ $ sanity context delete kb-abc123 --yes
1052
+ ```
1053
+
1054
+ ## `sanity context get KNOWLEDGEBASEID`
1055
+
1056
+ Get details of a knowledge base
1057
+
1058
+ ```
1059
+ USAGE
1060
+ $ sanity context get KNOWLEDGEBASEID [--json]
1061
+
1062
+ ARGUMENTS
1063
+ KNOWLEDGEBASEID Knowledge base ID
1064
+
1065
+ FLAGS
1066
+ --json Output the knowledge base in JSON format
1067
+
1068
+ DESCRIPTION
1069
+ Get details of a knowledge base
1070
+
1071
+ EXAMPLES
1072
+ Get details of a specific knowledge base
1073
+
1074
+ $ sanity context get kb-abc123
1075
+
1076
+ Output the knowledge base as JSON
1077
+
1078
+ $ sanity context get kb-abc123 --json
1079
+ ```
1080
+
1081
+ ## `sanity context imports create KNOWLEDGEBASEID`
1082
+
1083
+ Import content into a knowledge base
1084
+
1085
+ ```
1086
+ USAGE
1087
+ $ sanity context imports create KNOWLEDGEBASEID [--content-type <mime>] [--file <path> | --text <value> | --url <url> |
1088
+ --query <value>] [--sanity-dataset <name> ] [--sanity-project <id> ] [--title <value> ]
1089
+
1090
+ ARGUMENTS
1091
+ KNOWLEDGEBASEID Knowledge base ID
1092
+
1093
+ FLAGS
1094
+ --content-type=<mime> Content type of the import (--text: text/markdown or text/plain; --file: any MIME type,
1095
+ inferred from the file extension when omitted)
1096
+ --file=<path> Path to a local file to import
1097
+ --query=<value> GROQ query binding a Sanity dataset as a source
1098
+ --sanity-dataset=<name> Sanity dataset for a dataset import
1099
+ --sanity-project=<id> Sanity project ID for a dataset import
1100
+ --text=<value> Inline text content to import (requires --title)
1101
+ --title=<value> Title for an inline text import
1102
+ --url=<url> Website URL to crawl
1103
+
1104
+ DESCRIPTION
1105
+ Import content into a knowledge base
1106
+
1107
+ EXAMPLES
1108
+ Import inline text
1109
+
1110
+ $ sanity context imports create kb-abc123 --text "Refunds are processed within 5 days" --title "Refund policy"
1111
+
1112
+ Upload and import a local file
1113
+
1114
+ $ sanity context imports create kb-abc123 --file ./handbook.pdf
1115
+
1116
+ Crawl a website
1117
+
1118
+ $ sanity context imports create kb-abc123 --url https://example.com/docs
1119
+
1120
+ Bind a Sanity dataset as a source
1121
+
1122
+ $ sanity context imports create kb-abc123 --query '*[_type == "article"]' --sanity-project abc123 \
1123
+ --sanity-dataset production
1124
+ ```
1125
+
1126
+ ## `sanity context imports delete KNOWLEDGEBASEID IMPORTID`
1127
+
1128
+ Delete an import from a knowledge base
1129
+
1130
+ ```
1131
+ USAGE
1132
+ $ sanity context imports delete KNOWLEDGEBASEID IMPORTID [-y]
1133
+
1134
+ ARGUMENTS
1135
+ KNOWLEDGEBASEID Knowledge base ID
1136
+ IMPORTID Import ID
1137
+
1138
+ FLAGS
1139
+ -y, --yes Skip confirmation prompt (unattended mode)
1140
+
1141
+ DESCRIPTION
1142
+ Delete an import from a knowledge base
1143
+
1144
+ EXAMPLES
1145
+ Delete an import after confirmation
1146
+
1147
+ $ sanity context imports delete kb-abc123 import-def456
1148
+
1149
+ Delete an import without confirmation prompt
1150
+
1151
+ $ sanity context imports delete kb-abc123 import-def456 --yes
1152
+ ```
1153
+
1154
+ ## `sanity context imports download KNOWLEDGEBASEID IMPORTID`
1155
+
1156
+ Get a short-lived signed URL for the original uploaded bytes of a file import
1157
+
1158
+ ```
1159
+ USAGE
1160
+ $ sanity context imports download KNOWLEDGEBASEID IMPORTID [--json]
1161
+
1162
+ ARGUMENTS
1163
+ KNOWLEDGEBASEID Knowledge base ID
1164
+ IMPORTID Import ID
1165
+
1166
+ FLAGS
1167
+ --json Output the download URL in JSON format
1168
+
1169
+ DESCRIPTION
1170
+ Get a short-lived signed URL for the original uploaded bytes of a file import
1171
+
1172
+ EXAMPLES
1173
+ Get a download URL for an uploaded file
1174
+
1175
+ $ sanity context imports download kb-abc123 import-def456
1176
+ ```
1177
+
1178
+ ## `sanity context imports get KNOWLEDGEBASEID IMPORTID`
1179
+
1180
+ Get details of an import
1181
+
1182
+ ```
1183
+ USAGE
1184
+ $ sanity context imports get KNOWLEDGEBASEID IMPORTID [--json]
1185
+
1186
+ ARGUMENTS
1187
+ KNOWLEDGEBASEID Knowledge base ID
1188
+ IMPORTID Import ID
1189
+
1190
+ FLAGS
1191
+ --json Output the import in JSON format
1192
+
1193
+ DESCRIPTION
1194
+ Get details of an import
1195
+
1196
+ EXAMPLES
1197
+ Get details of a specific import
1198
+
1199
+ $ sanity context imports get kb-abc123 import-def456
1200
+
1201
+ Output the import as JSON
1202
+
1203
+ $ sanity context imports get kb-abc123 import-def456 --json
1204
+ ```
1205
+
1206
+ ## `sanity context imports list KNOWLEDGEBASEID`
1207
+
1208
+ List imports for a knowledge base
1209
+
1210
+ ```
1211
+ USAGE
1212
+ $ sanity context imports list KNOWLEDGEBASEID [--json]
1213
+
1214
+ ARGUMENTS
1215
+ KNOWLEDGEBASEID Knowledge base ID
1216
+
1217
+ FLAGS
1218
+ --json Output imports in JSON format
1219
+
1220
+ DESCRIPTION
1221
+ List imports for a knowledge base
1222
+
1223
+ EXAMPLES
1224
+ List all imports for a knowledge base
1225
+
1226
+ $ sanity context imports list kb-abc123
1227
+
1228
+ Output imports as JSON
1229
+
1230
+ $ sanity context imports list kb-abc123 --json
1231
+ ```
1232
+
1233
+ ## `sanity context jobs get KNOWLEDGEBASEID JOBID`
1234
+
1235
+ Get the status of a knowledge base job
1236
+
1237
+ ```
1238
+ USAGE
1239
+ $ sanity context jobs get KNOWLEDGEBASEID JOBID [--json] [--watch]
1240
+
1241
+ ARGUMENTS
1242
+ KNOWLEDGEBASEID Knowledge base ID
1243
+ JOBID Job ID
1244
+
1245
+ FLAGS
1246
+ --json Output the job in JSON format
1247
+ --watch Poll until the job reaches a terminal state
1248
+
1249
+ DESCRIPTION
1250
+ Get the status of a knowledge base job
1251
+
1252
+ EXAMPLES
1253
+ Get the current status of a job
1254
+
1255
+ $ sanity context jobs get kb-abc123 job-def456
1256
+
1257
+ Wait for the job to finish (non-zero exit if it does not succeed)
1258
+
1259
+ $ sanity context jobs get kb-abc123 job-def456 --watch
1260
+ ```
1261
+
1262
+ ## `sanity context list`
1263
+
1264
+ List knowledge bases in an organization
1265
+
1266
+ ```
1267
+ USAGE
1268
+ $ sanity context list [--organization <id>] [--json]
1269
+
1270
+ FLAGS
1271
+ --json Output knowledge bases in JSON format
1272
+
1273
+ OVERRIDE FLAGS
1274
+ --organization=<id> Organization to list knowledge bases for (overrides CLI configuration)
1275
+
1276
+ DESCRIPTION
1277
+ List knowledge bases in an organization
1278
+
1279
+ EXAMPLES
1280
+ List knowledge bases, selecting the organization interactively
1281
+
1282
+ $ sanity context list
1283
+
1284
+ List knowledge bases in a specific organization
1285
+
1286
+ $ sanity context list --organization org-abc123
1287
+
1288
+ Output knowledge bases as JSON
1289
+
1290
+ $ sanity context list --organization org-abc123 --json
1291
+ ```
1292
+
1293
+ ## `sanity context refresh KNOWLEDGEBASEID`
1294
+
1295
+ Refresh a knowledge base: re-check sources and apply what changed
1296
+
1297
+ ```
1298
+ USAGE
1299
+ $ sanity context refresh KNOWLEDGEBASEID
1300
+
1301
+ ARGUMENTS
1302
+ KNOWLEDGEBASEID Knowledge base ID
1303
+
1304
+ DESCRIPTION
1305
+ Refresh a knowledge base: re-check sources and apply what changed
1306
+
1307
+ EXAMPLES
1308
+ Start an incremental refresh
1309
+
1310
+ $ sanity context refresh kb-abc123
1311
+ ```
1312
+
1313
+ ## `sanity context update KNOWLEDGEBASEID`
1314
+
1315
+ Update a knowledge base
1316
+
1317
+ ```
1318
+ USAGE
1319
+ $ sanity context update KNOWLEDGEBASEID [--description <value>] [--refresh-enabled] [--refresh-frequency
1320
+ weekly|monthly] [--title <value>]
1321
+
1322
+ ARGUMENTS
1323
+ KNOWLEDGEBASEID Knowledge base ID
1324
+
1325
+ FLAGS
1326
+ --description=<value> New knowledge base description
1327
+ --[no-]refresh-enabled Enable scheduled refresh (--no-refresh-enabled to disable)
1328
+ --refresh-frequency=<option> How often scheduled refresh runs
1329
+ <options: weekly|monthly>
1330
+ --title=<value> New knowledge base title
1331
+
1332
+ DESCRIPTION
1333
+ Update a knowledge base
1334
+
1335
+ EXAMPLES
1336
+ Rename a knowledge base
1337
+
1338
+ $ sanity context update kb-abc123 --title "New title"
1339
+
1340
+ Enable weekly scheduled refresh
1341
+
1342
+ $ sanity context update kb-abc123 --refresh-enabled --refresh-frequency weekly
1343
+
1344
+ Disable scheduled refresh
1345
+
1346
+ $ sanity context update kb-abc123 --no-refresh-enabled
1347
+ ```
1348
+
953
1349
  ## `sanity cors add ORIGIN`
954
1350
 
955
1351
  Add a CORS origin to the project
@@ -88,7 +88,7 @@ import { API_VERSION_SEGMENT_RE } from './constants.js';
88
88
  const [pathPart = ''] = path.split('?');
89
89
  const segments = pathPart.split('/').filter((segment)=>segment !== '');
90
90
  let version;
91
- if (segments[0] === '{apiVersion}') {
91
+ if (segments[0] === '{apiVersion}' || segments[0] === '{apiV}') {
92
92
  segments.shift();
93
93
  } else if (segments.length > 0 && API_VERSION_SEGMENT_RE.test(segments[0])) {
94
94
  version = segments.shift();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/api/distillApiRoutes.ts"],"sourcesContent":["import {API_VERSION_SEGMENT_RE} from './constants.js'\nimport {type ApiHost, type ApiRouteEntry, type OpenApiDocument} from './types.js'\n\n/**\n * A fetched OpenAPI specification, ready for distillation.\n */\nexport interface SpecSource {\n document: OpenApiDocument\n slug: string\n title: string\n}\n\ninterface ServerBase {\n host: ApiHost\n segments: string[]\n\n defaultApiVersion?: string\n}\n\n/**\n * Distill fetched OpenAPI specifications into the routing manifest used by\n * `sanity api` to decide which host serves a request path and which API\n * version to default to.\n *\n * The output is deterministic (sorted) so the generated manifest can be\n * compared against a fresh distillation to detect drift.\n */\nexport function distillApiRoutes(specs: SpecSource[]): ApiRouteEntry[] {\n const entries: ApiRouteEntry[] = []\n\n for (const {document, slug, title} of specs) {\n const servers = document.servers ?? []\n const paths = Object.keys(document.paths ?? {})\n if (servers.length === 0 || paths.length === 0) continue\n\n // A spec may be served on both hosts (eg the Applications API). Emit one\n // entry per host, each carrying the full set of path patterns.\n const byHost = new Map<ApiHost, ServerBase>()\n for (const server of servers) {\n const base = parseServerUrl(server.url, server.variables)\n if (!base) continue\n if (!byHost.has(base.host)) byHost.set(base.host, base)\n }\n\n for (const base of byHost.values()) {\n const patterns = new Set<string>()\n let defaultApiVersion = base.defaultApiVersion\n\n for (const path of paths) {\n const {segments, version} = normalizePathSegments(path)\n defaultApiVersion = defaultApiVersion ?? version\n const pattern = [...base.segments, ...segments].join('/')\n if (pattern !== '') patterns.add(pattern)\n }\n\n if (patterns.size === 0) continue\n\n entries.push({\n ...(defaultApiVersion === undefined ? {} : {defaultApiVersion}),\n host: base.host,\n pathPatterns: [...patterns].toSorted(),\n slug,\n title,\n })\n }\n }\n\n return entries.toSorted((a, b) => a.slug.localeCompare(b.slug) || a.host.localeCompare(b.host))\n}\n\n/**\n * Parse an OpenAPI server URL template (eg\n * `https://{projectId}.api.sanity.io/{apiVersion}/agent/action`) into a host\n * classification, base path segments and a default API version.\n *\n * Returns `undefined` for URLs that don't look like a Sanity API host.\n */\nfunction parseServerUrl(\n url: string,\n variables?: Record<string, {default?: string}>,\n): ServerBase | undefined {\n const withoutScheme = url.replace(/^https?:\\/\\//, '')\n const [hostname = '', ...pathSegments] = withoutScheme.split('/')\n\n if (!hostname.includes('api.sanity.io') && !hostname.includes('api.sanity.work')) {\n return undefined\n }\n\n const host: ApiHost = hostname.includes('{projectId}') ? 'project' : 'global'\n\n let defaultApiVersion = normalizeVersion(variables?.apiVersion?.default)\n const segments: string[] = []\n for (const segment of pathSegments) {\n if (segment === '') continue\n if (segment === '{apiVersion}') continue\n if (API_VERSION_SEGMENT_RE.test(segment)) {\n defaultApiVersion = defaultApiVersion ?? segment\n continue\n }\n segments.push(segment)\n }\n\n return {defaultApiVersion, host, segments}\n}\n\n/**\n * Normalize an OpenAPI path template into version-less path segments, plus the\n * API version if one was embedded in the path (eg `/v2024-07-01/access/...`).\n *\n * Some specs carry a stray query marker in the path key (eg `/foo/{id}?`) -\n * anything from `?` on is not part of the path and is dropped.\n */\nfunction normalizePathSegments(path: string): {segments: string[]; version?: string} {\n const [pathPart = ''] = path.split('?')\n const segments = pathPart.split('/').filter((segment) => segment !== '')\n let version: string | undefined\n\n if (segments[0] === '{apiVersion}') {\n segments.shift()\n } else if (segments.length > 0 && API_VERSION_SEGMENT_RE.test(segments[0])) {\n version = segments.shift()\n }\n\n return {segments, version}\n}\n\nfunction normalizeVersion(version: string | undefined): string | undefined {\n if (!version) return undefined\n return API_VERSION_SEGMENT_RE.test(version) ? version : undefined\n}\n"],"names":["API_VERSION_SEGMENT_RE","distillApiRoutes","specs","entries","document","slug","title","servers","paths","Object","keys","length","byHost","Map","server","base","parseServerUrl","url","variables","has","host","set","values","patterns","Set","defaultApiVersion","path","segments","version","normalizePathSegments","pattern","join","add","size","push","undefined","pathPatterns","toSorted","a","b","localeCompare","withoutScheme","replace","hostname","pathSegments","split","includes","normalizeVersion","apiVersion","default","segment","test","pathPart","filter","shift"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/actions/api/distillApiRoutes.ts"],"sourcesContent":["import {API_VERSION_SEGMENT_RE} from './constants.js'\nimport {type ApiHost, type ApiRouteEntry, type OpenApiDocument} from './types.js'\n\n/**\n * A fetched OpenAPI specification, ready for distillation.\n */\nexport interface SpecSource {\n document: OpenApiDocument\n slug: string\n title: string\n}\n\ninterface ServerBase {\n host: ApiHost\n segments: string[]\n\n defaultApiVersion?: string\n}\n\n/**\n * Distill fetched OpenAPI specifications into the routing manifest used by\n * `sanity api` to decide which host serves a request path and which API\n * version to default to.\n *\n * The output is deterministic (sorted) so the generated manifest can be\n * compared against a fresh distillation to detect drift.\n */\nexport function distillApiRoutes(specs: SpecSource[]): ApiRouteEntry[] {\n const entries: ApiRouteEntry[] = []\n\n for (const {document, slug, title} of specs) {\n const servers = document.servers ?? []\n const paths = Object.keys(document.paths ?? {})\n if (servers.length === 0 || paths.length === 0) continue\n\n // A spec may be served on both hosts (eg the Applications API). Emit one\n // entry per host, each carrying the full set of path patterns.\n const byHost = new Map<ApiHost, ServerBase>()\n for (const server of servers) {\n const base = parseServerUrl(server.url, server.variables)\n if (!base) continue\n if (!byHost.has(base.host)) byHost.set(base.host, base)\n }\n\n for (const base of byHost.values()) {\n const patterns = new Set<string>()\n let defaultApiVersion = base.defaultApiVersion\n\n for (const path of paths) {\n const {segments, version} = normalizePathSegments(path)\n defaultApiVersion = defaultApiVersion ?? version\n const pattern = [...base.segments, ...segments].join('/')\n if (pattern !== '') patterns.add(pattern)\n }\n\n if (patterns.size === 0) continue\n\n entries.push({\n ...(defaultApiVersion === undefined ? {} : {defaultApiVersion}),\n host: base.host,\n pathPatterns: [...patterns].toSorted(),\n slug,\n title,\n })\n }\n }\n\n return entries.toSorted((a, b) => a.slug.localeCompare(b.slug) || a.host.localeCompare(b.host))\n}\n\n/**\n * Parse an OpenAPI server URL template (eg\n * `https://{projectId}.api.sanity.io/{apiVersion}/agent/action`) into a host\n * classification, base path segments and a default API version.\n *\n * Returns `undefined` for URLs that don't look like a Sanity API host.\n */\nfunction parseServerUrl(\n url: string,\n variables?: Record<string, {default?: string}>,\n): ServerBase | undefined {\n const withoutScheme = url.replace(/^https?:\\/\\//, '')\n const [hostname = '', ...pathSegments] = withoutScheme.split('/')\n\n if (!hostname.includes('api.sanity.io') && !hostname.includes('api.sanity.work')) {\n return undefined\n }\n\n const host: ApiHost = hostname.includes('{projectId}') ? 'project' : 'global'\n\n let defaultApiVersion = normalizeVersion(variables?.apiVersion?.default)\n const segments: string[] = []\n for (const segment of pathSegments) {\n if (segment === '') continue\n if (segment === '{apiVersion}') continue\n if (API_VERSION_SEGMENT_RE.test(segment)) {\n defaultApiVersion = defaultApiVersion ?? segment\n continue\n }\n segments.push(segment)\n }\n\n return {defaultApiVersion, host, segments}\n}\n\n/**\n * Normalize an OpenAPI path template into version-less path segments, plus the\n * API version if one was embedded in the path (eg `/v2024-07-01/access/...`).\n *\n * Some specs carry a stray query marker in the path key (eg `/foo/{id}?`) -\n * anything from `?` on is not part of the path and is dropped.\n */\nfunction normalizePathSegments(path: string): {segments: string[]; version?: string} {\n const [pathPart = ''] = path.split('?')\n const segments = pathPart.split('/').filter((segment) => segment !== '')\n let version: string | undefined\n\n if (segments[0] === '{apiVersion}' || segments[0] === '{apiV}') {\n segments.shift()\n } else if (segments.length > 0 && API_VERSION_SEGMENT_RE.test(segments[0])) {\n version = segments.shift()\n }\n\n return {segments, version}\n}\n\nfunction normalizeVersion(version: string | undefined): string | undefined {\n if (!version) return undefined\n return API_VERSION_SEGMENT_RE.test(version) ? version : undefined\n}\n"],"names":["API_VERSION_SEGMENT_RE","distillApiRoutes","specs","entries","document","slug","title","servers","paths","Object","keys","length","byHost","Map","server","base","parseServerUrl","url","variables","has","host","set","values","patterns","Set","defaultApiVersion","path","segments","version","normalizePathSegments","pattern","join","add","size","push","undefined","pathPatterns","toSorted","a","b","localeCompare","withoutScheme","replace","hostname","pathSegments","split","includes","normalizeVersion","apiVersion","default","segment","test","pathPart","filter","shift"],"mappings":"AAAA,SAAQA,sBAAsB,QAAO,iBAAgB;AAmBrD;;;;;;;CAOC,GACD,OAAO,SAASC,iBAAiBC,KAAmB;IAClD,MAAMC,UAA2B,EAAE;IAEnC,KAAK,MAAM,EAACC,QAAQ,EAAEC,IAAI,EAAEC,KAAK,EAAC,IAAIJ,MAAO;QAC3C,MAAMK,UAAUH,SAASG,OAAO,IAAI,EAAE;QACtC,MAAMC,QAAQC,OAAOC,IAAI,CAACN,SAASI,KAAK,IAAI,CAAC;QAC7C,IAAID,QAAQI,MAAM,KAAK,KAAKH,MAAMG,MAAM,KAAK,GAAG;QAEhD,yEAAyE;QACzE,+DAA+D;QAC/D,MAAMC,SAAS,IAAIC;QACnB,KAAK,MAAMC,UAAUP,QAAS;YAC5B,MAAMQ,OAAOC,eAAeF,OAAOG,GAAG,EAAEH,OAAOI,SAAS;YACxD,IAAI,CAACH,MAAM;YACX,IAAI,CAACH,OAAOO,GAAG,CAACJ,KAAKK,IAAI,GAAGR,OAAOS,GAAG,CAACN,KAAKK,IAAI,EAAEL;QACpD;QAEA,KAAK,MAAMA,QAAQH,OAAOU,MAAM,GAAI;YAClC,MAAMC,WAAW,IAAIC;YACrB,IAAIC,oBAAoBV,KAAKU,iBAAiB;YAE9C,KAAK,MAAMC,QAAQlB,MAAO;gBACxB,MAAM,EAACmB,QAAQ,EAAEC,OAAO,EAAC,GAAGC,sBAAsBH;gBAClDD,oBAAoBA,qBAAqBG;gBACzC,MAAME,UAAU;uBAAIf,KAAKY,QAAQ;uBAAKA;iBAAS,CAACI,IAAI,CAAC;gBACrD,IAAID,YAAY,IAAIP,SAASS,GAAG,CAACF;YACnC;YAEA,IAAIP,SAASU,IAAI,KAAK,GAAG;YAEzB9B,QAAQ+B,IAAI,CAAC;gBACX,GAAIT,sBAAsBU,YAAY,CAAC,IAAI;oBAACV;gBAAiB,CAAC;gBAC9DL,MAAML,KAAKK,IAAI;gBACfgB,cAAc;uBAAIb;iBAAS,CAACc,QAAQ;gBACpChC;gBACAC;YACF;QACF;IACF;IAEA,OAAOH,QAAQkC,QAAQ,CAAC,CAACC,GAAGC,IAAMD,EAAEjC,IAAI,CAACmC,aAAa,CAACD,EAAElC,IAAI,KAAKiC,EAAElB,IAAI,CAACoB,aAAa,CAACD,EAAEnB,IAAI;AAC/F;AAEA;;;;;;CAMC,GACD,SAASJ,eACPC,GAAW,EACXC,SAA8C;IAE9C,MAAMuB,gBAAgBxB,IAAIyB,OAAO,CAAC,gBAAgB;IAClD,MAAM,CAACC,WAAW,EAAE,EAAE,GAAGC,aAAa,GAAGH,cAAcI,KAAK,CAAC;IAE7D,IAAI,CAACF,SAASG,QAAQ,CAAC,oBAAoB,CAACH,SAASG,QAAQ,CAAC,oBAAoB;QAChF,OAAOX;IACT;IAEA,MAAMf,OAAgBuB,SAASG,QAAQ,CAAC,iBAAiB,YAAY;IAErE,IAAIrB,oBAAoBsB,iBAAiB7B,WAAW8B,YAAYC;IAChE,MAAMtB,WAAqB,EAAE;IAC7B,KAAK,MAAMuB,WAAWN,aAAc;QAClC,IAAIM,YAAY,IAAI;QACpB,IAAIA,YAAY,gBAAgB;QAChC,IAAIlD,uBAAuBmD,IAAI,CAACD,UAAU;YACxCzB,oBAAoBA,qBAAqByB;YACzC;QACF;QACAvB,SAASO,IAAI,CAACgB;IAChB;IAEA,OAAO;QAACzB;QAAmBL;QAAMO;IAAQ;AAC3C;AAEA;;;;;;CAMC,GACD,SAASE,sBAAsBH,IAAY;IACzC,MAAM,CAAC0B,WAAW,EAAE,CAAC,GAAG1B,KAAKmB,KAAK,CAAC;IACnC,MAAMlB,WAAWyB,SAASP,KAAK,CAAC,KAAKQ,MAAM,CAAC,CAACH,UAAYA,YAAY;IACrE,IAAItB;IAEJ,IAAID,QAAQ,CAAC,EAAE,KAAK,kBAAkBA,QAAQ,CAAC,EAAE,KAAK,UAAU;QAC9DA,SAAS2B,KAAK;IAChB,OAAO,IAAI3B,SAAShB,MAAM,GAAG,KAAKX,uBAAuBmD,IAAI,CAACxB,QAAQ,CAAC,EAAE,GAAG;QAC1EC,UAAUD,SAAS2B,KAAK;IAC1B;IAEA,OAAO;QAAC3B;QAAUC;IAAO;AAC3B;AAEA,SAASmB,iBAAiBnB,OAA2B;IACnD,IAAI,CAACA,SAAS,OAAOO;IACrB,OAAOnC,uBAAuBmD,IAAI,CAACvB,WAAWA,UAAUO;AAC1D"}
@@ -23,7 +23,7 @@ const ALLOWED_URL_HOST_RE = /(?:^|\.)api\.sanity\.(?:io|work)$/;
23
23
  // An API version embedded in the path (eg `v2021-06-07/projects`) is peeled
24
24
  // off and re-applied as the version segment of the final URL.
25
25
  let embeddedVersion;
26
- if (segments[0] === '{apiVersion}') {
26
+ if (segments[0] === '{apiVersion}' || segments[0] === '{apiV}') {
27
27
  segments.shift();
28
28
  } else if (segments.length > 0 && API_VERSION_SEGMENT_RE.test(segments[0])) {
29
29
  embeddedVersion = segments.shift();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/api/resolveEndpoint.ts"],"sourcesContent":["import {parse} from 'node:querystring'\n\nimport {API_DEFAULT_VERSION, API_VERSION_SEGMENT_RE} from './constants.js'\nimport {ApiUsageError, ProjectIdRequiredError} from './errors.js'\nimport {type ApiHost, type ApiRouteEntry} from './types.js'\n\n/**\n * Options for {@link resolveEndpoint}.\n */\nexport interface ResolveEndpointOptions {\n /** The endpoint argument: a path, optionally with placeholders, or a full URL. */\n endpoint: string\n\n /** The generated routing manifest. */\n routes: ApiRouteEntry[]\n\n /** Explicit API version (`--api-version`), wins over everything else. */\n apiVersion?: string\n\n /** Dataset used to fill `{dataset}` / `{datasetName}` placeholders. */\n dataset?: string\n\n /** Force a host family (`--global` / `--project-hosted`), restricting route matching to it. */\n forceHost?: ApiHost\n\n /** Project ID used to fill `{projectId}` placeholders and project-hosted requests. */\n projectId?: string\n}\n\n/**\n * A fully resolved request target.\n */\nexport type ResolvedEndpoint =\n | {\n /** API version path segment, eg `v2021-06-07`. */\n apiVersion: string\n host: ApiHost\n kind: 'path'\n /** Slug of the OpenAPI spec whose paths matched, if any. */\n matchedSlug?: string\n /** Version-less path, without leading slash. */\n path: string\n /** Project ID; always set when `host` is `project`. */\n projectId?: string\n /** Query parameters parsed from the endpoint argument. */\n query: Record<string, string | string[]>\n }\n | {\n kind: 'url'\n query: Record<string, string | string[]>\n /** Absolute URL, used verbatim (query string stripped into `query`). */\n url: string\n }\n\nconst PLACEHOLDER_RE = /^\\{([^{}]+)\\}$/\n\nconst ALLOWED_URL_HOST_RE = /(?:^|\\.)api\\.sanity\\.(?:io|work)$/\n\n/**\n * Resolve the endpoint argument of `sanity api` into a concrete request\n * target: substitute placeholders, split off the query string, determine the\n * host family (via the generated OpenAPI routing manifest) and the API\n * version.\n *\n * Throws {@link ProjectIdRequiredError} when the request needs a project ID\n * that hasn't been provided - the command catches this to resolve one (flags,\n * CLI config or interactive prompt) and retries.\n */\nexport function resolveEndpoint(options: ResolveEndpointOptions): ResolvedEndpoint {\n const {apiVersion, dataset, endpoint, forceHost, projectId, routes} = options\n\n if (/^https?:\\/\\//.test(endpoint)) {\n return resolveUrlEndpoint(endpoint)\n }\n\n const [pathPart = '', ...queryParts] = endpoint.split('?')\n const query = parseQueryString(queryParts.join('?'))\n\n const segments = pathPart.split('/').filter((segment) => segment !== '')\n\n // An API version embedded in the path (eg `v2021-06-07/projects`) is peeled\n // off and re-applied as the version segment of the final URL.\n let embeddedVersion: string | undefined\n if (segments[0] === '{apiVersion}') {\n segments.shift()\n } else if (segments.length > 0 && API_VERSION_SEGMENT_RE.test(segments[0])) {\n embeddedVersion = segments.shift()\n }\n\n // Checked after version peeling so a bare version (eg `v2025-02-19`) is\n // also rejected as an empty path.\n if (segments.length === 0) {\n throw new ApiUsageError('Endpoint path is empty')\n }\n\n const unresolved: string[] = []\n const substituted = segments.map((segment, index) => {\n const placeholder = PLACEHOLDER_RE.exec(segment)?.[1]\n if (!placeholder) return segment\n if (placeholder === 'projectId') {\n if (!projectId) throw new ProjectIdRequiredError()\n return projectId\n }\n // The projects spec names the dataset placeholder `{name}` (eg\n // `projects/{projectId}/datasets/{name}`) - only that context is a\n // dataset; `{name}` elsewhere (eg video renditions) stays unresolved.\n const isDatasetName = placeholder === 'name' && segments[index - 1] === 'datasets'\n if (placeholder === 'dataset' || placeholder === 'datasetName' || isDatasetName) {\n if (!dataset) {\n throw new ApiUsageError(\n `Unable to resolve {${placeholder}} - provide a dataset with --dataset or configure one in sanity.cli.ts`,\n )\n }\n return dataset\n }\n unresolved.push(segment)\n return segment\n })\n\n if (unresolved.length > 0) {\n throw new ApiUsageError(\n `Unable to resolve placeholder(s) ${unresolved.join(', ')} - replace them with actual values`,\n )\n }\n\n // Forcing a host restricts matching to specs served on that host, so the\n // matched spec's default API version still applies when one exists there.\n const candidates = forceHost ? routes.filter((entry) => entry.host === forceHost) : routes\n const match = matchRoutes(substituted, candidates)\n const host = forceHost ?? match?.host ?? 'global'\n\n if (host === 'project' && !projectId) {\n throw new ProjectIdRequiredError()\n }\n\n return {\n apiVersion: apiVersion ?? embeddedVersion ?? match?.defaultApiVersion ?? API_DEFAULT_VERSION,\n host,\n kind: 'path',\n matchedSlug: match?.slug,\n path: substituted.join('/'),\n ...(host === 'project' ? {projectId} : {}),\n query,\n }\n}\n\nfunction resolveUrlEndpoint(endpoint: string): ResolvedEndpoint {\n let url: URL\n try {\n url = new URL(endpoint)\n } catch {\n throw new ApiUsageError(`Invalid URL \"${endpoint}\"`)\n }\n\n // Never put the token on the wire unencrypted\n if (url.protocol !== 'https:') {\n throw new ApiUsageError(\n `Refusing to send requests over \"${url.protocol}//\" - only https:// URLs are supported`,\n )\n }\n\n if (!ALLOWED_URL_HOST_RE.test(url.hostname)) {\n throw new ApiUsageError(\n `Refusing to send authenticated requests to \"${url.hostname}\" - only *.api.sanity.io hosts are supported`,\n )\n }\n\n const query = parseQueryString(url.search.replace(/^\\?/, ''))\n url.search = ''\n\n return {kind: 'url', query, url: url.toString()}\n}\n\ninterface RouteMatch {\n host: ApiHost\n score: number\n slug: string\n\n defaultApiVersion?: string\n}\n\n/**\n * Find the route entry whose path pattern best matches the request segments.\n *\n * A pattern only matches when the request path consumes it completely, and\n * segments are compared pairwise: literal matches score higher than\n * placeholder matches, and a literal mismatch disqualifies the pattern. On\n * equal scores across hosts the global host wins (APIs served on both hosts\n * don't need a project ID there); on equal scores within the same host the\n * first entry wins, keeping the result independent of later entries.\n */\nfunction matchRoutes(segments: string[], routes: ApiRouteEntry[]): RouteMatch | undefined {\n let best: RouteMatch | undefined\n\n for (const entry of routes) {\n for (const pattern of entry.pathPatterns) {\n const score = scorePattern(segments, pattern.split('/'))\n if (score === 0) continue\n const isBetter =\n !best ||\n score > best.score ||\n (score === best.score && best.host === 'project' && entry.host === 'global')\n if (isBetter) {\n best = {\n defaultApiVersion: entry.defaultApiVersion,\n host: entry.host,\n score,\n slug: entry.slug,\n }\n }\n }\n }\n\n return best\n}\n\nfunction scorePattern(segments: string[], patternSegments: string[]): number {\n // A pattern only matches when the request path consumes it completely - a\n // request that is a mere prefix of a longer pattern (eg `data` vs\n // `data/query/{dataset}`) must not inherit that route's host or version.\n if (patternSegments.length > segments.length) return 0\n\n let score = 0\n let literalMatches = 0\n\n for (const [index, patternSegment] of patternSegments.entries()) {\n if (PLACEHOLDER_RE.test(patternSegment)) {\n score += 1\n } else if (patternSegment === segments[index]) {\n score += 2\n literalMatches += 1\n } else {\n return 0\n }\n }\n\n // Require at least one literal segment match so a placeholder-only pattern\n // like `{resourceType}/{resourceId}` can't capture arbitrary paths.\n return literalMatches === 0 ? 0 : score\n}\n\nfunction parseQueryString(queryString: string): Record<string, string | string[]> {\n // node:querystring collects repeated keys into arrays and returns a\n // null-prototype object, so user-supplied keys like `__proto__` stay\n // ordinary own properties. Parsed values are never `undefined`, hence the\n // narrowing cast.\n return parse(queryString) as Record<string, string | string[]>\n}\n"],"names":["parse","API_DEFAULT_VERSION","API_VERSION_SEGMENT_RE","ApiUsageError","ProjectIdRequiredError","PLACEHOLDER_RE","ALLOWED_URL_HOST_RE","resolveEndpoint","options","apiVersion","dataset","endpoint","forceHost","projectId","routes","test","resolveUrlEndpoint","pathPart","queryParts","split","query","parseQueryString","join","segments","filter","segment","embeddedVersion","shift","length","unresolved","substituted","map","index","placeholder","exec","isDatasetName","push","candidates","entry","host","match","matchRoutes","defaultApiVersion","kind","matchedSlug","slug","path","url","URL","protocol","hostname","search","replace","toString","best","pattern","pathPatterns","score","scorePattern","isBetter","patternSegments","literalMatches","patternSegment","entries","queryString"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/actions/api/resolveEndpoint.ts"],"sourcesContent":["import {parse} from 'node:querystring'\n\nimport {API_DEFAULT_VERSION, API_VERSION_SEGMENT_RE} from './constants.js'\nimport {ApiUsageError, ProjectIdRequiredError} from './errors.js'\nimport {type ApiHost, type ApiRouteEntry} from './types.js'\n\n/**\n * Options for {@link resolveEndpoint}.\n */\nexport interface ResolveEndpointOptions {\n /** The endpoint argument: a path, optionally with placeholders, or a full URL. */\n endpoint: string\n\n /** The generated routing manifest. */\n routes: ApiRouteEntry[]\n\n /** Explicit API version (`--api-version`), wins over everything else. */\n apiVersion?: string\n\n /** Dataset used to fill `{dataset}` / `{datasetName}` placeholders. */\n dataset?: string\n\n /** Force a host family (`--global` / `--project-hosted`), restricting route matching to it. */\n forceHost?: ApiHost\n\n /** Project ID used to fill `{projectId}` placeholders and project-hosted requests. */\n projectId?: string\n}\n\n/**\n * A fully resolved request target.\n */\nexport type ResolvedEndpoint =\n | {\n /** API version path segment, eg `v2021-06-07`. */\n apiVersion: string\n host: ApiHost\n kind: 'path'\n /** Slug of the OpenAPI spec whose paths matched, if any. */\n matchedSlug?: string\n /** Version-less path, without leading slash. */\n path: string\n /** Project ID; always set when `host` is `project`. */\n projectId?: string\n /** Query parameters parsed from the endpoint argument. */\n query: Record<string, string | string[]>\n }\n | {\n kind: 'url'\n query: Record<string, string | string[]>\n /** Absolute URL, used verbatim (query string stripped into `query`). */\n url: string\n }\n\nconst PLACEHOLDER_RE = /^\\{([^{}]+)\\}$/\n\nconst ALLOWED_URL_HOST_RE = /(?:^|\\.)api\\.sanity\\.(?:io|work)$/\n\n/**\n * Resolve the endpoint argument of `sanity api` into a concrete request\n * target: substitute placeholders, split off the query string, determine the\n * host family (via the generated OpenAPI routing manifest) and the API\n * version.\n *\n * Throws {@link ProjectIdRequiredError} when the request needs a project ID\n * that hasn't been provided - the command catches this to resolve one (flags,\n * CLI config or interactive prompt) and retries.\n */\nexport function resolveEndpoint(options: ResolveEndpointOptions): ResolvedEndpoint {\n const {apiVersion, dataset, endpoint, forceHost, projectId, routes} = options\n\n if (/^https?:\\/\\//.test(endpoint)) {\n return resolveUrlEndpoint(endpoint)\n }\n\n const [pathPart = '', ...queryParts] = endpoint.split('?')\n const query = parseQueryString(queryParts.join('?'))\n\n const segments = pathPart.split('/').filter((segment) => segment !== '')\n\n // An API version embedded in the path (eg `v2021-06-07/projects`) is peeled\n // off and re-applied as the version segment of the final URL.\n let embeddedVersion: string | undefined\n if (segments[0] === '{apiVersion}' || segments[0] === '{apiV}') {\n segments.shift()\n } else if (segments.length > 0 && API_VERSION_SEGMENT_RE.test(segments[0])) {\n embeddedVersion = segments.shift()\n }\n\n // Checked after version peeling so a bare version (eg `v2025-02-19`) is\n // also rejected as an empty path.\n if (segments.length === 0) {\n throw new ApiUsageError('Endpoint path is empty')\n }\n\n const unresolved: string[] = []\n const substituted = segments.map((segment, index) => {\n const placeholder = PLACEHOLDER_RE.exec(segment)?.[1]\n if (!placeholder) return segment\n if (placeholder === 'projectId') {\n if (!projectId) throw new ProjectIdRequiredError()\n return projectId\n }\n // The projects spec names the dataset placeholder `{name}` (eg\n // `projects/{projectId}/datasets/{name}`) - only that context is a\n // dataset; `{name}` elsewhere (eg video renditions) stays unresolved.\n const isDatasetName = placeholder === 'name' && segments[index - 1] === 'datasets'\n if (placeholder === 'dataset' || placeholder === 'datasetName' || isDatasetName) {\n if (!dataset) {\n throw new ApiUsageError(\n `Unable to resolve {${placeholder}} - provide a dataset with --dataset or configure one in sanity.cli.ts`,\n )\n }\n return dataset\n }\n unresolved.push(segment)\n return segment\n })\n\n if (unresolved.length > 0) {\n throw new ApiUsageError(\n `Unable to resolve placeholder(s) ${unresolved.join(', ')} - replace them with actual values`,\n )\n }\n\n // Forcing a host restricts matching to specs served on that host, so the\n // matched spec's default API version still applies when one exists there.\n const candidates = forceHost ? routes.filter((entry) => entry.host === forceHost) : routes\n const match = matchRoutes(substituted, candidates)\n const host = forceHost ?? match?.host ?? 'global'\n\n if (host === 'project' && !projectId) {\n throw new ProjectIdRequiredError()\n }\n\n return {\n apiVersion: apiVersion ?? embeddedVersion ?? match?.defaultApiVersion ?? API_DEFAULT_VERSION,\n host,\n kind: 'path',\n matchedSlug: match?.slug,\n path: substituted.join('/'),\n ...(host === 'project' ? {projectId} : {}),\n query,\n }\n}\n\nfunction resolveUrlEndpoint(endpoint: string): ResolvedEndpoint {\n let url: URL\n try {\n url = new URL(endpoint)\n } catch {\n throw new ApiUsageError(`Invalid URL \"${endpoint}\"`)\n }\n\n // Never put the token on the wire unencrypted\n if (url.protocol !== 'https:') {\n throw new ApiUsageError(\n `Refusing to send requests over \"${url.protocol}//\" - only https:// URLs are supported`,\n )\n }\n\n if (!ALLOWED_URL_HOST_RE.test(url.hostname)) {\n throw new ApiUsageError(\n `Refusing to send authenticated requests to \"${url.hostname}\" - only *.api.sanity.io hosts are supported`,\n )\n }\n\n const query = parseQueryString(url.search.replace(/^\\?/, ''))\n url.search = ''\n\n return {kind: 'url', query, url: url.toString()}\n}\n\ninterface RouteMatch {\n host: ApiHost\n score: number\n slug: string\n\n defaultApiVersion?: string\n}\n\n/**\n * Find the route entry whose path pattern best matches the request segments.\n *\n * A pattern only matches when the request path consumes it completely, and\n * segments are compared pairwise: literal matches score higher than\n * placeholder matches, and a literal mismatch disqualifies the pattern. On\n * equal scores across hosts the global host wins (APIs served on both hosts\n * don't need a project ID there); on equal scores within the same host the\n * first entry wins, keeping the result independent of later entries.\n */\nfunction matchRoutes(segments: string[], routes: ApiRouteEntry[]): RouteMatch | undefined {\n let best: RouteMatch | undefined\n\n for (const entry of routes) {\n for (const pattern of entry.pathPatterns) {\n const score = scorePattern(segments, pattern.split('/'))\n if (score === 0) continue\n const isBetter =\n !best ||\n score > best.score ||\n (score === best.score && best.host === 'project' && entry.host === 'global')\n if (isBetter) {\n best = {\n defaultApiVersion: entry.defaultApiVersion,\n host: entry.host,\n score,\n slug: entry.slug,\n }\n }\n }\n }\n\n return best\n}\n\nfunction scorePattern(segments: string[], patternSegments: string[]): number {\n // A pattern only matches when the request path consumes it completely - a\n // request that is a mere prefix of a longer pattern (eg `data` vs\n // `data/query/{dataset}`) must not inherit that route's host or version.\n if (patternSegments.length > segments.length) return 0\n\n let score = 0\n let literalMatches = 0\n\n for (const [index, patternSegment] of patternSegments.entries()) {\n if (PLACEHOLDER_RE.test(patternSegment)) {\n score += 1\n } else if (patternSegment === segments[index]) {\n score += 2\n literalMatches += 1\n } else {\n return 0\n }\n }\n\n // Require at least one literal segment match so a placeholder-only pattern\n // like `{resourceType}/{resourceId}` can't capture arbitrary paths.\n return literalMatches === 0 ? 0 : score\n}\n\nfunction parseQueryString(queryString: string): Record<string, string | string[]> {\n // node:querystring collects repeated keys into arrays and returns a\n // null-prototype object, so user-supplied keys like `__proto__` stay\n // ordinary own properties. Parsed values are never `undefined`, hence the\n // narrowing cast.\n return parse(queryString) as Record<string, string | string[]>\n}\n"],"names":["parse","API_DEFAULT_VERSION","API_VERSION_SEGMENT_RE","ApiUsageError","ProjectIdRequiredError","PLACEHOLDER_RE","ALLOWED_URL_HOST_RE","resolveEndpoint","options","apiVersion","dataset","endpoint","forceHost","projectId","routes","test","resolveUrlEndpoint","pathPart","queryParts","split","query","parseQueryString","join","segments","filter","segment","embeddedVersion","shift","length","unresolved","substituted","map","index","placeholder","exec","isDatasetName","push","candidates","entry","host","match","matchRoutes","defaultApiVersion","kind","matchedSlug","slug","path","url","URL","protocol","hostname","search","replace","toString","best","pattern","pathPatterns","score","scorePattern","isBetter","patternSegments","literalMatches","patternSegment","entries","queryString"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { createWriteStream } from 'node:fs';
2
2
  import { createGzip } from 'node:zlib';
3
- import { pack } from 'tar-fs';
3
+ import { c as createTar } from 'tar';
4
4
  import { backupDownloadDebug } from './backupDownloadDebug.js';
5
5
  /**
6
6
  * Creates a compressed tarball of the given directory and writes it to the specified file path
@@ -25,9 +25,13 @@ import { backupDownloadDebug } from './backupDownloadDebug.js';
25
25
  backupDownloadDebug('Gzip stream error: %s', err.message);
26
26
  reject(err);
27
27
  });
28
- const tarStream = pack(tmpOutDir);
28
+ const tarStream = createTar({
29
+ cwd: tmpOutDir
30
+ }, [
31
+ '.'
32
+ ]);
29
33
  tarStream.on('error', (err)=>{
30
- backupDownloadDebug('Tar stream error: %s', err.message);
34
+ backupDownloadDebug('Tar stream error: %s', err instanceof Error ? err.message : String(err));
31
35
  reject(err);
32
36
  });
33
37
  // Track progress by listening to data events
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/actions/backup/archiveDir.ts"],"sourcesContent":["import {createWriteStream} from 'node:fs'\nimport {createGzip} from 'node:zlib'\n\nimport {pack} from 'tar-fs'\n\nimport {backupDownloadDebug} from './backupDownloadDebug.js'\n\n// ProgressCb is a callback that is called with the number of bytes processed so far.\ntype ProgressCb = (processedBytes: number) => void\n\n/**\n * Creates a compressed tarball of the given directory and writes it to the specified file path\n *\n * @param tmpOutDir - The directory to archive\n * @param outFilePath - The output file path for the compressed tarball\n * @param progressCb - Callback function called with the number of bytes processed\n */\nexport function archiveDir(\n tmpOutDir: string,\n outFilePath: string,\n progressCb: ProgressCb,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n const archiveDestination = createWriteStream(outFilePath)\n const gzipStream = createGzip()\n let processedBytes = 0\n\n archiveDestination.on('error', (err: Error) => {\n backupDownloadDebug('Archive destination error: %s', err.message)\n reject(err)\n })\n\n archiveDestination.on('close', () => {\n backupDownloadDebug('Archive completed successfully')\n resolve()\n })\n\n gzipStream.on('error', (err: Error) => {\n backupDownloadDebug('Gzip stream error: %s', err.message)\n reject(err)\n })\n\n const tarStream = pack(tmpOutDir)\n\n tarStream.on('error', (err: Error) => {\n backupDownloadDebug('Tar stream error: %s', err.message)\n reject(err)\n })\n\n // Track progress by listening to data events\n tarStream.on('data', (chunk: Buffer) => {\n processedBytes += chunk.length\n progressCb(processedBytes)\n })\n\n // Pipe tar -> gzip -> file\n tarStream.pipe(gzipStream).pipe(archiveDestination)\n })\n}\n"],"names":["createWriteStream","createGzip","pack","backupDownloadDebug","archiveDir","tmpOutDir","outFilePath","progressCb","Promise","resolve","reject","archiveDestination","gzipStream","processedBytes","on","err","message","tarStream","chunk","length","pipe"],"mappings":"AAAA,SAAQA,iBAAiB,QAAO,UAAS;AACzC,SAAQC,UAAU,QAAO,YAAW;AAEpC,SAAQC,IAAI,QAAO,SAAQ;AAE3B,SAAQC,mBAAmB,QAAO,2BAA0B;AAK5D;;;;;;CAMC,GACD,OAAO,SAASC,WACdC,SAAiB,EACjBC,WAAmB,EACnBC,UAAsB;IAEtB,OAAO,IAAIC,QAAQ,CAACC,SAASC;QAC3B,MAAMC,qBAAqBX,kBAAkBM;QAC7C,MAAMM,aAAaX;QACnB,IAAIY,iBAAiB;QAErBF,mBAAmBG,EAAE,CAAC,SAAS,CAACC;YAC9BZ,oBAAoB,iCAAiCY,IAAIC,OAAO;YAChEN,OAAOK;QACT;QAEAJ,mBAAmBG,EAAE,CAAC,SAAS;YAC7BX,oBAAoB;YACpBM;QACF;QAEAG,WAAWE,EAAE,CAAC,SAAS,CAACC;YACtBZ,oBAAoB,yBAAyBY,IAAIC,OAAO;YACxDN,OAAOK;QACT;QAEA,MAAME,YAAYf,KAAKG;QAEvBY,UAAUH,EAAE,CAAC,SAAS,CAACC;YACrBZ,oBAAoB,wBAAwBY,IAAIC,OAAO;YACvDN,OAAOK;QACT;QAEA,6CAA6C;QAC7CE,UAAUH,EAAE,CAAC,QAAQ,CAACI;YACpBL,kBAAkBK,MAAMC,MAAM;YAC9BZ,WAAWM;QACb;QAEA,2BAA2B;QAC3BI,UAAUG,IAAI,CAACR,YAAYQ,IAAI,CAACT;IAClC;AACF"}
1
+ {"version":3,"sources":["../../../src/actions/backup/archiveDir.ts"],"sourcesContent":["import {createWriteStream} from 'node:fs'\nimport {createGzip} from 'node:zlib'\n\nimport {c as createTar} from 'tar'\n\nimport {backupDownloadDebug} from './backupDownloadDebug.js'\n\n// ProgressCb is a callback that is called with the number of bytes processed so far.\ntype ProgressCb = (processedBytes: number) => void\n\n/**\n * Creates a compressed tarball of the given directory and writes it to the specified file path\n *\n * @param tmpOutDir - The directory to archive\n * @param outFilePath - The output file path for the compressed tarball\n * @param progressCb - Callback function called with the number of bytes processed\n */\nexport function archiveDir(\n tmpOutDir: string,\n outFilePath: string,\n progressCb: ProgressCb,\n): Promise<void> {\n return new Promise((resolve, reject) => {\n const archiveDestination = createWriteStream(outFilePath)\n const gzipStream = createGzip()\n let processedBytes = 0\n\n archiveDestination.on('error', (err: Error) => {\n backupDownloadDebug('Archive destination error: %s', err.message)\n reject(err)\n })\n\n archiveDestination.on('close', () => {\n backupDownloadDebug('Archive completed successfully')\n resolve()\n })\n\n gzipStream.on('error', (err: Error) => {\n backupDownloadDebug('Gzip stream error: %s', err.message)\n reject(err)\n })\n\n const tarStream = createTar({cwd: tmpOutDir}, ['.'])\n\n tarStream.on('error', (err) => {\n backupDownloadDebug('Tar stream error: %s', err instanceof Error ? err.message : String(err))\n reject(err)\n })\n\n // Track progress by listening to data events\n tarStream.on('data', (chunk: Buffer) => {\n processedBytes += chunk.length\n progressCb(processedBytes)\n })\n\n // Pipe tar -> gzip -> file\n tarStream.pipe(gzipStream).pipe(archiveDestination)\n })\n}\n"],"names":["createWriteStream","createGzip","c","createTar","backupDownloadDebug","archiveDir","tmpOutDir","outFilePath","progressCb","Promise","resolve","reject","archiveDestination","gzipStream","processedBytes","on","err","message","tarStream","cwd","Error","String","chunk","length","pipe"],"mappings":"AAAA,SAAQA,iBAAiB,QAAO,UAAS;AACzC,SAAQC,UAAU,QAAO,YAAW;AAEpC,SAAQC,KAAKC,SAAS,QAAO,MAAK;AAElC,SAAQC,mBAAmB,QAAO,2BAA0B;AAK5D;;;;;;CAMC,GACD,OAAO,SAASC,WACdC,SAAiB,EACjBC,WAAmB,EACnBC,UAAsB;IAEtB,OAAO,IAAIC,QAAQ,CAACC,SAASC;QAC3B,MAAMC,qBAAqBZ,kBAAkBO;QAC7C,MAAMM,aAAaZ;QACnB,IAAIa,iBAAiB;QAErBF,mBAAmBG,EAAE,CAAC,SAAS,CAACC;YAC9BZ,oBAAoB,iCAAiCY,IAAIC,OAAO;YAChEN,OAAOK;QACT;QAEAJ,mBAAmBG,EAAE,CAAC,SAAS;YAC7BX,oBAAoB;YACpBM;QACF;QAEAG,WAAWE,EAAE,CAAC,SAAS,CAACC;YACtBZ,oBAAoB,yBAAyBY,IAAIC,OAAO;YACxDN,OAAOK;QACT;QAEA,MAAME,YAAYf,UAAU;YAACgB,KAAKb;QAAS,GAAG;YAAC;SAAI;QAEnDY,UAAUH,EAAE,CAAC,SAAS,CAACC;YACrBZ,oBAAoB,wBAAwBY,eAAeI,QAAQJ,IAAIC,OAAO,GAAGI,OAAOL;YACxFL,OAAOK;QACT;QAEA,6CAA6C;QAC7CE,UAAUH,EAAE,CAAC,QAAQ,CAACO;YACpBR,kBAAkBQ,MAAMC,MAAM;YAC9Bf,WAAWM;QACb;QAEA,2BAA2B;QAC3BI,UAAUM,IAAI,CAACX,YAAYW,IAAI,CAACZ;IAClC;AACF"}
@@ -0,0 +1,17 @@
1
+ import { promptForOrganization } from '../../prompts/promptForOrganization.js';
2
+ /** The organization could not be determined and prompting is not possible. */ export class MissingOrganizationError extends Error {
3
+ }
4
+ /**
5
+ * Resolve the organization to operate on: the `--organization` flag wins,
6
+ * then the CLI config's `app.organizationId`, then an interactive prompt.
7
+ */ export async function resolveOrganizationId(options) {
8
+ const { configuredOrganizationId, flagOrganizationId, unattended } = options;
9
+ if (flagOrganizationId) return flagOrganizationId;
10
+ if (configuredOrganizationId) return configuredOrganizationId;
11
+ if (unattended) {
12
+ throw new MissingOrganizationError('Organization ID is required. Provide it with the --organization flag.');
13
+ }
14
+ return promptForOrganization();
15
+ }
16
+
17
+ //# sourceMappingURL=resolveOrganizationId.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/actions/context/resolveOrganizationId.ts"],"sourcesContent":["import {promptForOrganization} from '../../prompts/promptForOrganization.js'\n\n/** The organization could not be determined and prompting is not possible. */\nexport class MissingOrganizationError extends Error {}\n\n/**\n * Resolve the organization to operate on: the `--organization` flag wins,\n * then the CLI config's `app.organizationId`, then an interactive prompt.\n */\nexport async function resolveOrganizationId(options: {\n configuredOrganizationId: string | undefined\n flagOrganizationId: string | undefined\n unattended: boolean\n}): Promise<string> {\n const {configuredOrganizationId, flagOrganizationId, unattended} = options\n\n if (flagOrganizationId) return flagOrganizationId\n if (configuredOrganizationId) return configuredOrganizationId\n\n if (unattended) {\n throw new MissingOrganizationError(\n 'Organization ID is required. Provide it with the --organization flag.',\n )\n }\n\n return promptForOrganization()\n}\n"],"names":["promptForOrganization","MissingOrganizationError","Error","resolveOrganizationId","options","configuredOrganizationId","flagOrganizationId","unattended"],"mappings":"AAAA,SAAQA,qBAAqB,QAAO,yCAAwC;AAE5E,4EAA4E,GAC5E,OAAO,MAAMC,iCAAiCC;AAAO;AAErD;;;CAGC,GACD,OAAO,eAAeC,sBAAsBC,OAI3C;IACC,MAAM,EAACC,wBAAwB,EAAEC,kBAAkB,EAAEC,UAAU,EAAC,GAAGH;IAEnE,IAAIE,oBAAoB,OAAOA;IAC/B,IAAID,0BAA0B,OAAOA;IAErC,IAAIE,YAAY;QACd,MAAM,IAAIN,yBACR;IAEJ;IAEA,OAAOD;AACT"}
@@ -0,0 +1,24 @@
1
+ import { setTimeout as sleep } from 'node:timers/promises';
2
+ import { getJob } from '../../services/context.js';
3
+ const JOB_POLL_INTERVAL_MS = 5000;
4
+ const TERMINAL_JOB_STATUSES = new Set([
5
+ 'cancelled',
6
+ 'failed',
7
+ 'succeeded'
8
+ ]);
9
+ /**
10
+ * Poll a knowledge base job until it reaches a terminal state
11
+ * (succeeded, failed or cancelled), resolving with the final job.
12
+ */ export async function waitForJob(knowledgeBaseId, jobId, options) {
13
+ const pollIntervalMs = options?.pollIntervalMs ?? JOB_POLL_INTERVAL_MS;
14
+ while(true){
15
+ const job = await getJob(knowledgeBaseId, jobId);
16
+ if (TERMINAL_JOB_STATUSES.has(job.status)) {
17
+ return job;
18
+ }
19
+ options?.onPoll?.(job);
20
+ await sleep(pollIntervalMs);
21
+ }
22
+ }
23
+
24
+ //# sourceMappingURL=waitForJob.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/actions/context/waitForJob.ts"],"sourcesContent":["import {setTimeout as sleep} from 'node:timers/promises'\n\nimport {type Context} from '@sanity/client'\n\nimport {getJob} from '../../services/context.js'\n\nconst JOB_POLL_INTERVAL_MS = 5000\n\nconst TERMINAL_JOB_STATUSES: ReadonlySet<Context.Job['status']> = new Set([\n 'cancelled',\n 'failed',\n 'succeeded',\n])\n\n/**\n * Poll a knowledge base job until it reaches a terminal state\n * (succeeded, failed or cancelled), resolving with the final job.\n */\nexport async function waitForJob(\n knowledgeBaseId: string,\n jobId: string,\n options?: {\n onPoll?: (job: Context.Job) => void\n pollIntervalMs?: number\n },\n): Promise<Context.Job> {\n const pollIntervalMs = options?.pollIntervalMs ?? JOB_POLL_INTERVAL_MS\n\n while (true) {\n const job = await getJob(knowledgeBaseId, jobId)\n if (TERMINAL_JOB_STATUSES.has(job.status)) {\n return job\n }\n options?.onPoll?.(job)\n await sleep(pollIntervalMs)\n }\n}\n"],"names":["setTimeout","sleep","getJob","JOB_POLL_INTERVAL_MS","TERMINAL_JOB_STATUSES","Set","waitForJob","knowledgeBaseId","jobId","options","pollIntervalMs","job","has","status","onPoll"],"mappings":"AAAA,SAAQA,cAAcC,KAAK,QAAO,uBAAsB;AAIxD,SAAQC,MAAM,QAAO,4BAA2B;AAEhD,MAAMC,uBAAuB;AAE7B,MAAMC,wBAA4D,IAAIC,IAAI;IACxE;IACA;IACA;CACD;AAED;;;CAGC,GACD,OAAO,eAAeC,WACpBC,eAAuB,EACvBC,KAAa,EACbC,OAGC;IAED,MAAMC,iBAAiBD,SAASC,kBAAkBP;IAElD,MAAO,KAAM;QACX,MAAMQ,MAAM,MAAMT,OAAOK,iBAAiBC;QAC1C,IAAIJ,sBAAsBQ,GAAG,CAACD,IAAIE,MAAM,GAAG;YACzC,OAAOF;QACT;QACAF,SAASK,SAASH;QAClB,MAAMV,MAAMS;IACd;AACF"}
@@ -0,0 +1,32 @@
1
+ import { spinner } from '@sanity/cli-core/ux';
2
+ import { waitForJob } from './waitForJob.js';
3
+ /**
4
+ * Watch a knowledge base job with a spinner until it reaches a terminal
5
+ * state, updating the spinner text with the job status on each poll.
6
+ *
7
+ * Fails the spinner and rethrows on polling errors, and fails it when the
8
+ * terminal status is not `succeeded` — turning either outcome into a
9
+ * command error (and exit code) is the caller's responsibility.
10
+ */ export async function watchJob(knowledgeBaseId, jobId, options) {
11
+ const { label, successText } = options;
12
+ const spin = spinner(label).start();
13
+ let job;
14
+ try {
15
+ job = await waitForJob(knowledgeBaseId, jobId, {
16
+ onPoll: (pending)=>{
17
+ spin.text = `${label} (${pending.status})`;
18
+ }
19
+ });
20
+ } catch (error) {
21
+ spin.fail();
22
+ throw error;
23
+ }
24
+ if (job.status === 'succeeded') {
25
+ spin.succeed(successText);
26
+ } else {
27
+ spin.fail();
28
+ }
29
+ return job;
30
+ }
31
+
32
+ //# sourceMappingURL=watchJob.js.map