@myapihq/cli 2.8.0 → 2.9.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.
|
@@ -17,6 +17,7 @@ export declare function deploy(id: string, image: string, flags: Flags): Promise
|
|
|
17
17
|
export declare function _parseSmoke(raw: string): sdkContainer.SmokeCheck;
|
|
18
18
|
export declare function revisions(id: string, flags: Flags): Promise<void>;
|
|
19
19
|
export declare function promote(id: string, revision: string | undefined, flags: Flags): Promise<void>;
|
|
20
|
+
export declare function rollback(id: string, flags: Flags): Promise<void>;
|
|
20
21
|
export declare function logs(id: string, flags: Flags): Promise<void>;
|
|
21
22
|
export declare function domain(id: string, domainArg: string | undefined, flags: Flags): Promise<void>;
|
|
22
23
|
export declare function buildLogs(id: string | undefined, flags: Flags): Promise<void>;
|
|
@@ -239,17 +239,16 @@ export async function deploy(id, image, flags) {
|
|
|
239
239
|
// write their own; a guard that silently passes makes them stop. Restore
|
|
240
240
|
// these the moment the upstream fix lands — see
|
|
241
241
|
// docs/cross-repo-prompts/backend-consolidated-2026-07-28.md.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
'
|
|
248
|
-
'
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
'Reported upstream; this message goes away when the flag works.');
|
|
242
|
+
// --smoke is still not honoured; --no-promote was fixed upstream on
|
|
243
|
+
// 2026-07-28 and works on both deploy paths. Refusing what does not work
|
|
244
|
+
// beats accepting it, and un-refusing what does beats obstructing.
|
|
245
|
+
if (typeof flags.smoke === 'string') {
|
|
246
|
+
error('--smoke is not honoured yet, so this CLI refuses it rather than letting you believe a deploy was checked.\n\n' +
|
|
247
|
+
'Use --no-promote instead, which now works:\n' +
|
|
248
|
+
' 1. myapi container deploy <id> <image> --no-promote\n' +
|
|
249
|
+
' 2. curl the revision URL it prints, for a string only a real build emits\n' +
|
|
250
|
+
' 3. myapi container promote <id> <revision>\n\n' +
|
|
251
|
+
'That is the same verify-then-promote, done by you rather than the platform.');
|
|
253
252
|
}
|
|
254
253
|
const source = typeof flags.source === 'string' ? flags.source : undefined;
|
|
255
254
|
// --image is an alias for the positional image ref.
|
|
@@ -258,6 +257,11 @@ export async function deploy(id, image, flags) {
|
|
|
258
257
|
if (source && image) {
|
|
259
258
|
error('Pass either an image ref or --source, not both.');
|
|
260
259
|
}
|
|
260
|
+
// Built once, passed to BOTH branches. The original bug was building these
|
|
261
|
+
// inside the image branch only, so a --source deploy dropped them silently.
|
|
262
|
+
const deployOpts = {};
|
|
263
|
+
if (flags['no-promote'] === true)
|
|
264
|
+
deployOpts.promote = false;
|
|
261
265
|
// ── Source-build path (async) ───────────────────────────────────────────
|
|
262
266
|
if (source) {
|
|
263
267
|
let tarball;
|
|
@@ -281,7 +285,7 @@ export async function deploy(id, image, flags) {
|
|
|
281
285
|
else {
|
|
282
286
|
error(`--source must be a directory or a .tar/.tar.gz/.tgz archive — got ${source}`);
|
|
283
287
|
}
|
|
284
|
-
const start = await sdkContainer.deployContainerSource(config.api_key, orgId, id, tarball, filename);
|
|
288
|
+
const start = await sdkContainer.deployContainerSource(config.api_key, orgId, id, tarball, filename, deployOpts);
|
|
285
289
|
if (flags.json && start.status !== 'building') {
|
|
286
290
|
printJson(start);
|
|
287
291
|
return;
|
|
@@ -320,10 +324,7 @@ export async function deploy(id, image, flags) {
|
|
|
320
324
|
// ── Pre-built image path (sync) ─────────────────────────────────────────
|
|
321
325
|
if (!image)
|
|
322
326
|
error('Missing image ref.\nUsage: myapi container deploy <id> <image-ref>\n or: myapi container deploy <id> --source <dir|tar>\n\n→ <image-ref> is a pre-built container image (e.g. a registry path).');
|
|
323
|
-
|
|
324
|
-
// until the upstream fix lands. The SDK still carries them so the wiring is
|
|
325
|
-
// one commit away, and sdk-container.test.ts keeps them covered.
|
|
326
|
-
const result = await sdkContainer.deployContainer(config.api_key, orgId, id, image);
|
|
327
|
+
const result = await sdkContainer.deployContainer(config.api_key, orgId, id, image, deployOpts);
|
|
327
328
|
if (flags.json) {
|
|
328
329
|
printJson(result);
|
|
329
330
|
return;
|
|
@@ -331,9 +332,18 @@ export async function deploy(id, image, flags) {
|
|
|
331
332
|
// An unpromoted revision must NOT read like a completed deploy. A response
|
|
332
333
|
// that looked the same either way is how an agent concludes it has shipped
|
|
333
334
|
// when it has not — the original outage in miniature.
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
335
|
+
// Read `promoted` rather than assuming --no-promote was honoured. On a
|
|
336
|
+
// container's FIRST deploy there is nothing already serving to hold traffic,
|
|
337
|
+
// so the platform promotes anyway and says so — reporting "not serving"
|
|
338
|
+
// there would be the lie this flag exists to prevent.
|
|
339
|
+
if (flags['no-promote'] === true && result.promoted !== false) {
|
|
340
|
+
success(`Deployed container ${id} (revision ${result.revision_id})`);
|
|
341
|
+
info('');
|
|
342
|
+
info('Note: --no-promote was NOT applied. This container had nothing already');
|
|
343
|
+
info('serving, so withholding traffic would have left it answering nothing.');
|
|
344
|
+
info(`URL: ${result.url}`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
337
347
|
if (result.promoted === false) {
|
|
338
348
|
success(`Revision ${result.revision_id} built — NOT serving traffic`);
|
|
339
349
|
info(`Test it: ${result.revision_url ?? '(no revision URL returned)'}`);
|
|
@@ -417,13 +427,14 @@ export async function revisions(id, flags) {
|
|
|
417
427
|
// state.
|
|
418
428
|
//
|
|
419
429
|
// Say so rather than render a table that reads as "nothing is live".
|
|
430
|
+
// The 0%-everywhere reporting bug was fixed upstream on 2026-07-28 (a v2
|
|
431
|
+
// traffic target of type LATEST carries no revision name, so 100% was filed
|
|
432
|
+
// under ""). Keeping a narrower check: all-zero on an active container is
|
|
433
|
+
// still worth flagging, it is just no longer expected.
|
|
420
434
|
if (revs.length > 0 && revs.every(r => !r.serving && !r.traffic_percent) && container?.status === 'active') {
|
|
421
435
|
info('');
|
|
422
|
-
info('Note:
|
|
423
|
-
info('
|
|
424
|
-
info('today, so this is not limited to older ones — an earlier version of this message');
|
|
425
|
-
info('said redeploying fixes it, which was wrong.');
|
|
426
|
-
info('promote depends on this data and currently fails. Reported upstream.');
|
|
436
|
+
info('Note: no revision reports any traffic while this container is active.');
|
|
437
|
+
info('That should not happen — report it rather than trusting the column.');
|
|
427
438
|
}
|
|
428
439
|
}
|
|
429
440
|
// promote moves all traffic to one revision. Omitting the revision rolls back
|
|
@@ -447,16 +458,25 @@ export async function promote(id, revision, flags) {
|
|
|
447
458
|
if (res.message)
|
|
448
459
|
info(res.message);
|
|
449
460
|
}
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
'
|
|
459
|
-
|
|
461
|
+
// rollback moves traffic to the previous ready revision. The API models it as
|
|
462
|
+
// `promote` with no revision named — one operation, two targets — but
|
|
463
|
+
// `rollback` is the word someone types during an incident, so it is a verb
|
|
464
|
+
// here even though the SDK has a single function.
|
|
465
|
+
export async function rollback(id, flags) {
|
|
466
|
+
const config = requireConfig();
|
|
467
|
+
const orgId = requireOrg(flags, config, 'myapi container rollback <id> [--org <id>]');
|
|
468
|
+
if (!id)
|
|
469
|
+
error('Missing id.\nUsage: myapi container rollback <id>');
|
|
470
|
+
const res = await sdkContainer.promoteRevision(config.api_key, orgId, id);
|
|
471
|
+
if (flags.json) {
|
|
472
|
+
printJson(res);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
success(`Rolled back ${id} to the previous ready revision`);
|
|
476
|
+
if (res.serving)
|
|
477
|
+
info(`Now serving: ${res.serving}`);
|
|
478
|
+
if (res.message)
|
|
479
|
+
info(res.message);
|
|
460
480
|
}
|
|
461
481
|
// logs prints the container's recent runtime logs, newest first. By default
|
|
462
482
|
// this is the container's own stdout/stderr; --scope all adds the platform
|
|
@@ -565,19 +585,25 @@ Two ways to deploy:
|
|
|
565
585
|
built server-side (typically ~4 minutes), then deployed.
|
|
566
586
|
Asynchronous — the CLI polls until it's live.
|
|
567
587
|
|
|
568
|
-
|
|
588
|
+
Options:
|
|
589
|
+
--no-promote Build the revision without giving it traffic, on either
|
|
590
|
+
deploy path. Prints a revision URL to test at, then:
|
|
591
|
+
myapi container promote <id> <revision>
|
|
569
592
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
A guard that silently passes is worse than no guard.
|
|
593
|
+
On a container's FIRST deploy there is nothing already
|
|
594
|
+
serving, so traffic is NOT withheld and the output says so.
|
|
573
595
|
|
|
574
|
-
|
|
575
|
-
|
|
596
|
+
--smoke Still refused — the platform accepts it and does nothing.
|
|
597
|
+
--no-promote gives you the same verify-then-promote by hand.
|
|
576
598
|
|
|
577
599
|
Examples:
|
|
578
600
|
myapi container deploy <id> registry.example.com/my-app:v2
|
|
579
601
|
myapi container deploy <id> --source ./my-app
|
|
580
|
-
myapi container deploy <id> --
|
|
602
|
+
myapi container deploy <id> <image> --no-promote`,
|
|
603
|
+
'rollback': `myapi container rollback <id> [--org <id>] [--json]
|
|
604
|
+
|
|
605
|
+
Move traffic back to the previous ready revision. Seconds, no rebuild.
|
|
606
|
+
Refuses rather than guessing when it cannot tell what is serving.`,
|
|
581
607
|
'revisions': `myapi container revisions <id> [--org <id>] [--json]
|
|
582
608
|
|
|
583
609
|
Every revision the runtime currently holds, newest first, with the traffic
|
|
@@ -629,6 +655,7 @@ Subcommands:
|
|
|
629
655
|
logs <id> Show recent runtime logs (--tail <n>, --scope all) — see build-logs for build failures
|
|
630
656
|
promote <id> <rev> Move all traffic to a revision (seconds, no rebuild)
|
|
631
657
|
revisions <id> List revisions and the traffic each takes
|
|
658
|
+
rollback <id> Move traffic back to the previous ready revision
|
|
632
659
|
|
|
633
660
|
All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
634
661
|
return;
|
|
@@ -652,12 +679,7 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
652
679
|
case 'promote': return promote(args[0], args[1], flags);
|
|
653
680
|
case 'domain': return domain(args[0], args[1], flags);
|
|
654
681
|
case 'delete': return del(args[0], flags);
|
|
655
|
-
|
|
656
|
-
// NOT a verb here — the API models rollback as `promote` with no revision.
|
|
657
|
-
// A bare "unknown subcommand" would cost minutes at the worst possible
|
|
658
|
-
// moment, so say what to do instead, and be honest that the underlying
|
|
659
|
-
// call is currently broken rather than let someone discover that live.
|
|
660
|
-
case 'rollback': return rollbackGuidance(args[0]);
|
|
682
|
+
case 'rollback': return rollback(args[0], flags);
|
|
661
683
|
default: error(`Unknown subcommand: ${subcommand}. Run "myapi container --help" for a list of valid subcommands.`);
|
|
662
684
|
}
|
|
663
685
|
}
|
|
@@ -24,7 +24,8 @@ export const SCHEMA = {
|
|
|
24
24
|
route: 'string',
|
|
25
25
|
origins: 'string',
|
|
26
26
|
};
|
|
27
|
-
|
|
27
|
+
// The platform's enum, checked against the schema rather than invented.
|
|
28
|
+
const KINDS = ['bug', 'issue', 'suggestion'];
|
|
28
29
|
export async function list(flags) {
|
|
29
30
|
const config = requireConfig();
|
|
30
31
|
const orgId = requireOrg(flags, config, 'myapi feedback list [--kind <k>] [--status open|resolved] [--org <id>]');
|
|
@@ -64,7 +65,7 @@ export async function create(bodyArg, flags) {
|
|
|
64
65
|
const orgId = requireOrg(flags, config, 'myapi feedback create "<text>" --kind <k> [--org <id>]');
|
|
65
66
|
const body = bodyArg ?? flags.body;
|
|
66
67
|
requireArg(body, 'text', 'myapi feedback create "<text>" --kind bug');
|
|
67
|
-
const kind = flags.kind ?? '
|
|
68
|
+
const kind = flags.kind ?? 'issue';
|
|
68
69
|
if (!KINDS.includes(kind)) {
|
|
69
70
|
error(`Invalid --kind "${kind}". Use one of: ${KINDS.join(', ')}.\n\n→ Kind is what the PERSON says it is. "bug" is a claim that the product is broken; do not infer it from the wording.`);
|
|
70
71
|
}
|
|
@@ -127,7 +128,7 @@ export async function widget(sub, arg, flags) {
|
|
|
127
128
|
error('Usage: myapi feedback widget create <name> [--origins <list>]\n myapi feedback widget revoke <id>');
|
|
128
129
|
}
|
|
129
130
|
const SUBCOMMAND_USAGE = {
|
|
130
|
-
'list': `myapi feedback list [--kind bug|
|
|
131
|
+
'list': `myapi feedback list [--kind bug|issue|suggestion] [--status open|resolved]
|
|
131
132
|
[--limit N] [--offset N] [--org <id>] [--json]
|
|
132
133
|
|
|
133
134
|
Newest first. \`total\` is the number of matches, not the page size.`,
|
|
@@ -149,7 +150,7 @@ Collect feedback from the people using what you built. A widget key lets a
|
|
|
149
150
|
page submit without a credential; you list, filter and resolve the results.
|
|
150
151
|
|
|
151
152
|
Subcommands:
|
|
152
|
-
create "<text>" Record one piece of feedback (--kind bug|
|
|
153
|
+
create "<text>" Record one piece of feedback (--kind bug|issue|suggestion)
|
|
153
154
|
list List feedback, newest first (--kind, --status, --limit, --offset)
|
|
154
155
|
resolve <id> Close a piece of feedback
|
|
155
156
|
widget create <name> Mint a PUBLIC widget key for a site (--origins to restrict)
|
|
@@ -83,38 +83,51 @@ async function run(fn) {
|
|
|
83
83
|
throw e;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
describe('container deploy —
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
86
|
+
describe('container deploy — --no-promote must REACH both paths', () => {
|
|
87
|
+
// This is the original bug in its final form. --no-promote was wired to the
|
|
88
|
+
// image branch only, so a --source deploy accepted it and dropped it, and
|
|
89
|
+
// the build took 100% of traffic. The platform has since fixed its side and
|
|
90
|
+
// the flag works on both paths — so the assertion flips from "is refused" to
|
|
91
|
+
// "arrives", and the bug it guards is the same one either way.
|
|
92
|
+
const DEPLOYED = {
|
|
93
|
+
container_id: 'c1', revision_id: 'r1', url: 'https://x',
|
|
94
|
+
status: 'active', scoped_api_key: 'k', promoted: false,
|
|
95
|
+
revision_url: 'https://rev---x.run.app',
|
|
96
|
+
};
|
|
97
|
+
it('sends promote:false on the image path', async () => {
|
|
98
|
+
sdk.container.deployContainer.mockResolvedValue(DEPLOYED);
|
|
91
99
|
const { deploy } = await import('./container.js');
|
|
92
|
-
await run(() => deploy('c1', '', {
|
|
93
|
-
expect(
|
|
94
|
-
expect(sdk.container.deployContainerSource).not.toHaveBeenCalled();
|
|
95
|
-
expect(sdk.container.deployContainer).not.toHaveBeenCalled();
|
|
100
|
+
await run(() => deploy('c1', 'img:v1', { 'no-promote': true, org: ORG }));
|
|
101
|
+
expect(sdk.container.deployContainer).toHaveBeenCalledWith('hq_live_test', ORG, 'c1', 'img:v1', { promote: false });
|
|
96
102
|
});
|
|
97
|
-
|
|
103
|
+
// The branch that shipped broken. It reads a real tarball, so the assertion
|
|
104
|
+
// is that the options object reaches the call — argument 6.
|
|
105
|
+
it('sends promote:false on the --source path', async () => {
|
|
106
|
+
sdk.container.deployContainerSource.mockResolvedValue({ container_id: 'c1', revision_id: 'r1', status: 'building' });
|
|
107
|
+
sdk.container.getContainer.mockResolvedValue({ id: 'c1', status: 'active', url: 'https://x' });
|
|
108
|
+
const { _isTarball } = await import('./container.js');
|
|
109
|
+
void _isTarball;
|
|
98
110
|
const { deploy } = await import('./container.js');
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
const fsp = await import('node:fs/promises');
|
|
112
|
+
const tmp = `${process.env.TMPDIR ?? '/tmp'}/reach-${Date.now()}.tar.gz`;
|
|
113
|
+
await fsp.writeFile(tmp, 'not-a-real-tarball');
|
|
114
|
+
await run(() => deploy('c1', '', { source: tmp, 'no-promote': true, org: ORG }));
|
|
115
|
+
await fsp.rm(tmp, { force: true });
|
|
116
|
+
expect(sdk.container.deployContainerSource).toHaveBeenCalled();
|
|
117
|
+
expect(sdk.container.deployContainerSource.mock.calls[0][5]).toEqual({ promote: false });
|
|
102
118
|
});
|
|
103
|
-
it('
|
|
119
|
+
it('sends no options at all when the flag is absent', async () => {
|
|
120
|
+
sdk.container.deployContainer.mockResolvedValue({ ...DEPLOYED, promoted: true });
|
|
104
121
|
const { deploy } = await import('./container.js');
|
|
105
|
-
await run(() => deploy('c1', 'img:v1', {
|
|
106
|
-
expect(
|
|
107
|
-
expect(sdk.container.deployContainer).not.toHaveBeenCalled();
|
|
122
|
+
await run(() => deploy('c1', 'img:v1', { org: ORG }));
|
|
123
|
+
expect(sdk.container.deployContainer).toHaveBeenCalledWith('hq_live_test', ORG, 'c1', 'img:v1', {});
|
|
108
124
|
});
|
|
109
|
-
//
|
|
110
|
-
it('still
|
|
111
|
-
sdk.container.deployContainer.mockResolvedValue({
|
|
112
|
-
container_id: 'c1', revision_id: 'r1', url: 'https://x', status: 'active', scoped_api_key: 'k',
|
|
113
|
-
});
|
|
125
|
+
// --smoke is still not honoured upstream, and refusing beats accepting.
|
|
126
|
+
it('still refuses --smoke, on either path', async () => {
|
|
114
127
|
const { deploy } = await import('./container.js');
|
|
115
|
-
await run(() => deploy('c1', 'img:v1', { org: ORG }));
|
|
116
|
-
expect(exitError).
|
|
117
|
-
expect(sdk.container.deployContainer).
|
|
128
|
+
await run(() => deploy('c1', 'img:v1', { smoke: 'GET / contains x', org: ORG }));
|
|
129
|
+
expect(exitError).toMatch(/--smoke is not honoured yet/);
|
|
130
|
+
expect(sdk.container.deployContainer).not.toHaveBeenCalled();
|
|
118
131
|
});
|
|
119
132
|
});
|
|
120
133
|
describe('crm pagination — --offset must reach the SDK', () => {
|
|
@@ -4,7 +4,7 @@ version: 1.0.0
|
|
|
4
4
|
description: >
|
|
5
5
|
Edge-hosted asset storage. Upload a local file of any content type directly, or have the server fetch from a public URL. Each asset gets a stable public CDN URL.
|
|
6
6
|
triggers: [storage, upload, ingest, asset, cdn, image hosting, file upload, get-url, download, public url]
|
|
7
|
-
checksum: sha256-
|
|
7
|
+
checksum: sha256-1f3e7f6de3435e0334a808b9d2faa978c9cb70982509a376abc0e539cb44e1ed
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MyStorageAPI
|
|
@@ -93,11 +93,17 @@ Both produce identical asset records — `list` doesn't distinguish.
|
|
|
93
93
|
fetch it; there is no private mode, no signed URL, and no revocation. The id
|
|
94
94
|
being long and random is **not** access control — treat the URL as public the
|
|
95
95
|
moment it exists.
|
|
96
|
-
- **
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
- **Private assets exist as of 2026-07-28** — upload with `visibility: private`,
|
|
97
|
+
or `PATCH` an existing asset to close an exposure, which takes effect
|
|
98
|
+
immediately. Fetch it with a signed URL (15 minutes default, 24 hours max);
|
|
99
|
+
`revoke-links` kills every link already handed out, including unexpired ones.
|
|
100
|
+
A private upload deliberately returns **no plain `url`**, because that URL
|
|
101
|
+
does not serve the file and would look like the answer.
|
|
102
|
+
- **Encrypting before upload is still worth doing for the strictest cases.**
|
|
103
|
+
Private assets protect against the internet; client-side encryption protects
|
|
104
|
+
against the platform, and those are different threat models. A team shipping
|
|
105
|
+
Swiss lease documents used AES-256-GCM envelope encryption with a
|
|
106
|
+
per-document data key wrapped under a master key in function secrets.
|
|
101
107
|
- Delete is immediate and unrecoverable — run `myapi storage list` first to confirm the asset, pass `--org` explicitly, and pass `--yes` in non-interactive runs.
|
|
102
108
|
- The URL is permanent until you `myapi storage delete <id>` — embed it freely.
|
|
103
109
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.9.0",
|
|
5
5
|
"description": "MyAPI command-line interface",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"lint:skills:strict": "node scripts/copy-skills.js && node scripts/lint-skills.js --strict"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@myapihq/sdk": "^2.
|
|
49
|
+
"@myapihq/sdk": "^2.9.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^25.6.0",
|