stacks 0.62.0 → 0.63.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/buddy/build.ts +1 -0
- package/buddy/package.json +10 -9
- package/buddy/src/commands/cloud.ts +164 -10
- package/buddy/src/commands/configure.ts +2 -1
- package/buddy/src/commands/deploy.ts +55 -4
- package/buddy/src/commands/dev.ts +2 -2
- package/buddy/src/commands/make.ts +1 -2
- package/buddy/src/commands/migrate.ts +1 -1
- package/package.json +1 -1
package/buddy/build.ts
CHANGED
package/buddy/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stacksjs/buddy",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.63.0",
|
|
5
5
|
"description": "Meet Buddy. The Stacks runtime.",
|
|
6
6
|
"author": "Chris Breuer",
|
|
7
7
|
"license": "MIT",
|
|
@@ -50,24 +50,25 @@
|
|
|
50
50
|
},
|
|
51
51
|
"module": "dist/index.js",
|
|
52
52
|
"types": "dist/index.d.ts",
|
|
53
|
-
"contributors": [
|
|
54
|
-
"Chris Breuer <chris@stacksjs.org>"
|
|
55
|
-
],
|
|
53
|
+
"contributors": ["Chris Breuer <chris@stacksjs.org>"],
|
|
56
54
|
"bin": {
|
|
57
55
|
"stacks": "dist/cli.mjs",
|
|
58
56
|
"stx": "dist/cli.mjs",
|
|
59
57
|
"buddy": "dist/cli.mjs",
|
|
60
58
|
"bud": "dist/cli.mjs"
|
|
61
59
|
},
|
|
62
|
-
"files": [
|
|
63
|
-
"README.md",
|
|
64
|
-
"dist",
|
|
65
|
-
"src"
|
|
66
|
-
],
|
|
60
|
+
"files": ["README.md", "dist", "src"],
|
|
67
61
|
"scripts": {
|
|
68
62
|
"buddy": "bunx ./src/cli.ts",
|
|
69
63
|
"build": "bun --bun build.ts && bun run compile",
|
|
70
64
|
"compile": "bun build ./bin/cli.ts --compile --minify --sourcemap --outfile dist/stacks",
|
|
65
|
+
"compile:linux:x64-modern": "bun build ./bin/cli.ts --compile --target=bun-linux-x64-modern --minify --sourcemap --outfile dist/stacks",
|
|
66
|
+
"compile:linux:x64-baseline": "bun build ./bin/cli.ts --compile --target=bun-linux-x64-baseline --minify --sourcemap --outfile dist/stacks",
|
|
67
|
+
"compile:linux:arm64": "bun build ./bin/cli.ts --compile --target=bun-linux-arm64 --minify --sourcemap --outfile dist/stacks",
|
|
68
|
+
"compile:windows:x64-modern": "bun build ./bin/cli.ts --compile --target=bun-windows-x64-modern --minify --sourcemap --outfile dist/stacks",
|
|
69
|
+
"compile:windows:x64-baseline": "bun build ./bin/cli.ts --compile --target=bun-windows-x64-baseline --minify --sourcemap --outfile dist/stacks",
|
|
70
|
+
"compile:mac:arm64": "bun build ./bin/cli.ts --compile --target=bun-darwin-arm64 --minify --sourcemap --outfile dist/stacks",
|
|
71
|
+
"compile:mac:x64": "bun build ./bin/cli.ts --compile --target=bun-darwin-x64 --minify --sourcemap --outfile dist/stacks",
|
|
71
72
|
"typecheck": "bun --bun tsc --noEmit",
|
|
72
73
|
"prepublishOnly": "bun run build"
|
|
73
74
|
},
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import process from 'node:process'
|
|
2
|
-
import {
|
|
2
|
+
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront'
|
|
3
|
+
import { intro, italic, log, outro, prompts, runCommand, underline } from '@stacksjs/cli'
|
|
3
4
|
import {
|
|
4
5
|
addJumpBox,
|
|
5
6
|
deleteCdkRemnants,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
deleteParameterStore,
|
|
10
11
|
deleteStacksBuckets,
|
|
11
12
|
deleteStacksFunctions,
|
|
13
|
+
getCloudFrontDistributionId,
|
|
12
14
|
getJumpBoxInstanceId,
|
|
13
15
|
} from '@stacksjs/cloud'
|
|
14
16
|
import { path as p } from '@stacksjs/path'
|
|
@@ -21,9 +23,12 @@ export function cloud(buddy: CLI) {
|
|
|
21
23
|
cloud: 'Interact with the Stacks Cloud',
|
|
22
24
|
ssh: 'SSH into the Stacks Cloud',
|
|
23
25
|
add: 'Add a resource to the Stacks Cloud',
|
|
24
|
-
remove: '
|
|
25
|
-
optimizeCost: '
|
|
26
|
-
cleanUp: '
|
|
26
|
+
remove: 'Remove the Stacks Cloud. In case it fails, try again',
|
|
27
|
+
optimizeCost: 'Remove certain resources that may be re-applied at a later time',
|
|
28
|
+
cleanUp: 'Remove all resources that were retained during the cloud deletion',
|
|
29
|
+
invalidateCache: 'Invalidate the CloudFront cache',
|
|
30
|
+
diff: 'Show the diff of the current, undeployed cloud changes ',
|
|
31
|
+
paths: 'The paths to invalidate',
|
|
27
32
|
project: 'Target a specific project',
|
|
28
33
|
verbose: 'Enable verbose output',
|
|
29
34
|
}
|
|
@@ -32,13 +37,16 @@ export function cloud(buddy: CLI) {
|
|
|
32
37
|
.command('cloud', descriptions.cloud)
|
|
33
38
|
.option('--ssh', descriptions.ssh, { default: false })
|
|
34
39
|
.option('--connect', descriptions.ssh, { default: false })
|
|
40
|
+
.option('--invalidate-cache', descriptions.invalidateCache, { default: false })
|
|
41
|
+
.option('--paths [paths]', descriptions.paths)
|
|
42
|
+
.option('--diff', descriptions.diff, { default: false })
|
|
35
43
|
.option('-p, --project [project]', descriptions.project, { default: false })
|
|
36
44
|
.option('--verbose', descriptions.verbose, { default: false })
|
|
37
45
|
.action(async (options: CloudCliOptions) => {
|
|
38
46
|
log.debug('Running `buddy cloud` ...', options)
|
|
47
|
+
const startTime = performance.now()
|
|
39
48
|
|
|
40
49
|
if (options.ssh || options.connect) {
|
|
41
|
-
const startTime = performance.now()
|
|
42
50
|
const jumpBoxId = await getJumpBoxInstanceId()
|
|
43
51
|
const result = await runCommand(`aws ssm start-session --target ${jumpBoxId}`, {
|
|
44
52
|
...options,
|
|
@@ -59,7 +67,69 @@ export function cloud(buddy: CLI) {
|
|
|
59
67
|
process.exit(ExitCode.Success)
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
|
|
70
|
+
if (options.invalidateCache) {
|
|
71
|
+
const { confirm } = await prompts({
|
|
72
|
+
name: 'confirm',
|
|
73
|
+
type: 'confirm',
|
|
74
|
+
message: 'Would you like to invalidate the CDN (CloudFront) cache?',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (!confirm) {
|
|
78
|
+
await outro('Exited', { startTime, useSeconds: true })
|
|
79
|
+
process.exit(ExitCode.Success)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
log.info('Invalidating the CloudFront cache...')
|
|
83
|
+
|
|
84
|
+
const cloudfront = new CloudFrontClient()
|
|
85
|
+
const distributionId = await getCloudFrontDistributionId()
|
|
86
|
+
|
|
87
|
+
const params = {
|
|
88
|
+
DistributionId: distributionId,
|
|
89
|
+
InvalidationBatch: {
|
|
90
|
+
CallerReference: `${Date.now()}`,
|
|
91
|
+
Paths: {
|
|
92
|
+
Quantity: 1,
|
|
93
|
+
Items: [
|
|
94
|
+
'/*',
|
|
95
|
+
/* more items */
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const command = new CreateInvalidationCommand(params)
|
|
102
|
+
|
|
103
|
+
cloudfront.send(command).then(
|
|
104
|
+
(data) => console.log(data),
|
|
105
|
+
(err) => console.log(err, err.stack),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
await outro('Exited', { startTime, useSeconds: true })
|
|
109
|
+
process.exit(ExitCode.Success)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (options.diff) {
|
|
113
|
+
const result = await runCommand('npx cdk diff', {
|
|
114
|
+
cwd: p.frameworkCloudPath(),
|
|
115
|
+
stdin: 'pipe',
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
if (result.isErr()) {
|
|
119
|
+
await outro(
|
|
120
|
+
'While running the cloud diff command, there was an issue',
|
|
121
|
+
{ startTime, useSeconds: true },
|
|
122
|
+
result.error,
|
|
123
|
+
)
|
|
124
|
+
process.exit(ExitCode.FatalError)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await outro('Showing diff of the current, undeployed cloud changes', { startTime, useSeconds: true })
|
|
128
|
+
console.log(result.value)
|
|
129
|
+
process.exit(ExitCode.Success)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
log.info('Not implemented yet. Read more about `buddy cloud` here: https://stacksjs.org/docs/cloud')
|
|
63
133
|
process.exit(ExitCode.Success)
|
|
64
134
|
})
|
|
65
135
|
|
|
@@ -168,9 +238,9 @@ export function cloud(buddy: CLI) {
|
|
|
168
238
|
// sleep for 2 seconds to get the user to read the message
|
|
169
239
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
170
240
|
|
|
171
|
-
const result = await runCommand(`
|
|
241
|
+
const result = await runCommand(`npx cdk destroy`, {
|
|
172
242
|
...options,
|
|
173
|
-
cwd: p.
|
|
243
|
+
cwd: p.frameworkCloudPath(),
|
|
174
244
|
stdin: 'inherit',
|
|
175
245
|
})
|
|
176
246
|
|
|
@@ -196,8 +266,9 @@ export function cloud(buddy: CLI) {
|
|
|
196
266
|
try {
|
|
197
267
|
log.info('Finalizing the removal of your cloud resources.')
|
|
198
268
|
log.info('This will take a few moments...')
|
|
199
|
-
|
|
200
|
-
|
|
269
|
+
// sometimes, this fails, so we need to retry it until all resources are deleted -> weird workaround, and would love a more stable alternative
|
|
270
|
+
await loop(7, async () => {
|
|
271
|
+
await runCommand('buddy cloud:cleanup', {
|
|
201
272
|
...options,
|
|
202
273
|
cwd: p.projectPath(),
|
|
203
274
|
stdout: 'ignore',
|
|
@@ -373,6 +444,89 @@ export function cloud(buddy: CLI) {
|
|
|
373
444
|
process.exit(ExitCode.Success)
|
|
374
445
|
})
|
|
375
446
|
|
|
447
|
+
buddy
|
|
448
|
+
.command('cloud:invalidate-cache', descriptions.invalidateCache)
|
|
449
|
+
.option('--paths [paths]', descriptions.paths, { default: false })
|
|
450
|
+
.option('-p, --project [project]', descriptions.project, { default: false })
|
|
451
|
+
.option('--verbose', descriptions.verbose, { default: false })
|
|
452
|
+
.action(async (options: CloudCliOptions) => {
|
|
453
|
+
log.debug('Running `buddy cloud:invalidate-cache` ...', options)
|
|
454
|
+
|
|
455
|
+
const startTime = await intro('buddy cloud:invalidate-cache')
|
|
456
|
+
|
|
457
|
+
const { confirm } = await prompts({
|
|
458
|
+
name: 'confirm',
|
|
459
|
+
type: 'confirm',
|
|
460
|
+
message: 'Would you like to invalidate the CloudFront cache?',
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
if (!confirm) {
|
|
464
|
+
await outro('Exited', { startTime, useSeconds: true })
|
|
465
|
+
process.exit(ExitCode.Success)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
log.info('Invalidating the CloudFront cache...')
|
|
469
|
+
// const result = await runCommand('aws cloudfront create-invalidation --distribution-id E1U4Z2E9NJW9J --paths "/*"', {
|
|
470
|
+
// ...options,
|
|
471
|
+
// cwd: p.projectPath(),
|
|
472
|
+
// stdin: 'pipe',
|
|
473
|
+
// })
|
|
474
|
+
|
|
475
|
+
if (options.paths) {
|
|
476
|
+
const result = await runCommand(
|
|
477
|
+
`aws cloudfront create-invalidation --distribution-id E1U4Z2E9NJW9J --paths ${options.paths}`,
|
|
478
|
+
{
|
|
479
|
+
...options,
|
|
480
|
+
cwd: p.projectPath(), // TODO: this should be the cloud path
|
|
481
|
+
stdin: 'pipe',
|
|
482
|
+
},
|
|
483
|
+
) // TODO: this should be the cloud path
|
|
484
|
+
|
|
485
|
+
if (result.isErr()) {
|
|
486
|
+
await outro(
|
|
487
|
+
'While running the cloud command, there was an issue',
|
|
488
|
+
{ startTime, useSeconds: true },
|
|
489
|
+
result.error,
|
|
490
|
+
)
|
|
491
|
+
process.exit(ExitCode.FatalError)
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
await outro('Exited', { startTime, useSeconds: true })
|
|
495
|
+
process.exit(ExitCode.Success)
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
log.info('Not implemented yet. Read more about `buddy cloud` here: https://stacksjs.org/docs/cloud')
|
|
499
|
+
process.exit(ExitCode.Success)
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
buddy
|
|
503
|
+
.command('cloud:diff', descriptions.diff)
|
|
504
|
+
.option('-p, --project [project]', descriptions.project, { default: false })
|
|
505
|
+
.option('--verbose', descriptions.verbose, { default: false })
|
|
506
|
+
.action(async (options: CloudCliOptions) => {
|
|
507
|
+
log.debug('Running `buddy cloud:diff` ...', options)
|
|
508
|
+
|
|
509
|
+
const startTime = await intro('buddy cloud:diff')
|
|
510
|
+
|
|
511
|
+
const result = await runCommand('npx cdk diff', {
|
|
512
|
+
cwd: p.frameworkCloudPath(),
|
|
513
|
+
stdin: 'pipe',
|
|
514
|
+
})
|
|
515
|
+
|
|
516
|
+
if (result.isErr()) {
|
|
517
|
+
await outro(
|
|
518
|
+
'While running the cloud diff command, there was an issue',
|
|
519
|
+
{ startTime, useSeconds: true },
|
|
520
|
+
result.error,
|
|
521
|
+
)
|
|
522
|
+
process.exit(ExitCode.FatalError)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
await outro('Showing diff of the current, undeployed cloud changes', { startTime, useSeconds: true })
|
|
526
|
+
console.log(result.value)
|
|
527
|
+
process.exit(ExitCode.Success)
|
|
528
|
+
})
|
|
529
|
+
|
|
376
530
|
buddy.on('cloud:*', () => {
|
|
377
531
|
console.error('Invalid command: %s\nSee --help for a list of available commands.', buddy.args.join(' '))
|
|
378
532
|
process.exit(1)
|
|
@@ -62,7 +62,8 @@ async function configureAws(options?: ConfigureOptions) {
|
|
|
62
62
|
const defaultRegion = 'us-east-1' // we only support `us-east-1` for now
|
|
63
63
|
const defaultOutputFormat = options?.output ?? 'json'
|
|
64
64
|
|
|
65
|
-
const
|
|
65
|
+
const profile = process.env.AWS_PROFILE ?? options?.profile
|
|
66
|
+
const command = profile ? `aws configure --profile ${profile}` : `aws configure`
|
|
66
67
|
const input = `${awsAccessKeyId}\n${awsSecretAccessKey}\n${defaultRegion}\n${defaultOutputFormat}\n`
|
|
67
68
|
|
|
68
69
|
const result = await runCommand(command, {
|
|
@@ -4,27 +4,38 @@ import { intro, italic, log, outro, runCommand } from '@stacksjs/cli'
|
|
|
4
4
|
import { app } from '@stacksjs/config'
|
|
5
5
|
import { addDomain, hasUserDomainBeenAddedToCloud } from '@stacksjs/dns'
|
|
6
6
|
import { Action } from '@stacksjs/enums'
|
|
7
|
+
import { path as p } from '@stacksjs/path'
|
|
7
8
|
import { ExitCode } from '@stacksjs/types'
|
|
8
9
|
import type { CLI, DeployOptions } from '@stacksjs/types'
|
|
10
|
+
import { $ } from 'bun'
|
|
9
11
|
|
|
10
12
|
export function deploy(buddy: CLI) {
|
|
11
13
|
const descriptions = {
|
|
12
|
-
deploy: '
|
|
14
|
+
deploy: 'Deploy your project',
|
|
13
15
|
project: 'Target a specific project',
|
|
16
|
+
production: 'Deploy to production',
|
|
17
|
+
development: 'Deploy to development',
|
|
18
|
+
staging: 'Deploy to staging',
|
|
19
|
+
domain: 'Specify a domain to deploy to',
|
|
14
20
|
verbose: 'Enable verbose output',
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
buddy
|
|
18
|
-
.command('deploy', descriptions.deploy)
|
|
19
|
-
.option('--domain',
|
|
24
|
+
.command('deploy [env]', descriptions.deploy)
|
|
25
|
+
.option('--domain', descriptions.domain, { default: undefined })
|
|
20
26
|
.option('-p, --project [project]', descriptions.project, { default: false })
|
|
27
|
+
.option('--prod', descriptions.production, { default: true })
|
|
28
|
+
.option('--dev', descriptions.development, { default: false })
|
|
29
|
+
.option('--staging', descriptions.staging, { default: false })
|
|
21
30
|
.option('--verbose', descriptions.verbose, { default: false })
|
|
22
|
-
.action(async (options: DeployOptions) => {
|
|
31
|
+
.action(async (env: string | undefined, options: DeployOptions) => {
|
|
23
32
|
log.debug('Running `buddy deploy` ...', options)
|
|
24
33
|
|
|
25
34
|
const startTime = await intro('buddy deploy')
|
|
26
35
|
const domain = options.domain || app.url
|
|
27
36
|
|
|
37
|
+
if (options.prod || env === 'production' || env === 'prod') await confirmProductionDeployment()
|
|
38
|
+
|
|
28
39
|
if (!domain) {
|
|
29
40
|
log.info('No domain found in your .env or ./config/app.ts')
|
|
30
41
|
log.info('Please ensure your domain is properly configured.')
|
|
@@ -35,6 +46,7 @@ export function deploy(buddy: CLI) {
|
|
|
35
46
|
log.info(`Deploying to ${italic(domain)}`)
|
|
36
47
|
|
|
37
48
|
await checkIfAwsIsConfigured()
|
|
49
|
+
await checkIfAwsIsBootstrapped()
|
|
38
50
|
|
|
39
51
|
options.domain = await configureDomain(domain, options, startTime)
|
|
40
52
|
|
|
@@ -58,6 +70,15 @@ export function deploy(buddy: CLI) {
|
|
|
58
70
|
})
|
|
59
71
|
}
|
|
60
72
|
|
|
73
|
+
async function confirmProductionDeployment() {
|
|
74
|
+
const answer = await log.prompt.require().confirm('Are you sure you want to deploy to production?')
|
|
75
|
+
|
|
76
|
+
if (!answer) {
|
|
77
|
+
log.info('Aborting deployment...')
|
|
78
|
+
process.exit(ExitCode.InvalidArgument)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
61
82
|
async function configureDomain(domain: string, options: DeployOptions, startTime: number) {
|
|
62
83
|
if (!domain) {
|
|
63
84
|
log.info('We could not identify a domain to deploy to.')
|
|
@@ -130,3 +151,33 @@ async function checkIfAwsIsConfigured() {
|
|
|
130
151
|
|
|
131
152
|
log.success('AWS is configured')
|
|
132
153
|
}
|
|
154
|
+
|
|
155
|
+
async function checkIfAwsIsBootstrapped() {
|
|
156
|
+
try {
|
|
157
|
+
log.info('Ensuring AWS is bootstrapped...')
|
|
158
|
+
// const toolkitName = 'stacks-toolkit'
|
|
159
|
+
await runCommand('aws cloudformation describe-stacks --stack-name stacks-toolkit', { silent: true })
|
|
160
|
+
// await $`aws cloudformation describe-stacks --stack-name stacks-toolkit`.quiet()
|
|
161
|
+
log.success('AWS is bootstrapped')
|
|
162
|
+
|
|
163
|
+
return true
|
|
164
|
+
} catch (err: any) {
|
|
165
|
+
log.debug(`Not yet bootstrapped. Failed with code ${err.exitCode}`)
|
|
166
|
+
log.debug(err.stdout.toString())
|
|
167
|
+
log.debug(err.stderr.toString())
|
|
168
|
+
|
|
169
|
+
log.info('AWS is not bootstrapped yet')
|
|
170
|
+
log.info('Bootstrapping. This may take a few moments...')
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
$.cwd(p.frameworkCloudPath())
|
|
174
|
+
const result = await $`bun run bootstrap`
|
|
175
|
+
console.log(result)
|
|
176
|
+
return true
|
|
177
|
+
} catch (error) {
|
|
178
|
+
log.error('Failed to bootstrap AWS')
|
|
179
|
+
console.error(error)
|
|
180
|
+
process.exit(ExitCode.FatalError)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -17,8 +17,8 @@ import type { CLI, DevOptions } from '@stacksjs/types'
|
|
|
17
17
|
|
|
18
18
|
export function dev(buddy: CLI) {
|
|
19
19
|
const descriptions = {
|
|
20
|
-
dev: '
|
|
21
|
-
frontend: '
|
|
20
|
+
dev: 'Start development server',
|
|
21
|
+
frontend: 'Start the frontend development server',
|
|
22
22
|
components: 'Start the Components development server',
|
|
23
23
|
desktop: 'Start the Desktop App development server',
|
|
24
24
|
dashboard: 'Start the Dashboard development server',
|
|
@@ -327,12 +327,11 @@ export function make(buddy: CLI) {
|
|
|
327
327
|
log.debug('Running `buddy make:certificate` ...', options)
|
|
328
328
|
|
|
329
329
|
const domain = await localUrl()
|
|
330
|
-
log.info(`Creating SSL certificate...`)
|
|
331
330
|
|
|
331
|
+
log.info(`Creating SSL certificate...`)
|
|
332
332
|
await runCommand(`tlsx ${domain}`, {
|
|
333
333
|
cwd: p.projectStoragePath('keys'),
|
|
334
334
|
})
|
|
335
|
-
|
|
336
335
|
log.success('Certificate created')
|
|
337
336
|
|
|
338
337
|
log.info(`Installing SSL certificate...`)
|