@startanaicompany/cli 1.8.0 → 1.9.2

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.
@@ -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. Access your container shell
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.4.20 (Latest)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startanaicompany/cli",
3
- "version": "1.8.0",
3
+ "version": "1.9.2",
4
4
  "description": "Official CLI for StartAnAiCompany.com - Deploy AI recruitment sites with ease",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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
+ };
@@ -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,34 +95,52 @@ async function exec(command, options = {}) {
56
95
  logger.field(' Timeout', `${execRequest.timeout}s`);
57
96
  logger.newline();
58
97
 
59
- const spin = logger.spinner('Executing command in container...').start();
98
+ const spin = logger.spinner('Queueing command...').start();
60
99
 
100
+ let response;
61
101
  let result;
62
102
  try {
63
- result = await api.executeCommand(applicationUuid, execRequest);
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
- spin.fail('Command execution failed');
114
+ spin.fail('Command not allowed');
67
115
 
68
116
  if (error.response?.status === 400) {
69
- const data = error.response.data;
117
+ const data = error.response.data || {};
70
118
  logger.newline();
71
119
 
72
- if (data.error === 'VALIDATION_ERROR') {
73
- logger.error('Command validation failed');
120
+ // Show clear "command not allowed" message for all 400 errors
121
+ logger.error('This command is blocked for security reasons');
122
+
123
+ // Show backend error message if available (note: field is 'error', not 'message')
124
+ if (data.error) {
74
125
  logger.newline();
75
- logger.warn(data.message);
76
-
77
- if (data.message.includes('not in allowlist')) {
78
- logger.newline();
79
- logger.info('Allowed commands include:');
80
- logger.log(' Node.js: npm, node, npx, yarn, pnpm');
81
- logger.log(' Python: python, python3, pip, poetry');
82
- logger.log(' Ruby: bundle, rake, rails');
83
- logger.log(' Shell: sh, bash, echo, cat, ls, pwd');
84
- logger.log(' Database: psql, mysql, mongosh');
85
- }
126
+ logger.warn(data.error);
86
127
  }
128
+
129
+ logger.newline();
130
+ logger.info('Allowed commands include:');
131
+ logger.log(' Node.js: npm, node, npx, yarn, pnpm');
132
+ logger.log(' Python: python, python3, pip, poetry');
133
+ logger.log(' Ruby: bundle, rake, rails, ruby');
134
+ logger.log(' Shell: sh, bash, echo, cat, ls, pwd, env');
135
+ logger.log(' Database: psql, mysql, mongosh');
136
+ logger.log(' Build: go, cargo, make, cmake');
137
+ logger.newline();
138
+ logger.info('Blocked for security:');
139
+ logger.log(' System commands: whoami, ps, top, kill');
140
+ logger.log(' Destructive operations: rm, chmod, chown');
141
+ logger.log(' Advanced shell features: pipes (|), redirects (>), command substitution');
142
+
143
+ process.exit(1);
87
144
  } else if (error.response?.status === 408) {
88
145
  logger.newline();
89
146
  logger.error('Command execution timed out');
@@ -105,37 +162,50 @@ async function exec(command, options = {}) {
105
162
  logger.newline();
106
163
 
107
164
  // Display execution results
108
- logger.success(`✓ Execution ID: ${result.execution_id}`);
109
- logger.newline();
165
+ const execResult = result.result || {};
166
+
167
+ // Calculate duration
168
+ let duration = 'N/A';
169
+ if (result.created_at && result.completed_at) {
170
+ const start = new Date(result.created_at);
171
+ const end = new Date(result.completed_at);
172
+ duration = `${end - start}ms`;
173
+ }
110
174
 
111
- logger.field('Exit Code', result.exit_code === 0
112
- ? logger.chalk.green(result.exit_code)
113
- : logger.chalk.red(result.exit_code)
175
+ logger.field('Exit Code', execResult.exit_code !== undefined
176
+ ? (execResult.exit_code === 0
177
+ ? logger.chalk.green(execResult.exit_code)
178
+ : logger.chalk.red(execResult.exit_code))
179
+ : 'N/A'
114
180
  );
115
- logger.field('Duration', `${result.duration_ms}ms`);
116
- logger.field('Started', new Date(result.started_at).toLocaleString());
117
- logger.field('Completed', new Date(result.completed_at).toLocaleString());
181
+ logger.field('Duration', duration);
182
+ if (result.created_at) {
183
+ logger.field('Started', new Date(result.created_at).toLocaleString());
184
+ }
185
+ if (result.completed_at) {
186
+ logger.field('Completed', new Date(result.completed_at).toLocaleString());
187
+ }
118
188
 
119
189
  // Display stdout
120
- if (result.stdout) {
190
+ if (execResult.stdout) {
121
191
  logger.newline();
122
192
  logger.info('Standard Output:');
123
193
  logger.section('─'.repeat(60));
124
- console.log(result.stdout.trim());
194
+ console.log(execResult.stdout.trim());
125
195
  logger.section('─'.repeat(60));
126
196
  }
127
197
 
128
198
  // Display stderr
129
- if (result.stderr) {
199
+ if (execResult.stderr) {
130
200
  logger.newline();
131
201
  logger.warn('Standard Error:');
132
202
  logger.section('─'.repeat(60));
133
- console.error(result.stderr.trim());
203
+ console.error(execResult.stderr.trim());
134
204
  logger.section('─'.repeat(60));
135
205
  }
136
206
 
137
207
  // If no output
138
- if (!result.stdout && !result.stderr) {
208
+ if (!execResult.stdout && !execResult.stderr) {
139
209
  logger.newline();
140
210
  logger.info('(No output)');
141
211
  }
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
  };