suthep 0.1.0 → 0.2.0-beta.1

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 (72) hide show
  1. package/README.md +172 -71
  2. package/dist/commands/deploy.js +251 -37
  3. package/dist/commands/deploy.js.map +1 -1
  4. package/dist/commands/down.js +179 -0
  5. package/dist/commands/down.js.map +1 -0
  6. package/dist/commands/redeploy.js +59 -0
  7. package/dist/commands/redeploy.js.map +1 -0
  8. package/dist/commands/up.js +213 -0
  9. package/dist/commands/up.js.map +1 -0
  10. package/dist/index.js +36 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/utils/certbot.js +40 -3
  13. package/dist/utils/certbot.js.map +1 -1
  14. package/dist/utils/config-loader.js +30 -0
  15. package/dist/utils/config-loader.js.map +1 -1
  16. package/dist/utils/deployment.js +49 -16
  17. package/dist/utils/deployment.js.map +1 -1
  18. package/dist/utils/docker.js +396 -25
  19. package/dist/utils/docker.js.map +1 -1
  20. package/dist/utils/nginx.js +167 -8
  21. package/dist/utils/nginx.js.map +1 -1
  22. package/docs/README.md +25 -49
  23. package/docs/english/01-introduction.md +84 -0
  24. package/docs/english/02-installation.md +200 -0
  25. package/docs/english/03-quick-start.md +256 -0
  26. package/docs/english/04-configuration.md +358 -0
  27. package/docs/english/05-commands.md +363 -0
  28. package/docs/english/06-examples.md +456 -0
  29. package/docs/english/07-troubleshooting.md +417 -0
  30. package/docs/english/08-advanced.md +411 -0
  31. package/docs/english/README.md +48 -0
  32. package/docs/thai/01-introduction.md +84 -0
  33. package/docs/thai/02-installation.md +200 -0
  34. package/docs/thai/03-quick-start.md +256 -0
  35. package/docs/thai/04-configuration.md +358 -0
  36. package/docs/thai/05-commands.md +363 -0
  37. package/docs/thai/06-examples.md +456 -0
  38. package/docs/thai/07-troubleshooting.md +417 -0
  39. package/docs/thai/08-advanced.md +411 -0
  40. package/docs/thai/README.md +48 -0
  41. package/example/README.md +286 -53
  42. package/example/suthep-complete.yml +103 -0
  43. package/example/suthep-docker-only.yml +71 -0
  44. package/example/suthep-no-docker.yml +51 -0
  45. package/example/suthep-path-routing.yml +62 -0
  46. package/example/suthep.example.yml +89 -0
  47. package/package.json +1 -1
  48. package/src/commands/deploy.ts +322 -50
  49. package/src/commands/down.ts +240 -0
  50. package/src/commands/redeploy.ts +78 -0
  51. package/src/commands/up.ts +271 -0
  52. package/src/index.ts +62 -1
  53. package/src/types/config.ts +25 -24
  54. package/src/utils/certbot.ts +68 -6
  55. package/src/utils/config-loader.ts +40 -0
  56. package/src/utils/deployment.ts +61 -36
  57. package/src/utils/docker.ts +634 -30
  58. package/src/utils/nginx.ts +187 -4
  59. package/suthep-0.1.0-beta.1.tgz +0 -0
  60. package/suthep-0.1.1.tgz +0 -0
  61. package/suthep.example.yml +34 -0
  62. package/suthep.yml +39 -0
  63. package/test +0 -0
  64. package/docs/api-reference.md +0 -545
  65. package/docs/architecture.md +0 -367
  66. package/docs/commands.md +0 -273
  67. package/docs/configuration.md +0 -347
  68. package/docs/examples.md +0 -537
  69. package/docs/getting-started.md +0 -197
  70. package/docs/troubleshooting.md +0 -441
  71. package/example/docker-compose.yml +0 -72
  72. package/example/suthep.yml +0 -31
@@ -1,367 +0,0 @@
1
- # Architecture
2
-
3
- This document explains how Suthep works under the hood, including its components, deployment strategies, and internal architecture.
4
-
5
- ## Overview
6
-
7
- Suthep is a deployment automation tool that orchestrates multiple components to provide a seamless deployment experience:
8
-
9
- ```
10
- ┌─────────────────────────────────────────────────────────┐
11
- │ Suthep CLI Tool │
12
- │ (init, setup, deploy commands) │
13
- └─────────────────────────────────────────────────────────┘
14
-
15
-
16
- ┌─────────────────────────────────────┐
17
- │ Configuration Loader │
18
- │ (Loads and validates suthep.yml) │
19
- └─────────────────────────────────────┘
20
-
21
- ┌─────────────────┴─────────────────┐
22
- │ │
23
- ▼ ▼
24
- ┌───────────────┐ ┌──────────────┐
25
- │ Docker │ │ Nginx │
26
- │ Manager │ │ Manager │
27
- └───────────────┘ └──────────────┘
28
- │ │
29
- │ ▼
30
- │ ┌──────────────┐
31
- │ │ Certbot │
32
- │ │ Manager │
33
- │ └──────────────┘
34
- │ │
35
- └───────────────────────────────────┘
36
-
37
-
38
- ┌─────────────────┐
39
- │ Deployment │
40
- │ Strategy │
41
- │ (Rolling/ │
42
- │ Blue-Green) │
43
- └─────────────────┘
44
- ```
45
-
46
- ## Core Components
47
-
48
- ### 1. Configuration System
49
-
50
- **File**: `src/utils/config-loader.ts`
51
-
52
- The configuration system:
53
- - Loads YAML configuration files
54
- - Validates configuration structure
55
- - Provides type-safe configuration objects
56
-
57
- **Type Definitions**: `src/types/config.ts`
58
-
59
- All configuration types are defined using TypeScript interfaces:
60
- - `DeployConfig`: Root configuration object
61
- - `ServiceConfig`: Individual service configuration
62
- - `DockerConfig`: Docker-specific settings
63
- - `HealthCheckConfig`: Health check settings
64
- - `NginxConfig`: Nginx settings
65
- - `CertbotConfig`: SSL certificate settings
66
- - `DeploymentConfig`: Deployment strategy settings
67
-
68
- ### 2. Docker Manager
69
-
70
- **File**: `src/utils/docker.ts`
71
-
72
- The Docker manager handles:
73
- - **Container lifecycle**: Start, stop, remove containers
74
- - **Image management**: Pull and run Docker images
75
- - **Port mapping**: Maps host ports to container ports
76
- - **Environment variables**: Injects environment variables into containers
77
- - **Container inspection**: Check container status and logs
78
-
79
- **Key Functions**:
80
- - `startDockerContainer()`: Starts or connects to a container
81
- - `stopDockerContainer()`: Stops a running container
82
- - `isContainerRunning()`: Checks if a container is running
83
- - `getContainerLogs()`: Retrieves container logs
84
-
85
- **Container Logic**:
86
- 1. Check if container exists
87
- 2. If exists and running: Use existing container
88
- 3. If exists but stopped: Start the container
89
- 4. If doesn't exist and image provided: Create and run new container
90
- 5. If doesn't exist and no image: Error (connect to existing only)
91
-
92
- ### 3. Nginx Manager
93
-
94
- **File**: `src/utils/nginx.ts`
95
-
96
- The Nginx manager:
97
- - **Configuration generation**: Creates Nginx server blocks
98
- - **Site management**: Enables/disables Nginx sites
99
- - **Configuration reload**: Tests and reloads Nginx
100
-
101
- **Generated Configuration Structure**:
102
-
103
- ```nginx
104
- # Upstream definition
105
- upstream service_backend {
106
- server localhost:PORT;
107
- keepalive 32;
108
- }
109
-
110
- # HTTP server (redirects to HTTPS if SSL enabled)
111
- server {
112
- listen 80;
113
- server_name domain1.com domain2.com;
114
- return 301 https://$server_name$request_uri;
115
- }
116
-
117
- # HTTPS server
118
- server {
119
- listen 443 ssl http2;
120
- server_name domain1.com domain2.com;
121
-
122
- # SSL configuration
123
- ssl_certificate /etc/letsencrypt/live/domain/fullchain.pem;
124
- ssl_certificate_key /etc/letsencrypt/live/domain/privkey.pem;
125
-
126
- # Proxy settings
127
- location / {
128
- proxy_pass http://service_backend;
129
- proxy_set_header Host $host;
130
- proxy_set_header X-Real-IP $remote_addr;
131
- # ... more proxy headers
132
- }
133
-
134
- # Health check endpoint
135
- location /health {
136
- proxy_pass http://service_backend;
137
- access_log off;
138
- }
139
- }
140
- ```
141
-
142
- **Key Functions**:
143
- - `generateNginxConfig()`: Creates Nginx configuration
144
- - `enableSite()`: Creates symlink in sites-enabled
145
- - `reloadNginx()`: Tests and reloads Nginx
146
- - `disableSite()`: Removes site from sites-enabled
147
-
148
- ### 4. Certbot Manager
149
-
150
- **File**: `src/utils/certbot.ts`
151
-
152
- The Certbot manager handles SSL certificate operations:
153
- - **Certificate requests**: Obtains certificates from Let's Encrypt
154
- - **Certificate renewal**: Renews expiring certificates
155
- - **Certificate revocation**: Revokes certificates
156
- - **Certificate inspection**: Checks certificate expiration
157
-
158
- **Key Functions**:
159
- - `requestCertificate()`: Requests SSL certificate for a domain
160
- - `renewCertificates()`: Renews all certificates
161
- - `checkCertificateExpiration()`: Checks when certificate expires
162
- - `revokeCertificate()`: Revokes a certificate
163
-
164
- **Certificate Request Process**:
165
- 1. Run `certbot certonly --nginx` for each domain
166
- 2. Certbot validates domain ownership
167
- 3. Certbot obtains and stores certificate
168
- 4. Nginx configuration updated with certificate paths
169
-
170
- ### 5. Deployment Strategy
171
-
172
- **File**: `src/utils/deployment.ts`
173
-
174
- Suthep supports two deployment strategies:
175
-
176
- #### Rolling Deployment
177
-
178
- Gradually replaces old instances with new ones:
179
-
180
- 1. Start new service instance
181
- 2. Wait for health check to pass
182
- 3. Switch traffic to new instance
183
- 4. Stop old instance (if applicable)
184
-
185
- **Current Implementation**: Simplified - assumes service is already running and verifies health before proceeding.
186
-
187
- #### Blue-Green Deployment
188
-
189
- Maintains two identical environments:
190
-
191
- 1. Deploy to "green" environment
192
- 2. Run health checks on green
193
- 3. Switch router/load balancer to green
194
- 4. Keep blue as backup
195
-
196
- **Current Implementation**: Simplified - verifies service health before proceeding.
197
-
198
- **Key Functions**:
199
- - `deployService()`: Main deployment function (routes to strategy)
200
- - `rollingDeploy()`: Rolling deployment implementation
201
- - `blueGreenDeploy()`: Blue-green deployment implementation
202
- - `performHealthCheck()`: Checks service health endpoint
203
- - `waitForService()`: Waits for service to become healthy
204
-
205
- ### 6. Health Check System
206
-
207
- **File**: `src/utils/deployment.ts`
208
-
209
- Health checks ensure services are ready before routing traffic:
210
-
211
- 1. **Polling**: Checks health endpoint every 2 seconds
212
- 2. **Timeout**: Stops checking after configured timeout
213
- 3. **Validation**: Requires HTTP 200 response
214
- 4. **Failure**: Throws error if service doesn't become healthy
215
-
216
- **Health Check Flow**:
217
- ```
218
- Start deployment
219
-
220
- Wait 2 seconds
221
-
222
- GET /health endpoint
223
-
224
- Is response 200 OK?
225
- ├─ Yes → Service healthy ✅
226
- └─ No → Wait 2 seconds, retry
227
-
228
- Timeout exceeded?
229
- ├─ No → Retry
230
- └─ Yes → Deployment failed ❌
231
- ```
232
-
233
- ## Command Implementations
234
-
235
- ### `init` Command
236
-
237
- **File**: `src/commands/init.ts`
238
-
239
- Interactive configuration creation:
240
- 1. Check if file exists (prompt for overwrite)
241
- 2. Gather project information
242
- 3. Loop through services:
243
- - Service details
244
- - Docker configuration
245
- - Health check configuration
246
- 4. Gather Certbot information
247
- 5. Build configuration object
248
- 6. Save to YAML file
249
-
250
- ### `setup` Command
251
-
252
- **File**: `src/commands/setup.ts`
253
-
254
- Prerequisites installation:
255
- 1. Detect operating system
256
- 2. Check if Nginx installed → Install if needed
257
- 3. Start and enable Nginx service
258
- 4. Check if Certbot installed → Install if needed
259
- 5. Verify installations
260
-
261
- ### `deploy` Command
262
-
263
- **File**: `src/commands/deploy.ts`
264
-
265
- Main deployment orchestration:
266
- 1. Load and validate configuration
267
- 2. For each service:
268
- a. Start Docker container (if configured)
269
- b. Deploy service (using strategy)
270
- c. Generate Nginx config (HTTP)
271
- d. Enable Nginx site
272
- e. Request SSL certificates (if HTTPS enabled)
273
- f. Update Nginx config (HTTPS)
274
- g. Reload Nginx
275
- h. Perform health check
276
- 3. Print service URLs
277
-
278
- ## Data Flow
279
-
280
- ### Deployment Flow
281
-
282
- ```
283
- User runs: suthep deploy
284
-
285
- Load suthep.yml
286
-
287
- For each service:
288
- ├─ Start Docker container
289
- ├─ Deploy service (strategy)
290
- ├─ Generate Nginx config
291
- ├─ Enable Nginx site
292
- ├─ Request SSL certificate
293
- ├─ Update Nginx config (HTTPS)
294
- ├─ Reload Nginx
295
- └─ Health check
296
-
297
- All services deployed ✅
298
- ```
299
-
300
- ### Configuration Flow
301
-
302
- ```
303
- suthep.yml (YAML)
304
-
305
- config-loader.ts (Load & Parse)
306
-
307
- TypeScript Types (Validate)
308
-
309
- Command Handlers (Use)
310
-
311
- Utility Functions (Execute)
312
- ```
313
-
314
- ## File System Structure
315
-
316
- Suthep creates and manages:
317
-
318
- ```
319
- /etc/nginx/
320
- ├── sites-available/
321
- │ ├── service1.conf (Generated)
322
- │ └── service2.conf (Generated)
323
- └── sites-enabled/
324
- ├── service1.conf (Symlink)
325
- └── service2.conf (Symlink)
326
-
327
- /etc/letsencrypt/
328
- ├── live/
329
- │ ├── domain1.com/
330
- │ │ ├── fullchain.pem
331
- │ │ └── privkey.pem
332
- │ └── domain2.com/
333
- │ ├── fullchain.pem
334
- │ └── privkey.pem
335
- └── archive/ (Certificate history)
336
- ```
337
-
338
- ## Error Handling
339
-
340
- Suthep uses a fail-fast approach:
341
- - If any step fails, deployment stops
342
- - Detailed error messages are shown
343
- - Exit code 1 indicates failure
344
- - Previous successful steps remain in place
345
-
346
- ## Security Considerations
347
-
348
- 1. **sudo Access**: Required for Nginx and Certbot operations
349
- 2. **SSL Certificates**: Stored in `/etc/letsencrypt/` (root access required)
350
- 3. **Nginx Configs**: Written to system directories (sudo required)
351
- 4. **Docker**: Runs with user permissions (no sudo needed)
352
-
353
- ## Extensibility
354
-
355
- Suthep is designed to be extensible:
356
-
357
- 1. **New Deployment Strategies**: Add to `deployment.ts`
358
- 2. **New Service Types**: Extend `ServiceConfig` type
359
- 3. **Custom Health Checks**: Modify `performHealthCheck()`
360
- 4. **Additional Providers**: Add new managers (e.g., Traefik, Caddy)
361
-
362
- ## Performance Considerations
363
-
364
- 1. **Parallel Deployment**: Currently sequential (can be parallelized)
365
- 2. **Health Check Polling**: 2-second intervals (configurable)
366
- 3. **Nginx Reload**: Only once per service (could batch)
367
- 4. **Certificate Requests**: Sequential per domain (rate limits apply)
package/docs/commands.md DELETED
@@ -1,273 +0,0 @@
1
- # Commands Reference
2
-
3
- Suthep provides three main commands for managing deployments. This document describes each command in detail.
4
-
5
- ## `suthep init`
6
-
7
- Initialize a new deployment configuration file.
8
-
9
- ### Usage
10
-
11
- ```bash
12
- suthep init [options]
13
- ```
14
-
15
- ### Options
16
-
17
- | Option | Short | Description | Default |
18
- |--------|-------|-------------|---------|
19
- | `--file` | `-f` | Configuration file path | `suthep.yml` |
20
-
21
- ### Description
22
-
23
- The `init` command interactively creates a `suthep.yml` configuration file. It will:
24
-
25
- 1. Prompt for project name and version
26
- 2. Guide you through configuring one or more services
27
- 3. Ask about Docker configuration for each service
28
- 4. Set up health check endpoints
29
- 5. Configure Certbot email and staging mode
30
- 6. Save the configuration to the specified file
31
-
32
- ### Examples
33
-
34
- ```bash
35
- # Create default suthep.yml
36
- suthep init
37
-
38
- # Create custom configuration file
39
- suthep init -f production.yml
40
- ```
41
-
42
- ### Interactive Prompts
43
-
44
- The command will ask you:
45
-
46
- 1. **Project name**: Name of your project
47
- 2. **Project version**: Version number
48
- 3. **Service name**: Unique identifier for the service
49
- 4. **Service port**: Port the service runs on (1-65535)
50
- 5. **Domain names**: Comma-separated list of domains
51
- 6. **Use Docker?**: Whether to use Docker
52
- 7. **Docker image**: Image to use (optional, for new containers)
53
- 8. **Container name**: Name of the Docker container
54
- 9. **Container port**: Port inside the container
55
- 10. **Add health check?**: Whether to configure health checks
56
- 11. **Health check path**: HTTP path for health endpoint
57
- 12. **Health check interval**: Check interval in seconds
58
- 13. **Add another service?**: Whether to configure more services
59
- 14. **Email for SSL certificates**: Email for Let's Encrypt
60
- 15. **Use Certbot staging?**: Use staging environment for testing
61
-
62
- ## `suthep setup`
63
-
64
- Setup Nginx and Certbot on the system.
65
-
66
- ### Usage
67
-
68
- ```bash
69
- suthep setup [options]
70
- ```
71
-
72
- ### Options
73
-
74
- | Option | Description |
75
- |--------|-------------|
76
- | `--nginx-only` | Only setup Nginx (skip Certbot) |
77
- | `--certbot-only` | Only setup Certbot (skip Nginx) |
78
-
79
- ### Description
80
-
81
- The `setup` command installs and configures the prerequisites for Suthep:
82
-
83
- 1. **Nginx Installation**:
84
- - Checks if Nginx is already installed
85
- - Installs Nginx using the appropriate package manager:
86
- - `apt-get` on Debian/Ubuntu
87
- - `yum` on RHEL/CentOS
88
- - `brew` on macOS
89
- - Starts and enables the Nginx service
90
-
91
- 2. **Certbot Installation**:
92
- - Checks if Certbot is already installed
93
- - Installs Certbot and the Nginx plugin:
94
- - `certbot` and `python3-certbot-nginx` on Linux
95
- - `certbot` on macOS via Homebrew
96
-
97
- ### Examples
98
-
99
- ```bash
100
- # Setup both Nginx and Certbot
101
- suthep setup
102
-
103
- # Only setup Nginx
104
- suthep setup --nginx-only
105
-
106
- # Only setup Certbot
107
- suthep setup --certbot-only
108
- ```
109
-
110
- ### Platform Support
111
-
112
- - **Linux**: Supports Debian/Ubuntu (apt-get) and RHEL/CentOS (yum)
113
- - **macOS**: Uses Homebrew for installation
114
- - **Other platforms**: Will show an error message with manual installation instructions
115
-
116
- ### Requirements
117
-
118
- - `sudo` access (for installing system packages)
119
- - Internet connection (to download packages)
120
-
121
- ## `suthep deploy`
122
-
123
- Deploy a project using the configuration file.
124
-
125
- ### Usage
126
-
127
- ```bash
128
- suthep deploy [options]
129
- ```
130
-
131
- ### Options
132
-
133
- | Option | Short | Description | Default |
134
- |--------|-------|-------------|---------|
135
- | `--file` | `-f` | Configuration file path | `suthep.yml` |
136
- | `--no-https` | | Skip HTTPS setup | `false` |
137
- | `--no-nginx` | | Skip Nginx configuration | `false` |
138
-
139
- ### Description
140
-
141
- The `deploy` command is the main deployment command. It performs the following steps for each service:
142
-
143
- 1. **Load Configuration**: Reads and validates the configuration file
144
- 2. **Docker Management**: Starts or connects to Docker containers (if configured)
145
- 3. **Service Deployment**: Deploys the service using the configured strategy
146
- 4. **Nginx Configuration**: Generates and enables Nginx reverse proxy configuration
147
- 5. **SSL Certificate**: Requests SSL certificates from Let's Encrypt (if HTTPS enabled)
148
- 6. **Nginx Reload**: Tests and reloads Nginx configuration
149
- 7. **Health Check**: Performs health check to verify service is running
150
-
151
- ### Deployment Flow
152
-
153
- For each service in your configuration:
154
-
155
- ```
156
- 1. Start Docker container (if configured)
157
-
158
- 2. Deploy service (rolling or blue-green strategy)
159
-
160
- 3. Generate Nginx configuration (HTTP)
161
-
162
- 4. Enable Nginx site
163
-
164
- 5. Request SSL certificates (if --no-https not used)
165
-
166
- 6. Update Nginx configuration (HTTPS)
167
-
168
- 7. Reload Nginx
169
-
170
- 8. Perform health check (if configured)
171
-
172
- 9. Service deployed successfully!
173
- ```
174
-
175
- ### Examples
176
-
177
- ```bash
178
- # Deploy using default suthep.yml
179
- suthep deploy
180
-
181
- # Deploy using custom configuration file
182
- suthep deploy -f production.yml
183
-
184
- # Deploy without HTTPS (HTTP only)
185
- suthep deploy --no-https
186
-
187
- # Deploy without Nginx (only Docker/health checks)
188
- suthep deploy --no-nginx
189
-
190
- # Deploy without both HTTPS and Nginx
191
- suthep deploy --no-https --no-nginx
192
- ```
193
-
194
- ### Error Handling
195
-
196
- If any step fails, the deployment will:
197
- - Show a detailed error message
198
- - Exit with a non-zero status code
199
- - Not proceed with remaining services (if one fails)
200
-
201
- ### Output
202
-
203
- The command provides detailed output including:
204
- - Configuration loading status
205
- - Service deployment progress
206
- - Docker container status
207
- - Nginx configuration status
208
- - SSL certificate status
209
- - Health check results
210
- - Final service URLs
211
-
212
- Example output:
213
-
214
- ```
215
- 🚀 Deploying Services
216
-
217
- 📄 Loading configuration from suthep.yml...
218
- ✅ Configuration loaded for project: my-app
219
- Services: api, webapp
220
-
221
- 📦 Deploying service: api
222
- 🐳 Managing Docker container...
223
- ⚙️ Configuring Nginx reverse proxy...
224
- ✅ Nginx configured for api.example.com
225
- 🔐 Setting up HTTPS certificates...
226
- ✅ SSL certificate obtained for api.example.com
227
- 🔄 Reloading Nginx...
228
- 🏥 Performing health check...
229
- ✅ Service api is healthy
230
-
231
- ✨ Service api deployed successfully!
232
-
233
- 🎉 All services deployed successfully!
234
-
235
- 📋 Service URLs:
236
- api: https://api.example.com
237
- ```
238
-
239
- ## Global Options
240
-
241
- All commands support these global options:
242
-
243
- | Option | Short | Description |
244
- |--------|-------|-------------|
245
- | `--version` | `-V` | Show version number |
246
- | `--help` | `-h` | Show help for command |
247
-
248
- ### Examples
249
-
250
- ```bash
251
- # Show version
252
- suthep --version
253
-
254
- # Show help for deploy command
255
- suthep deploy --help
256
- ```
257
-
258
- ## Exit Codes
259
-
260
- - `0`: Success
261
- - `1`: Error occurred (see error message for details)
262
-
263
- ## Command Aliases
264
-
265
- After running `npm link`, the `suthep` command is available globally. You can also use it directly from the project directory:
266
-
267
- ```bash
268
- # From project directory
269
- npm run dev -- deploy
270
-
271
- # Or after building
272
- node dist/index.js deploy
273
- ```