@sanity/cli 8.11.0 → 8.13.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 +79 -29
- package/dist/actions/assets/ingestAssetFromUrlWithProgress.js +21 -0
- package/dist/actions/assets/ingestAssetFromUrlWithProgress.js.map +1 -0
- package/dist/actions/assets/withUrlIngestProgress.js +45 -0
- package/dist/actions/assets/withUrlIngestProgress.js.map +1 -0
- package/dist/actions/auth/authServer.js +5 -8
- package/dist/actions/auth/authServer.js.map +1 -1
- package/dist/actions/deploy/deployApp.js +5 -2
- package/dist/actions/deploy/deployApp.js.map +1 -1
- package/dist/actions/deploy/deployChecks.js +3 -2
- package/dist/actions/deploy/deployChecks.js.map +1 -1
- package/dist/actions/deploy/findUserApplication.js +3 -2
- package/dist/actions/deploy/findUserApplication.js.map +1 -1
- package/dist/actions/deploy/resolveDeployTarget.js +6 -1
- package/dist/actions/deploy/resolveDeployTarget.js.map +1 -1
- package/dist/actions/documents/validateDocuments.worker.js +1 -4
- package/dist/actions/documents/validateDocuments.worker.js.map +1 -1
- package/dist/actions/media/importMedia.js.map +1 -1
- package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js +19 -0
- package/dist/actions/media/ingestMediaAssetFromUrlWithProgress.js.map +1 -0
- package/dist/commands/assets/upload.js +78 -52
- package/dist/commands/assets/upload.js.map +1 -1
- package/dist/commands/deploy.js +33 -1
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/media/import.js +149 -13
- package/dist/commands/media/import.js.map +1 -1
- package/dist/commands/undeploy.js +41 -5
- package/dist/commands/undeploy.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/index.js +1 -4
- package/dist/exports/invokeSanityCli/commandPolicies/index.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js +26 -6
- package/dist/exports/invokeSanityCli/commandPolicies/mcpPolicy.js.map +1 -1
- package/dist/exports/invokeSanityCli/commandPolicies/resolve.js +112 -0
- package/dist/exports/invokeSanityCli/commandPolicies/resolve.js.map +1 -0
- package/dist/exports/invokeSanityCli/help.js +8 -1
- package/dist/exports/invokeSanityCli/help.js.map +1 -1
- package/dist/exports/invokeSanityCli/index.d.ts +1 -2
- package/dist/exports/invokeSanityCli/index.js +37 -15
- package/dist/exports/invokeSanityCli/index.js.map +1 -1
- package/dist/generated/apiRoutes.js +12 -0
- package/dist/generated/apiRoutes.js.map +1 -1
- package/dist/services/assets.js +46 -1
- package/dist/services/assets.js.map +1 -1
- package/dist/services/mediaLibraries.js +39 -1
- package/dist/services/mediaLibraries.js.map +1 -1
- package/dist/util/assetSourceValidation.js +56 -0
- package/dist/util/assetSourceValidation.js.map +1 -0
- package/dist/util/assetUploadErrors.js +64 -0
- package/dist/util/assetUploadErrors.js.map +1 -0
- package/dist/util/isRemoteAssetSource.js +19 -0
- package/dist/util/isRemoteAssetSource.js.map +1 -0
- package/dist/util/parseAspectFlags.js +34 -0
- package/dist/util/parseAspectFlags.js.map +1 -0
- package/oclif.config.js +0 -1
- package/oclif.manifest.json +952 -864
- package/package.json +18 -18
- package/templates/app-quickstart/AGENTS.md +18 -0
- package/templates/app-sanity-ui/AGENTS.md +18 -0
- package/templates/shopify/.gitignore +26 -0
- package/dist/exports/invokeSanityCli/commandPolicies/policy.js +0 -46
- package/dist/exports/invokeSanityCli/commandPolicies/policy.js.map +0 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { getErrorMessage } from '@sanity/cli-core/errors';
|
|
2
|
+
import { isHttpError } from '@sanity/client';
|
|
3
|
+
const DATASET_ASSET_LIMITS_URL = 'https://www.sanity.io/docs/content-lake/technical-limits#k2c53dc30e24b';
|
|
4
|
+
const MEDIA_LIBRARY_ASSET_LIMITS_URL = 'https://www.sanity.io/docs/media-library';
|
|
5
|
+
const TARGET_GUIDANCE = {
|
|
6
|
+
dataset: {
|
|
7
|
+
limitsUrl: DATASET_ASSET_LIMITS_URL,
|
|
8
|
+
writeAccessTo: 'this dataset'
|
|
9
|
+
},
|
|
10
|
+
'media-library': {
|
|
11
|
+
limitsUrl: MEDIA_LIBRARY_ASSET_LIMITS_URL,
|
|
12
|
+
writeAccessTo: 'this media library'
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
function isProjectUserNotFoundError(body) {
|
|
16
|
+
const responseError = body.error;
|
|
17
|
+
return typeof responseError === 'object' && responseError !== null && 'type' in responseError && responseError.type === 'projectUserNotFoundError';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Turn an asset upload failure into a message that says what the API reported
|
|
21
|
+
* and what to do about it.
|
|
22
|
+
*
|
|
23
|
+
* @param error - The thrown error, HTTP or otherwise.
|
|
24
|
+
* @param options - `fromUrl` selects the gateway-failure guidance, which only
|
|
25
|
+
* makes sense when Content Lake was fetching the source itself; `target`
|
|
26
|
+
* selects the resource-specific wording.
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/ export function getAssetUploadErrorMessage(error, options) {
|
|
30
|
+
if (!isHttpError(error)) {
|
|
31
|
+
return `Asset upload failed: ${getErrorMessage(error)}`;
|
|
32
|
+
}
|
|
33
|
+
const { limitsUrl, writeAccessTo } = TARGET_GUIDANCE[options.target];
|
|
34
|
+
const body = typeof error.response.body === 'object' && error.response.body !== null && !Array.isArray(error.response.body) ? error.response.body : {};
|
|
35
|
+
const responseError = typeof body.error === 'string' ? body.error : error.response.statusMessage || 'HTTP error';
|
|
36
|
+
const statusCode = typeof body.statusCode === 'number' || typeof body.statusCode === 'string' ? body.statusCode : error.statusCode;
|
|
37
|
+
const projectUserNotFound = isProjectUserNotFoundError(body);
|
|
38
|
+
const responseMessage = projectUserNotFound ? error.message : getErrorMessage(error);
|
|
39
|
+
const message = /[.!?]$/.test(responseMessage) ? responseMessage : `${responseMessage}.`;
|
|
40
|
+
const details = typeof body.details === 'string' ? `\n\nDetails:\n${body.details}` : '';
|
|
41
|
+
const response = `Asset upload failed: HTTP ${statusCode} - ${responseError}\n${message}${details}`;
|
|
42
|
+
if (error.statusCode === 401 && !projectUserNotFound) {
|
|
43
|
+
return `${response}\n\nRun \`sanity login\` to authenticate, then try again.`;
|
|
44
|
+
}
|
|
45
|
+
if (error.statusCode === 403) {
|
|
46
|
+
return `${response}\n\nCheck that your account has write access to ${writeAccessTo}, then try again.`;
|
|
47
|
+
}
|
|
48
|
+
if ([
|
|
49
|
+
400,
|
|
50
|
+
413,
|
|
51
|
+
422
|
|
52
|
+
].includes(error.statusCode)) {
|
|
53
|
+
return `${response}\n\nCheck the asset requirements and current technical limits, then try again: ${limitsUrl}`;
|
|
54
|
+
}
|
|
55
|
+
if (options.fromUrl && [
|
|
56
|
+
502,
|
|
57
|
+
504
|
|
58
|
+
].includes(error.statusCode)) {
|
|
59
|
+
return `${response}\n\nSanity could not fetch the source URL. Check that it is reachable from the public internet without authentication and serves the asset directly, then try again.`;
|
|
60
|
+
}
|
|
61
|
+
return `${response}\n\nTry again.`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=assetUploadErrors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/util/assetUploadErrors.ts"],"sourcesContent":["import {getErrorMessage} from '@sanity/cli-core/errors'\nimport {isHttpError} from '@sanity/client'\n\nconst DATASET_ASSET_LIMITS_URL =\n 'https://www.sanity.io/docs/content-lake/technical-limits#k2c53dc30e24b'\nconst MEDIA_LIBRARY_ASSET_LIMITS_URL = 'https://www.sanity.io/docs/media-library'\n\n/**\n * The asset store an upload targeted. Only affects guidance wording: which\n * resource the user needs write access to, and which limits to check.\n */\nexport type AssetUploadTarget = 'dataset' | 'media-library'\n\nconst TARGET_GUIDANCE: Record<AssetUploadTarget, {limitsUrl: string; writeAccessTo: string}> = {\n dataset: {limitsUrl: DATASET_ASSET_LIMITS_URL, writeAccessTo: 'this dataset'},\n 'media-library': {limitsUrl: MEDIA_LIBRARY_ASSET_LIMITS_URL, writeAccessTo: 'this media library'},\n}\n\nfunction isProjectUserNotFoundError(body: Record<string, unknown>): boolean {\n const responseError = body.error\n return (\n typeof responseError === 'object' &&\n responseError !== null &&\n 'type' in responseError &&\n responseError.type === 'projectUserNotFoundError'\n )\n}\n\n/**\n * Turn an asset upload failure into a message that says what the API reported\n * and what to do about it.\n *\n * @param error - The thrown error, HTTP or otherwise.\n * @param options - `fromUrl` selects the gateway-failure guidance, which only\n * makes sense when Content Lake was fetching the source itself; `target`\n * selects the resource-specific wording.\n *\n * @internal\n */\nexport function getAssetUploadErrorMessage(\n error: unknown,\n options: {fromUrl: boolean; target: AssetUploadTarget},\n): string {\n if (!isHttpError(error)) {\n return `Asset upload failed: ${getErrorMessage(error)}`\n }\n\n const {limitsUrl, writeAccessTo} = TARGET_GUIDANCE[options.target]\n\n const body =\n typeof error.response.body === 'object' &&\n error.response.body !== null &&\n !Array.isArray(error.response.body)\n ? (error.response.body as Record<string, unknown>)\n : {}\n\n const responseError =\n typeof body.error === 'string' ? body.error : error.response.statusMessage || 'HTTP error'\n\n const statusCode =\n typeof body.statusCode === 'number' || typeof body.statusCode === 'string'\n ? body.statusCode\n : error.statusCode\n\n const projectUserNotFound = isProjectUserNotFoundError(body)\n const responseMessage = projectUserNotFound ? error.message : getErrorMessage(error)\n const message = /[.!?]$/.test(responseMessage) ? responseMessage : `${responseMessage}.`\n const details = typeof body.details === 'string' ? `\\n\\nDetails:\\n${body.details}` : ''\n const response = `Asset upload failed: HTTP ${statusCode} - ${responseError}\\n${message}${details}`\n\n if (error.statusCode === 401 && !projectUserNotFound) {\n return `${response}\\n\\nRun \\`sanity login\\` to authenticate, then try again.`\n }\n if (error.statusCode === 403) {\n return `${response}\\n\\nCheck that your account has write access to ${writeAccessTo}, then try again.`\n }\n if ([400, 413, 422].includes(error.statusCode)) {\n return `${response}\\n\\nCheck the asset requirements and current technical limits, then try again: ${limitsUrl}`\n }\n if (options.fromUrl && [502, 504].includes(error.statusCode)) {\n return `${response}\\n\\nSanity could not fetch the source URL. Check that it is reachable from the public internet without authentication and serves the asset directly, then try again.`\n }\n return `${response}\\n\\nTry again.`\n}\n"],"names":["getErrorMessage","isHttpError","DATASET_ASSET_LIMITS_URL","MEDIA_LIBRARY_ASSET_LIMITS_URL","TARGET_GUIDANCE","dataset","limitsUrl","writeAccessTo","isProjectUserNotFoundError","body","responseError","error","type","getAssetUploadErrorMessage","options","target","response","Array","isArray","statusMessage","statusCode","projectUserNotFound","responseMessage","message","test","details","includes","fromUrl"],"mappings":"AAAA,SAAQA,eAAe,QAAO,0BAAyB;AACvD,SAAQC,WAAW,QAAO,iBAAgB;AAE1C,MAAMC,2BACJ;AACF,MAAMC,iCAAiC;AAQvC,MAAMC,kBAAyF;IAC7FC,SAAS;QAACC,WAAWJ;QAA0BK,eAAe;IAAc;IAC5E,iBAAiB;QAACD,WAAWH;QAAgCI,eAAe;IAAoB;AAClG;AAEA,SAASC,2BAA2BC,IAA6B;IAC/D,MAAMC,gBAAgBD,KAAKE,KAAK;IAChC,OACE,OAAOD,kBAAkB,YACzBA,kBAAkB,QAClB,UAAUA,iBACVA,cAAcE,IAAI,KAAK;AAE3B;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASC,2BACdF,KAAc,EACdG,OAAsD;IAEtD,IAAI,CAACb,YAAYU,QAAQ;QACvB,OAAO,CAAC,qBAAqB,EAAEX,gBAAgBW,QAAQ;IACzD;IAEA,MAAM,EAACL,SAAS,EAAEC,aAAa,EAAC,GAAGH,eAAe,CAACU,QAAQC,MAAM,CAAC;IAElE,MAAMN,OACJ,OAAOE,MAAMK,QAAQ,CAACP,IAAI,KAAK,YAC/BE,MAAMK,QAAQ,CAACP,IAAI,KAAK,QACxB,CAACQ,MAAMC,OAAO,CAACP,MAAMK,QAAQ,CAACP,IAAI,IAC7BE,MAAMK,QAAQ,CAACP,IAAI,GACpB,CAAC;IAEP,MAAMC,gBACJ,OAAOD,KAAKE,KAAK,KAAK,WAAWF,KAAKE,KAAK,GAAGA,MAAMK,QAAQ,CAACG,aAAa,IAAI;IAEhF,MAAMC,aACJ,OAAOX,KAAKW,UAAU,KAAK,YAAY,OAAOX,KAAKW,UAAU,KAAK,WAC9DX,KAAKW,UAAU,GACfT,MAAMS,UAAU;IAEtB,MAAMC,sBAAsBb,2BAA2BC;IACvD,MAAMa,kBAAkBD,sBAAsBV,MAAMY,OAAO,GAAGvB,gBAAgBW;IAC9E,MAAMY,UAAU,SAASC,IAAI,CAACF,mBAAmBA,kBAAkB,GAAGA,gBAAgB,CAAC,CAAC;IACxF,MAAMG,UAAU,OAAOhB,KAAKgB,OAAO,KAAK,WAAW,CAAC,cAAc,EAAEhB,KAAKgB,OAAO,EAAE,GAAG;IACrF,MAAMT,WAAW,CAAC,0BAA0B,EAAEI,WAAW,GAAG,EAAEV,cAAc,EAAE,EAAEa,UAAUE,SAAS;IAEnG,IAAId,MAAMS,UAAU,KAAK,OAAO,CAACC,qBAAqB;QACpD,OAAO,GAAGL,SAAS,yDAAyD,CAAC;IAC/E;IACA,IAAIL,MAAMS,UAAU,KAAK,KAAK;QAC5B,OAAO,GAAGJ,SAAS,gDAAgD,EAAET,cAAc,iBAAiB,CAAC;IACvG;IACA,IAAI;QAAC;QAAK;QAAK;KAAI,CAACmB,QAAQ,CAACf,MAAMS,UAAU,GAAG;QAC9C,OAAO,GAAGJ,SAAS,+EAA+E,EAAEV,WAAW;IACjH;IACA,IAAIQ,QAAQa,OAAO,IAAI;QAAC;QAAK;KAAI,CAACD,QAAQ,CAACf,MAAMS,UAAU,GAAG;QAC5D,OAAO,GAAGJ,SAAS,oKAAoK,CAAC;IAC1L;IACA,OAAO,GAAGA,SAAS,cAAc,CAAC;AACpC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a command's source argument names a remote asset for Content Lake to
|
|
3
|
+
* fetch, rather than a path on the local filesystem.
|
|
4
|
+
*
|
|
5
|
+
* The protocol allowlist is load-bearing rather than defensive: a Windows path
|
|
6
|
+
* parses as a URL whose protocol is its drive letter (`C:\media` → `c:`), so
|
|
7
|
+
* matching on parseability alone would route local imports to the network.
|
|
8
|
+
* Anything else, `file:` included, is treated as a local path.
|
|
9
|
+
*
|
|
10
|
+
* `http:` counts as remote even though Content Lake only fetches over `https:`,
|
|
11
|
+
* so that an insecure URL reaches `getIngestUrlError` and is reported as one,
|
|
12
|
+
* rather than being stat'ed as a directory that does not exist.
|
|
13
|
+
*/ export function isRemoteAssetSource(source) {
|
|
14
|
+
if (!URL.canParse(source)) return false;
|
|
15
|
+
const { protocol } = new URL(source);
|
|
16
|
+
return protocol === 'http:' || protocol === 'https:';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//# sourceMappingURL=isRemoteAssetSource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/util/isRemoteAssetSource.ts"],"sourcesContent":["/**\n * Whether a command's source argument names a remote asset for Content Lake to\n * fetch, rather than a path on the local filesystem.\n *\n * The protocol allowlist is load-bearing rather than defensive: a Windows path\n * parses as a URL whose protocol is its drive letter (`C:\\media` → `c:`), so\n * matching on parseability alone would route local imports to the network.\n * Anything else, `file:` included, is treated as a local path.\n *\n * `http:` counts as remote even though Content Lake only fetches over `https:`,\n * so that an insecure URL reaches `getIngestUrlError` and is reported as one,\n * rather than being stat'ed as a directory that does not exist.\n */\nexport function isRemoteAssetSource(source: string): boolean {\n if (!URL.canParse(source)) return false\n const {protocol} = new URL(source)\n return protocol === 'http:' || protocol === 'https:'\n}\n"],"names":["isRemoteAssetSource","source","URL","canParse","protocol"],"mappings":"AAAA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASA,oBAAoBC,MAAc;IAChD,IAAI,CAACC,IAAIC,QAAQ,CAACF,SAAS,OAAO;IAClC,MAAM,EAACG,QAAQ,EAAC,GAAG,IAAIF,IAAID;IAC3B,OAAOG,aAAa,WAAWA,aAAa;AAC9C"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parsed `--aspect` values, or the message explaining which occurrence was
|
|
3
|
+
* malformed. Returned rather than thrown so the caller decides the exit code.
|
|
4
|
+
*/ /**
|
|
5
|
+
* Assemble repeated `--aspect key=value` flags into the aspects object the
|
|
6
|
+
* media library ingest endpoint accepts.
|
|
7
|
+
*
|
|
8
|
+
* Values stay strings: the CLI has no access to the aspect definitions, so
|
|
9
|
+
* coercing `42` or `true` would guess at the target field's type. Aspects
|
|
10
|
+
* needing non-string or nested values belong in the `data.ndjson` of a
|
|
11
|
+
* directory import.
|
|
12
|
+
*
|
|
13
|
+
* Later occurrences of a key win, matching how `--header` and the NDJSON
|
|
14
|
+
* aspect index resolve duplicates.
|
|
15
|
+
*
|
|
16
|
+
* @internal
|
|
17
|
+
*/ export function parseAspectFlags(aspectFlags) {
|
|
18
|
+
// Null-prototype so an aspect named e.g. `constructor` cannot reach Object.prototype
|
|
19
|
+
const aspects = Object.create(null);
|
|
20
|
+
for (const aspect of aspectFlags){
|
|
21
|
+
const separatorIndex = aspect.indexOf('=');
|
|
22
|
+
if (separatorIndex < 1) {
|
|
23
|
+
return {
|
|
24
|
+
error: `Invalid --aspect "${aspect}": expected key=value format`
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
aspects[aspect.slice(0, separatorIndex).trim()] = aspect.slice(separatorIndex + 1);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
aspects
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=parseAspectFlags.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/util/parseAspectFlags.ts"],"sourcesContent":["/**\n * Parsed `--aspect` values, or the message explaining which occurrence was\n * malformed. Returned rather than thrown so the caller decides the exit code.\n */\ntype ParseAspectFlagsResult = {aspects: Record<string, string>} | {error: string}\n\n/**\n * Assemble repeated `--aspect key=value` flags into the aspects object the\n * media library ingest endpoint accepts.\n *\n * Values stay strings: the CLI has no access to the aspect definitions, so\n * coercing `42` or `true` would guess at the target field's type. Aspects\n * needing non-string or nested values belong in the `data.ndjson` of a\n * directory import.\n *\n * Later occurrences of a key win, matching how `--header` and the NDJSON\n * aspect index resolve duplicates.\n *\n * @internal\n */\nexport function parseAspectFlags(aspectFlags: readonly string[]): ParseAspectFlagsResult {\n // Null-prototype so an aspect named e.g. `constructor` cannot reach Object.prototype\n const aspects: Record<string, string> = Object.create(null)\n\n for (const aspect of aspectFlags) {\n const separatorIndex = aspect.indexOf('=')\n if (separatorIndex < 1) {\n return {error: `Invalid --aspect \"${aspect}\": expected key=value format`}\n }\n aspects[aspect.slice(0, separatorIndex).trim()] = aspect.slice(separatorIndex + 1)\n }\n\n return {aspects}\n}\n"],"names":["parseAspectFlags","aspectFlags","aspects","Object","create","aspect","separatorIndex","indexOf","error","slice","trim"],"mappings":"AAAA;;;CAGC,GAGD;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASA,iBAAiBC,WAA8B;IAC7D,qFAAqF;IACrF,MAAMC,UAAkCC,OAAOC,MAAM,CAAC;IAEtD,KAAK,MAAMC,UAAUJ,YAAa;QAChC,MAAMK,iBAAiBD,OAAOE,OAAO,CAAC;QACtC,IAAID,iBAAiB,GAAG;YACtB,OAAO;gBAACE,OAAO,CAAC,kBAAkB,EAAEH,OAAO,4BAA4B,CAAC;YAAA;QAC1E;QACAH,OAAO,CAACG,OAAOI,KAAK,CAAC,GAAGH,gBAAgBI,IAAI,GAAG,GAAGL,OAAOI,KAAK,CAACH,iBAAiB;IAClF;IAEA,OAAO;QAACJ;IAAO;AACjB"}
|
package/oclif.config.js
CHANGED
|
@@ -36,7 +36,6 @@ export default {
|
|
|
36
36
|
tokens: {description: 'Manage API tokens for your project'},
|
|
37
37
|
typegen: {description: 'Generate TypeScript types for schema and GROQ'},
|
|
38
38
|
users: {description: 'Manage project users and invitations'},
|
|
39
|
-
workflows: {hidden: true},
|
|
40
39
|
},
|
|
41
40
|
topicSeparator: ' ',
|
|
42
41
|
}
|