@startanaicompany/cli 1.6.0 → 1.9.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.
- package/.claude/settings.local.json +19 -1
- package/CLAUDE.md +143 -1300
- package/README.md +263 -2
- package/bin/saac.js +32 -2
- package/package.json +1 -1
- package/src/commands/db.js +322 -0
- package/src/commands/deploy.js +194 -30
- package/src/commands/exec.js +77 -16
- package/src/lib/api.js +66 -2
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
- 🔄 **Auto-healing** - Automatically fixes common deployment issues
|
|
16
16
|
- 🖥️ **Remote Shell** - Access your container via WebSocket (Project Aurora)
|
|
17
17
|
- 🔧 **Remote Execution** - Run commands inside your container
|
|
18
|
+
- 🗄️ **Database Management** - Direct access to PostgreSQL and Redis
|
|
18
19
|
- 📊 **Real-time Logs** - View runtime and deployment logs
|
|
19
20
|
|
|
20
21
|
## Installation
|
|
@@ -47,7 +48,11 @@ saac deploy
|
|
|
47
48
|
# 7. View logs
|
|
48
49
|
saac logs
|
|
49
50
|
|
|
50
|
-
# 8.
|
|
51
|
+
# 8. Query your database
|
|
52
|
+
saac db sql "SELECT NOW()"
|
|
53
|
+
saac db redis PING
|
|
54
|
+
|
|
55
|
+
# 9. Access your container shell
|
|
51
56
|
saac shell
|
|
52
57
|
```
|
|
53
58
|
|
|
@@ -66,6 +71,11 @@ saac shell
|
|
|
66
71
|
- [Remote Shell (saac shell)](#remote-shell)
|
|
67
72
|
- [Remote Execution (saac exec)](#remote-execution)
|
|
68
73
|
- [Local Development (saac run)](#local-development)
|
|
74
|
+
- [Database Management](#database-management)
|
|
75
|
+
- [List Containers (saac db list)](#list-containers)
|
|
76
|
+
- [SQL Queries (saac db sql)](#sql-queries)
|
|
77
|
+
- [Redis Commands (saac db redis)](#redis-commands)
|
|
78
|
+
- [Connection Info (saac db info)](#connection-info)
|
|
69
79
|
- [Logs & Monitoring](#logs--monitoring)
|
|
70
80
|
- [Domain Management](#domain-management)
|
|
71
81
|
- [Complete Workflows](#complete-workflows)
|
|
@@ -1599,6 +1609,231 @@ postgresql://user:pass@db.internal:5432/myapp
|
|
|
1599
1609
|
|
|
1600
1610
|
---
|
|
1601
1611
|
|
|
1612
|
+
## Database Management
|
|
1613
|
+
|
|
1614
|
+
Manage and query your application's databases directly from the CLI.
|
|
1615
|
+
|
|
1616
|
+
### List Containers
|
|
1617
|
+
|
|
1618
|
+
#### `saac db list`
|
|
1619
|
+
|
|
1620
|
+
List all database containers for your application.
|
|
1621
|
+
|
|
1622
|
+
```bash
|
|
1623
|
+
saac db list
|
|
1624
|
+
saac db ls # Alias
|
|
1625
|
+
```
|
|
1626
|
+
|
|
1627
|
+
**Shows:**
|
|
1628
|
+
- Container name
|
|
1629
|
+
- Type (postgres, redis, app)
|
|
1630
|
+
- Status (running, healthy, stopped)
|
|
1631
|
+
- Docker image
|
|
1632
|
+
|
|
1633
|
+
**Example output:**
|
|
1634
|
+
```
|
|
1635
|
+
Database Containers for my-app
|
|
1636
|
+
───────────────────────────────
|
|
1637
|
+
|
|
1638
|
+
┌─────────────────────────────┬──────────┬────────────┬──────────────────┐
|
|
1639
|
+
│ Container Name │ Type │ Status │ Image │
|
|
1640
|
+
├─────────────────────────────┼──────────┼────────────┼──────────────────┤
|
|
1641
|
+
│ postgres-abc123-456def │ postgres │ healthy │ postgres:15 │
|
|
1642
|
+
│ redis-abc123-456def │ redis │ healthy │ redis:7 │
|
|
1643
|
+
│ app-abc123-456def │ app │ running │ node:18 │
|
|
1644
|
+
└─────────────────────────────┴──────────┴────────────┴──────────────────┘
|
|
1645
|
+
```
|
|
1646
|
+
|
|
1647
|
+
### SQL Queries
|
|
1648
|
+
|
|
1649
|
+
#### `saac db sql <query>`
|
|
1650
|
+
|
|
1651
|
+
Execute SQL queries on your PostgreSQL database.
|
|
1652
|
+
|
|
1653
|
+
```bash
|
|
1654
|
+
# Simple SELECT query
|
|
1655
|
+
saac db sql "SELECT NOW()"
|
|
1656
|
+
|
|
1657
|
+
# Query your data
|
|
1658
|
+
saac db sql "SELECT * FROM users LIMIT 10"
|
|
1659
|
+
|
|
1660
|
+
# Count records
|
|
1661
|
+
saac db sql "SELECT COUNT(*) FROM posts"
|
|
1662
|
+
|
|
1663
|
+
# Specify database name (optional)
|
|
1664
|
+
saac db sql "SELECT version()" --db my_database
|
|
1665
|
+
|
|
1666
|
+
# Write operations (CREATE, INSERT, UPDATE, DELETE)
|
|
1667
|
+
saac db sql "INSERT INTO users (name, email) VALUES ('John', 'john@example.com')" --write
|
|
1668
|
+
saac db sql "UPDATE users SET active = true WHERE id = 123" --write
|
|
1669
|
+
saac db sql "DELETE FROM sessions WHERE expires_at < NOW()" --write
|
|
1670
|
+
```
|
|
1671
|
+
|
|
1672
|
+
**Options:**
|
|
1673
|
+
- `--db <name>` - Database name (default: from environment variables)
|
|
1674
|
+
- `--write` - Allow write operations (INSERT, UPDATE, DELETE, CREATE, DROP)
|
|
1675
|
+
|
|
1676
|
+
**Security:**
|
|
1677
|
+
- Read-only by default (SELECT, SHOW, DESCRIBE, EXPLAIN)
|
|
1678
|
+
- Write operations require `--write` flag
|
|
1679
|
+
- Dangerous operations (DROP, TRUNCATE) require `--write` flag
|
|
1680
|
+
- Rate limit: 60 queries per 5 minutes
|
|
1681
|
+
|
|
1682
|
+
**Example output:**
|
|
1683
|
+
```bash
|
|
1684
|
+
$ saac db sql "SELECT * FROM users LIMIT 3"
|
|
1685
|
+
|
|
1686
|
+
Executing SQL Query on my-app
|
|
1687
|
+
──────────────────────────────
|
|
1688
|
+
|
|
1689
|
+
┌────┬─────────────┬──────────────────┬────────────────────┐
|
|
1690
|
+
│ id │ name │ email │ created_at │
|
|
1691
|
+
├────┼─────────────┼──────────────────┼────────────────────┤
|
|
1692
|
+
│ 1 │ Alice │ alice@example.com│ 2026-02-15 10:00:00│
|
|
1693
|
+
│ 2 │ Bob │ bob@example.com │ 2026-02-15 11:30:00│
|
|
1694
|
+
│ 3 │ Charlie │ charlie@test.com │ 2026-02-16 09:15:00│
|
|
1695
|
+
└────┴─────────────┴──────────────────┴────────────────────┘
|
|
1696
|
+
|
|
1697
|
+
Rows returned: 3
|
|
1698
|
+
```
|
|
1699
|
+
|
|
1700
|
+
### Redis Commands
|
|
1701
|
+
|
|
1702
|
+
#### `saac db redis <command>`
|
|
1703
|
+
|
|
1704
|
+
Execute Redis commands.
|
|
1705
|
+
|
|
1706
|
+
```bash
|
|
1707
|
+
# Test connection
|
|
1708
|
+
saac db redis PING
|
|
1709
|
+
|
|
1710
|
+
# Get a key
|
|
1711
|
+
saac db redis GET mykey
|
|
1712
|
+
|
|
1713
|
+
# Set a key
|
|
1714
|
+
saac db redis SET mykey "hello world"
|
|
1715
|
+
|
|
1716
|
+
# Hash operations
|
|
1717
|
+
saac db redis HSET user:123 name "John"
|
|
1718
|
+
saac db redis HGETALL user:123
|
|
1719
|
+
|
|
1720
|
+
# List operations
|
|
1721
|
+
saac db redis LPUSH mylist "item1"
|
|
1722
|
+
saac db redis LRANGE mylist 0 -1
|
|
1723
|
+
|
|
1724
|
+
# Check key existence
|
|
1725
|
+
saac db redis EXISTS mykey
|
|
1726
|
+
|
|
1727
|
+
# Get key type
|
|
1728
|
+
saac db redis TYPE mykey
|
|
1729
|
+
```
|
|
1730
|
+
|
|
1731
|
+
**Supported Commands:**
|
|
1732
|
+
- String: GET, SET, APPEND, INCR, DECR
|
|
1733
|
+
- Hash: HGET, HSET, HGETALL, HDEL
|
|
1734
|
+
- List: LPUSH, RPUSH, LRANGE, LLEN
|
|
1735
|
+
- Set: SADD, SMEMBERS, SISMEMBER
|
|
1736
|
+
- Sorted Set: ZADD, ZRANGE, ZSCORE
|
|
1737
|
+
- Key: EXISTS, DEL, TYPE, EXPIRE, TTL
|
|
1738
|
+
- Info: PING, INFO, DBSIZE
|
|
1739
|
+
|
|
1740
|
+
**Blocked Commands** (for safety):
|
|
1741
|
+
- FLUSHDB, FLUSHALL (data loss)
|
|
1742
|
+
- CONFIG (security)
|
|
1743
|
+
- SHUTDOWN (availability)
|
|
1744
|
+
|
|
1745
|
+
**Example output:**
|
|
1746
|
+
```bash
|
|
1747
|
+
$ saac db redis GET user:session:abc123
|
|
1748
|
+
|
|
1749
|
+
Executing Redis Command on my-app
|
|
1750
|
+
──────────────────────────────────
|
|
1751
|
+
|
|
1752
|
+
Command: GET user:session:abc123
|
|
1753
|
+
|
|
1754
|
+
✓ Result:
|
|
1755
|
+
{"userId":"123","expires":"2026-02-20T10:00:00Z"}
|
|
1756
|
+
```
|
|
1757
|
+
|
|
1758
|
+
### Connection Info
|
|
1759
|
+
|
|
1760
|
+
#### `saac db info`
|
|
1761
|
+
|
|
1762
|
+
Show database connection information.
|
|
1763
|
+
|
|
1764
|
+
```bash
|
|
1765
|
+
saac db info
|
|
1766
|
+
```
|
|
1767
|
+
|
|
1768
|
+
**Shows:**
|
|
1769
|
+
- PostgreSQL host, port, database name
|
|
1770
|
+
- Redis host, port
|
|
1771
|
+
- Internal network addresses
|
|
1772
|
+
|
|
1773
|
+
**Example output:**
|
|
1774
|
+
```
|
|
1775
|
+
Database Connection Info for my-app
|
|
1776
|
+
────────────────────────────────────
|
|
1777
|
+
|
|
1778
|
+
✓ PostgreSQL:
|
|
1779
|
+
Host: postgres.internal
|
|
1780
|
+
Port: 5432
|
|
1781
|
+
Database: myapp_db
|
|
1782
|
+
|
|
1783
|
+
✓ Redis:
|
|
1784
|
+
Host: redis.internal
|
|
1785
|
+
Port: 6379
|
|
1786
|
+
|
|
1787
|
+
ℹ Note: These are internal network addresses
|
|
1788
|
+
(only accessible within the application network)
|
|
1789
|
+
```
|
|
1790
|
+
|
|
1791
|
+
### Database Management Workflows
|
|
1792
|
+
|
|
1793
|
+
**Create table and insert data:**
|
|
1794
|
+
```bash
|
|
1795
|
+
# 1. Create table
|
|
1796
|
+
saac db sql "CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(100), email VARCHAR(100))" --write
|
|
1797
|
+
|
|
1798
|
+
# 2. Insert data
|
|
1799
|
+
saac db sql "INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')" --write
|
|
1800
|
+
|
|
1801
|
+
# 3. Query data
|
|
1802
|
+
saac db sql "SELECT * FROM users"
|
|
1803
|
+
```
|
|
1804
|
+
|
|
1805
|
+
**Redis caching workflow:**
|
|
1806
|
+
```bash
|
|
1807
|
+
# 1. Set cache value
|
|
1808
|
+
saac db redis SET cache:users:123 "{\"name\":\"Alice\",\"email\":\"alice@example.com\"}"
|
|
1809
|
+
|
|
1810
|
+
# 2. Get cache value
|
|
1811
|
+
saac db redis GET cache:users:123
|
|
1812
|
+
|
|
1813
|
+
# 3. Set expiration
|
|
1814
|
+
saac db redis EXPIRE cache:users:123 3600
|
|
1815
|
+
|
|
1816
|
+
# 4. Check TTL
|
|
1817
|
+
saac db redis TTL cache:users:123
|
|
1818
|
+
```
|
|
1819
|
+
|
|
1820
|
+
**Check database health:**
|
|
1821
|
+
```bash
|
|
1822
|
+
# List all containers
|
|
1823
|
+
saac db list
|
|
1824
|
+
|
|
1825
|
+
# Check PostgreSQL version
|
|
1826
|
+
saac db sql "SELECT version()"
|
|
1827
|
+
|
|
1828
|
+
# Test Redis connection
|
|
1829
|
+
saac db redis PING
|
|
1830
|
+
|
|
1831
|
+
# View connection details
|
|
1832
|
+
saac db info
|
|
1833
|
+
```
|
|
1834
|
+
|
|
1835
|
+
---
|
|
1836
|
+
|
|
1602
1837
|
## Logs & Monitoring
|
|
1603
1838
|
|
|
1604
1839
|
View runtime logs and deployment logs for your application.
|
|
@@ -2496,7 +2731,33 @@ MIT © StartAnAiCompany
|
|
|
2496
2731
|
|
|
2497
2732
|
## Changelog
|
|
2498
2733
|
|
|
2499
|
-
### Version 1.
|
|
2734
|
+
### Version 1.9.1 (Latest)
|
|
2735
|
+
- Fixed `saac exec` to use SSE command channel for faster, more reliable execution
|
|
2736
|
+
- Migrated from direct docker exec to daemon-based execution
|
|
2737
|
+
- Consistent with database commands (sql, redis, containers)
|
|
2738
|
+
- No more 30-second timeouts - average response time ~500ms
|
|
2739
|
+
|
|
2740
|
+
### Version 1.9.0
|
|
2741
|
+
- **New: Database Management Commands**
|
|
2742
|
+
- Added `saac db list` - List all database containers (postgres, redis, app)
|
|
2743
|
+
- Added `saac db sql <query>` - Execute SQL queries with formatted table output
|
|
2744
|
+
- Added `saac db redis <command>` - Execute Redis commands (GET, SET, PING, etc.)
|
|
2745
|
+
- Added `saac db info` - Show database connection information
|
|
2746
|
+
- Read-only by default with `--write` flag for modifications
|
|
2747
|
+
- Rate limiting: 60 queries per 5 minutes
|
|
2748
|
+
- Uses SSE command channel with ~1s response time
|
|
2749
|
+
|
|
2750
|
+
### Version 1.8.0
|
|
2751
|
+
- Made deploy streaming the default behavior
|
|
2752
|
+
- Changed `--stream` flag to `--no-stream` for fire-and-forget mode
|
|
2753
|
+
- Improved visibility for AI agents and users during deployments
|
|
2754
|
+
|
|
2755
|
+
### Version 1.7.0
|
|
2756
|
+
- Added real-time deploy streaming with `--stream` flag
|
|
2757
|
+
- Added `--no-cache` flag for clean rebuilds
|
|
2758
|
+
- Improved deployment visibility
|
|
2759
|
+
|
|
2760
|
+
### Version 1.4.20
|
|
2500
2761
|
- Fixed logs command - handle logs as string instead of array
|
|
2501
2762
|
- Backend returns `result.logs` as string, not array
|
|
2502
2763
|
|
package/bin/saac.js
CHANGED
|
@@ -34,6 +34,7 @@ const manual = require('../src/commands/manual');
|
|
|
34
34
|
const run = require('../src/commands/run');
|
|
35
35
|
const shell = require('../src/commands/shell');
|
|
36
36
|
const execCmd = require('../src/commands/exec');
|
|
37
|
+
const db = require('../src/commands/db');
|
|
37
38
|
|
|
38
39
|
// Configure CLI
|
|
39
40
|
program
|
|
@@ -205,8 +206,9 @@ program
|
|
|
205
206
|
|
|
206
207
|
program
|
|
207
208
|
.command('deploy')
|
|
208
|
-
.description('Deploy current application')
|
|
209
|
-
.option('-
|
|
209
|
+
.description('Deploy current application (streams build logs by default)')
|
|
210
|
+
.option('--no-stream', 'Skip streaming and return immediately after queuing')
|
|
211
|
+
.option('--no-cache', 'Rebuild without Docker cache (slower but fresh build)')
|
|
210
212
|
.action(deploy);
|
|
211
213
|
|
|
212
214
|
program
|
|
@@ -265,6 +267,34 @@ program
|
|
|
265
267
|
}
|
|
266
268
|
});
|
|
267
269
|
|
|
270
|
+
// Database commands
|
|
271
|
+
const dbCommand = program
|
|
272
|
+
.command('db')
|
|
273
|
+
.description('Manage application databases');
|
|
274
|
+
|
|
275
|
+
dbCommand
|
|
276
|
+
.command('list')
|
|
277
|
+
.alias('ls')
|
|
278
|
+
.description('List database containers (postgres, redis, etc.)')
|
|
279
|
+
.action(db.list);
|
|
280
|
+
|
|
281
|
+
dbCommand
|
|
282
|
+
.command('sql <query>')
|
|
283
|
+
.description('Execute SQL query (read-only by default)')
|
|
284
|
+
.option('--db <name>', 'Database name (default: from env vars)')
|
|
285
|
+
.option('--write', 'Allow write operations (INSERT, UPDATE, DELETE)')
|
|
286
|
+
.action(db.sql);
|
|
287
|
+
|
|
288
|
+
dbCommand
|
|
289
|
+
.command('redis <command...>')
|
|
290
|
+
.description('Execute Redis command (e.g., GET mykey, HGETALL user:123)')
|
|
291
|
+
.action(db.redis);
|
|
292
|
+
|
|
293
|
+
dbCommand
|
|
294
|
+
.command('info')
|
|
295
|
+
.description('Show database connection information')
|
|
296
|
+
.action(db.info);
|
|
297
|
+
|
|
268
298
|
// Environment variable commands
|
|
269
299
|
const envCommand = program
|
|
270
300
|
.command('env')
|
package/package.json
CHANGED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database management commands
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const api = require('../lib/api');
|
|
6
|
+
const { getProjectConfig, ensureAuthenticated } = require('../lib/config');
|
|
7
|
+
const logger = require('../lib/logger');
|
|
8
|
+
const { table } = require('table');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Poll for command result with timeout
|
|
12
|
+
*/
|
|
13
|
+
async function pollForResult(applicationUuid, commandId, commandType, maxWaitSeconds = 120) {
|
|
14
|
+
const startTime = Date.now();
|
|
15
|
+
const pollInterval = 1000; // 1 second
|
|
16
|
+
|
|
17
|
+
while (true) {
|
|
18
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
19
|
+
|
|
20
|
+
if (elapsed > maxWaitSeconds) {
|
|
21
|
+
throw new Error(`Command timed out after ${maxWaitSeconds} seconds`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const result = await api.getDbCommandResult(applicationUuid, commandType, commandId);
|
|
26
|
+
|
|
27
|
+
if (result.status === 'completed') {
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (result.status === 'failed') {
|
|
32
|
+
const errorMsg = result.result?.error || result.error || 'Command failed';
|
|
33
|
+
throw new Error(errorMsg);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Still pending, wait and retry
|
|
37
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// If error is not a 404 (command not found yet), rethrow
|
|
40
|
+
if (error.response?.status !== 404) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
// 404 means command not processed yet, keep polling
|
|
44
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* List database containers
|
|
51
|
+
*/
|
|
52
|
+
async function list(options) {
|
|
53
|
+
try {
|
|
54
|
+
if (!(await ensureAuthenticated())) {
|
|
55
|
+
logger.error('Not logged in');
|
|
56
|
+
logger.info('Run: saac login -e <email> -k <api-key>');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const projectConfig = getProjectConfig();
|
|
61
|
+
if (!projectConfig || !projectConfig.applicationUuid) {
|
|
62
|
+
logger.error('No application found in current directory');
|
|
63
|
+
logger.info('Run: saac init or saac create');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const { applicationUuid, applicationName } = projectConfig;
|
|
68
|
+
|
|
69
|
+
logger.section(`Database Containers for ${applicationName}`);
|
|
70
|
+
logger.newline();
|
|
71
|
+
|
|
72
|
+
const spin = logger.spinner('Fetching database containers...').start();
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const response = await api.listDbContainers(applicationUuid);
|
|
76
|
+
const commandId = response.command_id;
|
|
77
|
+
|
|
78
|
+
spin.text = 'Waiting for daemon to respond...';
|
|
79
|
+
const result = await pollForResult(applicationUuid, commandId, 'containers');
|
|
80
|
+
|
|
81
|
+
spin.succeed('Database containers retrieved');
|
|
82
|
+
logger.newline();
|
|
83
|
+
|
|
84
|
+
if (result.result && result.result.containers) {
|
|
85
|
+
const containers = result.result.containers;
|
|
86
|
+
|
|
87
|
+
if (containers.length === 0) {
|
|
88
|
+
logger.info('No database containers found');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Display as table
|
|
93
|
+
const tableData = [
|
|
94
|
+
['Container Name', 'Type', 'Status', 'Image']
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
containers.forEach(container => {
|
|
98
|
+
tableData.push([
|
|
99
|
+
container.name || 'N/A',
|
|
100
|
+
container.type || 'N/A',
|
|
101
|
+
container.status || 'N/A',
|
|
102
|
+
container.image || 'N/A'
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
console.log(table(tableData, {
|
|
107
|
+
header: {
|
|
108
|
+
alignment: 'center',
|
|
109
|
+
content: 'Database Containers'
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
} else {
|
|
113
|
+
logger.warn('No container data in response');
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
spin.fail('Failed to fetch database containers');
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.error(error.response?.data?.message || error.message);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Execute SQL query
|
|
127
|
+
*/
|
|
128
|
+
async function sql(query, options) {
|
|
129
|
+
try {
|
|
130
|
+
if (!(await ensureAuthenticated())) {
|
|
131
|
+
logger.error('Not logged in');
|
|
132
|
+
logger.info('Run: saac login -e <email> -k <api-key>');
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const projectConfig = getProjectConfig();
|
|
137
|
+
if (!projectConfig || !projectConfig.applicationUuid) {
|
|
138
|
+
logger.error('No application found in current directory');
|
|
139
|
+
logger.info('Run: saac init or saac create');
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const { applicationUuid, applicationName } = projectConfig;
|
|
144
|
+
|
|
145
|
+
logger.section(`Executing SQL Query on ${applicationName}`);
|
|
146
|
+
logger.newline();
|
|
147
|
+
|
|
148
|
+
const spin = logger.spinner('Executing query...').start();
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const requestBody = {
|
|
152
|
+
query: query,
|
|
153
|
+
allow_writes: options.write || false
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
if (options.db) {
|
|
157
|
+
requestBody.db_name = options.db;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const response = await api.executeSql(applicationUuid, requestBody);
|
|
161
|
+
const commandId = response.command_id;
|
|
162
|
+
|
|
163
|
+
spin.text = 'Waiting for daemon to execute query...';
|
|
164
|
+
const result = await pollForResult(applicationUuid, commandId, 'sql');
|
|
165
|
+
|
|
166
|
+
spin.succeed('Query executed');
|
|
167
|
+
logger.newline();
|
|
168
|
+
|
|
169
|
+
if (result.result && result.result.output) {
|
|
170
|
+
// Display CSV output as table
|
|
171
|
+
const csvData = result.result.output;
|
|
172
|
+
const rows = csvData.trim().split('\n').map(row => row.split(','));
|
|
173
|
+
|
|
174
|
+
if (rows.length > 0) {
|
|
175
|
+
console.log(table(rows));
|
|
176
|
+
logger.newline();
|
|
177
|
+
logger.info(`Rows returned: ${rows.length - 1}`); // -1 for header
|
|
178
|
+
} else {
|
|
179
|
+
logger.info('Query returned no results');
|
|
180
|
+
}
|
|
181
|
+
} else if (result.result && result.result.error) {
|
|
182
|
+
logger.error('Query error:');
|
|
183
|
+
logger.log(result.result.error);
|
|
184
|
+
} else {
|
|
185
|
+
logger.info('Query completed (no output)');
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
spin.fail('Query execution failed');
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
} catch (error) {
|
|
192
|
+
logger.error(error.response?.data?.message || error.message);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Execute Redis command
|
|
199
|
+
*/
|
|
200
|
+
async function redis(commandArgs, options) {
|
|
201
|
+
try {
|
|
202
|
+
if (!(await ensureAuthenticated())) {
|
|
203
|
+
logger.error('Not logged in');
|
|
204
|
+
logger.info('Run: saac login -e <email> -k <api-key>');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const projectConfig = getProjectConfig();
|
|
209
|
+
if (!projectConfig || !projectConfig.applicationUuid) {
|
|
210
|
+
logger.error('No application found in current directory');
|
|
211
|
+
logger.info('Run: saac init or saac create');
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const { applicationUuid, applicationName } = projectConfig;
|
|
216
|
+
|
|
217
|
+
// Join command args into a single string
|
|
218
|
+
const command = commandArgs.join(' ');
|
|
219
|
+
|
|
220
|
+
logger.section(`Executing Redis Command on ${applicationName}`);
|
|
221
|
+
logger.newline();
|
|
222
|
+
logger.info(`Command: ${command}`);
|
|
223
|
+
logger.newline();
|
|
224
|
+
|
|
225
|
+
const spin = logger.spinner('Executing command...').start();
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const response = await api.executeRedis(applicationUuid, { command });
|
|
229
|
+
const commandId = response.command_id;
|
|
230
|
+
|
|
231
|
+
spin.text = 'Waiting for daemon to execute command...';
|
|
232
|
+
const result = await pollForResult(applicationUuid, commandId, 'redis');
|
|
233
|
+
|
|
234
|
+
spin.succeed('Command executed');
|
|
235
|
+
logger.newline();
|
|
236
|
+
|
|
237
|
+
if (result.result && result.result.output) {
|
|
238
|
+
logger.success('Result:');
|
|
239
|
+
logger.log(result.result.output);
|
|
240
|
+
} else if (result.result && result.result.error) {
|
|
241
|
+
logger.error('Command error:');
|
|
242
|
+
logger.log(result.result.error);
|
|
243
|
+
} else {
|
|
244
|
+
logger.info('Command completed (no output)');
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
spin.fail('Command execution failed');
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
logger.error(error.response?.data?.message || error.message);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Show database connection info
|
|
258
|
+
*/
|
|
259
|
+
async function info(options) {
|
|
260
|
+
try {
|
|
261
|
+
if (!(await ensureAuthenticated())) {
|
|
262
|
+
logger.error('Not logged in');
|
|
263
|
+
logger.info('Run: saac login -e <email> -k <api-key>');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const projectConfig = getProjectConfig();
|
|
268
|
+
if (!projectConfig || !projectConfig.applicationUuid) {
|
|
269
|
+
logger.error('No application found in current directory');
|
|
270
|
+
logger.info('Run: saac init or saac create');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const { applicationUuid, applicationName } = projectConfig;
|
|
275
|
+
|
|
276
|
+
logger.section(`Database Connection Info for ${applicationName}`);
|
|
277
|
+
logger.newline();
|
|
278
|
+
|
|
279
|
+
const spin = logger.spinner('Fetching connection info...').start();
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const result = await api.getDbInfo(applicationUuid);
|
|
283
|
+
|
|
284
|
+
spin.succeed('Connection info retrieved');
|
|
285
|
+
logger.newline();
|
|
286
|
+
|
|
287
|
+
if (result.postgres) {
|
|
288
|
+
logger.success('PostgreSQL:');
|
|
289
|
+
logger.field(' Host', result.postgres.host || 'postgres.internal');
|
|
290
|
+
logger.field(' Port', result.postgres.port || '5432');
|
|
291
|
+
logger.field(' Database', result.postgres.database || 'N/A');
|
|
292
|
+
logger.newline();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (result.redis) {
|
|
296
|
+
logger.success('Redis:');
|
|
297
|
+
logger.field(' Host', result.redis.host || 'redis.internal');
|
|
298
|
+
logger.field(' Port', result.redis.port || '6379');
|
|
299
|
+
logger.newline();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!result.postgres && !result.redis) {
|
|
303
|
+
logger.warn('No database connection info available');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
logger.info('Note: These are internal network addresses (only accessible within the application network)');
|
|
307
|
+
} catch (error) {
|
|
308
|
+
spin.fail('Failed to fetch connection info');
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
logger.error(error.response?.data?.message || error.message);
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
module.exports = {
|
|
318
|
+
list,
|
|
319
|
+
sql,
|
|
320
|
+
redis,
|
|
321
|
+
info
|
|
322
|
+
};
|