@startanaicompany/cli 1.8.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 +7 -1
- package/README.md +263 -2
- package/bin/saac.js +29 -0
- package/package.json +1 -1
- package/src/commands/db.js +322 -0
- package/src/commands/exec.js +77 -16
- package/src/lib/api.js +64 -0
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
"Bash(echo:*)",
|
|
15
15
|
"Bash(timeout 15 /home/goryanio2025-ai/saac-cli/bin/saac.js logs:*)",
|
|
16
16
|
"Bash(timeout 30 /home/goryanio2025-ai/saac-cli/bin/saac.js deploy:*)",
|
|
17
|
-
"Bash(timeout 60 /home/goryanio2025-ai/saac-cli/bin/saac.js deploy:*)"
|
|
17
|
+
"Bash(timeout 60 /home/goryanio2025-ai/saac-cli/bin/saac.js deploy:*)",
|
|
18
|
+
"Bash(/home/goryanio2025-ai/saac-cli/bin/saac.js db info:*)",
|
|
19
|
+
"Bash(/home/goryanio2025-ai/saac-cli/bin/saac.js db list:*)",
|
|
20
|
+
"Bash(/home/goryanio2025-ai/saac-cli/bin/saac.js db sql:*)",
|
|
21
|
+
"Bash(/home/goryanio2025-ai/saac-cli/bin/saac.js db redis PING:*)",
|
|
22
|
+
"Bash(/home/goryanio2025-ai/saac-cli/bin/saac.js db redis SET:*)",
|
|
23
|
+
"Bash(/home/goryanio2025-ai/saac-cli/bin/saac.js db redis GET:*)"
|
|
18
24
|
],
|
|
19
25
|
"deny": [],
|
|
20
26
|
"ask": []
|
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
|
|
@@ -266,6 +267,34 @@ program
|
|
|
266
267
|
}
|
|
267
268
|
});
|
|
268
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
|
+
|
|
269
298
|
// Environment variable commands
|
|
270
299
|
const envCommand = program
|
|
271
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
|
+
};
|
package/src/commands/exec.js
CHANGED
|
@@ -7,6 +7,45 @@ const { getProjectConfig, ensureAuthenticated } = require('../lib/config');
|
|
|
7
7
|
const logger = require('../lib/logger');
|
|
8
8
|
const { table } = require('table');
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Poll for command result with timeout
|
|
12
|
+
*/
|
|
13
|
+
async function pollForResult(applicationUuid, commandId, 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, 'exec', 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
|
+
|
|
10
49
|
/**
|
|
11
50
|
* Execute a command in the remote container
|
|
12
51
|
* @param {string} command - Command to execute
|
|
@@ -39,7 +78,7 @@ async function exec(command, options = {}) {
|
|
|
39
78
|
const execRequest = {
|
|
40
79
|
command,
|
|
41
80
|
workdir: options.workdir || '/app',
|
|
42
|
-
timeout: options.timeout || 30
|
|
81
|
+
timeout: parseInt(options.timeout) || 30
|
|
43
82
|
};
|
|
44
83
|
|
|
45
84
|
// Validate timeout
|
|
@@ -56,11 +95,20 @@ async function exec(command, options = {}) {
|
|
|
56
95
|
logger.field(' Timeout', `${execRequest.timeout}s`);
|
|
57
96
|
logger.newline();
|
|
58
97
|
|
|
59
|
-
const spin = logger.spinner('
|
|
98
|
+
const spin = logger.spinner('Queueing command...').start();
|
|
60
99
|
|
|
100
|
+
let response;
|
|
61
101
|
let result;
|
|
62
102
|
try {
|
|
63
|
-
|
|
103
|
+
// Queue the command
|
|
104
|
+
response = await api.executeCommand(applicationUuid, execRequest);
|
|
105
|
+
const commandId = response.command_id;
|
|
106
|
+
|
|
107
|
+
spin.text = 'Waiting for daemon to execute command...';
|
|
108
|
+
|
|
109
|
+
// Poll for result with timeout buffer
|
|
110
|
+
result = await pollForResult(applicationUuid, commandId, execRequest.timeout + 30);
|
|
111
|
+
|
|
64
112
|
spin.succeed('Command executed');
|
|
65
113
|
} catch (error) {
|
|
66
114
|
spin.fail('Command execution failed');
|
|
@@ -105,37 +153,50 @@ async function exec(command, options = {}) {
|
|
|
105
153
|
logger.newline();
|
|
106
154
|
|
|
107
155
|
// Display execution results
|
|
108
|
-
|
|
109
|
-
|
|
156
|
+
const execResult = result.result || {};
|
|
157
|
+
|
|
158
|
+
// Calculate duration
|
|
159
|
+
let duration = 'N/A';
|
|
160
|
+
if (result.created_at && result.completed_at) {
|
|
161
|
+
const start = new Date(result.created_at);
|
|
162
|
+
const end = new Date(result.completed_at);
|
|
163
|
+
duration = `${end - start}ms`;
|
|
164
|
+
}
|
|
110
165
|
|
|
111
|
-
logger.field('Exit Code',
|
|
112
|
-
?
|
|
113
|
-
|
|
166
|
+
logger.field('Exit Code', execResult.exit_code !== undefined
|
|
167
|
+
? (execResult.exit_code === 0
|
|
168
|
+
? logger.chalk.green(execResult.exit_code)
|
|
169
|
+
: logger.chalk.red(execResult.exit_code))
|
|
170
|
+
: 'N/A'
|
|
114
171
|
);
|
|
115
|
-
logger.field('Duration',
|
|
116
|
-
|
|
117
|
-
|
|
172
|
+
logger.field('Duration', duration);
|
|
173
|
+
if (result.created_at) {
|
|
174
|
+
logger.field('Started', new Date(result.created_at).toLocaleString());
|
|
175
|
+
}
|
|
176
|
+
if (result.completed_at) {
|
|
177
|
+
logger.field('Completed', new Date(result.completed_at).toLocaleString());
|
|
178
|
+
}
|
|
118
179
|
|
|
119
180
|
// Display stdout
|
|
120
|
-
if (
|
|
181
|
+
if (execResult.stdout) {
|
|
121
182
|
logger.newline();
|
|
122
183
|
logger.info('Standard Output:');
|
|
123
184
|
logger.section('─'.repeat(60));
|
|
124
|
-
console.log(
|
|
185
|
+
console.log(execResult.stdout.trim());
|
|
125
186
|
logger.section('─'.repeat(60));
|
|
126
187
|
}
|
|
127
188
|
|
|
128
189
|
// Display stderr
|
|
129
|
-
if (
|
|
190
|
+
if (execResult.stderr) {
|
|
130
191
|
logger.newline();
|
|
131
192
|
logger.warn('Standard Error:');
|
|
132
193
|
logger.section('─'.repeat(60));
|
|
133
|
-
console.error(
|
|
194
|
+
console.error(execResult.stderr.trim());
|
|
134
195
|
logger.section('─'.repeat(60));
|
|
135
196
|
}
|
|
136
197
|
|
|
137
198
|
// If no output
|
|
138
|
-
if (!
|
|
199
|
+
if (!execResult.stdout && !execResult.stderr) {
|
|
139
200
|
logger.newline();
|
|
140
201
|
logger.info('(No output)');
|
|
141
202
|
}
|
package/src/lib/api.js
CHANGED
|
@@ -319,6 +319,65 @@ async function listGitRepositories(gitHost, options = {}) {
|
|
|
319
319
|
return response.data;
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
/**
|
|
323
|
+
* List database containers for an application
|
|
324
|
+
* @param {string} uuid - Application UUID
|
|
325
|
+
* @returns {Promise<object>} - { command_id, status }
|
|
326
|
+
*/
|
|
327
|
+
async function listDbContainers(uuid) {
|
|
328
|
+
const client = createClient();
|
|
329
|
+
const response = await client.get(`/applications/${uuid}/db/containers`);
|
|
330
|
+
return response.data;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Execute SQL query on application database
|
|
335
|
+
* @param {string} uuid - Application UUID
|
|
336
|
+
* @param {object} queryData - { query, db_name?, allow_writes? }
|
|
337
|
+
* @returns {Promise<object>} - { command_id, status }
|
|
338
|
+
*/
|
|
339
|
+
async function executeSql(uuid, queryData) {
|
|
340
|
+
const client = createClient();
|
|
341
|
+
const response = await client.post(`/applications/${uuid}/db/sql`, queryData);
|
|
342
|
+
return response.data;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Get result of a database command (universal endpoint for all command types)
|
|
347
|
+
* @param {string} uuid - Application UUID
|
|
348
|
+
* @param {string} commandType - 'sql', 'redis', 'containers' (not used, kept for API compatibility)
|
|
349
|
+
* @param {string} commandId - Command ID
|
|
350
|
+
* @returns {Promise<object>} - { status, result, command_type, created_at, completed_at }
|
|
351
|
+
*/
|
|
352
|
+
async function getDbCommandResult(uuid, commandType, commandId) {
|
|
353
|
+
const client = createClient();
|
|
354
|
+
const response = await client.get(`/applications/${uuid}/db/result/${commandId}`);
|
|
355
|
+
return response.data;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Execute Redis command on application database
|
|
360
|
+
* @param {string} uuid - Application UUID
|
|
361
|
+
* @param {object} commandData - { command }
|
|
362
|
+
* @returns {Promise<object>} - { command_id, status }
|
|
363
|
+
*/
|
|
364
|
+
async function executeRedis(uuid, commandData) {
|
|
365
|
+
const client = createClient();
|
|
366
|
+
const response = await client.post(`/applications/${uuid}/db/redis`, commandData);
|
|
367
|
+
return response.data;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get database connection information
|
|
372
|
+
* @param {string} uuid - Application UUID
|
|
373
|
+
* @returns {Promise<object>} - { postgres: {...}, redis: {...} }
|
|
374
|
+
*/
|
|
375
|
+
async function getDbInfo(uuid) {
|
|
376
|
+
const client = createClient();
|
|
377
|
+
const response = await client.get(`/applications/${uuid}/db/info`);
|
|
378
|
+
return response.data;
|
|
379
|
+
}
|
|
380
|
+
|
|
322
381
|
module.exports = {
|
|
323
382
|
createClient,
|
|
324
383
|
login,
|
|
@@ -345,4 +404,9 @@ module.exports = {
|
|
|
345
404
|
executeCommand,
|
|
346
405
|
getExecutionHistory,
|
|
347
406
|
listGitRepositories,
|
|
407
|
+
listDbContainers,
|
|
408
|
+
executeSql,
|
|
409
|
+
getDbCommandResult,
|
|
410
|
+
executeRedis,
|
|
411
|
+
getDbInfo,
|
|
348
412
|
};
|