pg-local-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # pg-local-cli
2
+
3
+ Một công cụ CLI bằng **Node.js** giúp bạn dễ dàng khởi tạo, khởi chạy và quản lý các database instance **PostgreSQL** trên localhost mà **không cần sử dụng Docker, Podman hay cài đặt PostgreSQL toàn hệ thống**.
4
+
5
+ Dự án này sử dụng các binary PostgreSQL portable chính thức được tải tự động tùy theo hệ điều hành (Windows, macOS, Linux) thông qua thư viện `embedded-postgres`.
6
+
7
+ ## ✨ Tính năng chính
8
+
9
+ - **Không cần Docker/Podman**: Phù hợp cho máy yếu hoặc môi trường không cài được Docker.
10
+ - **Không cần cài Postgres hệ thống**: Tự động tải binary độc lập về thư mục home của user.
11
+ - **Hỗ trợ đổi Host/Port**: Dễ dàng đổi cổng hoặc địa chỉ IP lắng nghe để tránh xung đột cổng (ví dụ: cổng mặc định 5432 bị chiếm).
12
+ - **Chạy ngầm (Daemon)**: Khi gõ `start`, Postgres sẽ chạy ngầm, bạn có thể tắt terminal đi mà database vẫn chạy bình thường.
13
+ - **Hỗ trợ nhiều Instance**: Tạo và cấu hình độc lập nhiều database khác nhau (ví dụ: `my_app`, `dev_db`).
14
+ - **Đa nền tảng**: Hỗ trợ đầy đủ Windows (x64), macOS (Intel, Apple Silicon) và Linux (x64, arm64, v.v.).
15
+
16
+ ---
17
+
18
+ ## 🚀 Cài đặt
19
+
20
+ Cài đặt global thông qua npm để sử dụng CLI ở mọi nơi:
21
+
22
+ ```bash
23
+ npm install -g pg-local-cli
24
+ ```
25
+
26
+ *(Lưu ý: npm sẽ tự động tải phiên bản PostgreSQL binary phù hợp nhất với OS và kiến trúc CPU của bạn trong quá trình cài đặt).*
27
+
28
+ ---
29
+
30
+ ## 🛠 Hướng dẫn sử dụng
31
+
32
+ Sau khi cài đặt, bạn sẽ có lệnh `pg-local`.
33
+
34
+ ### 1. Khởi động PostgreSQL (Start)
35
+ Khởi chạy instance mặc định (tên là `default`):
36
+
37
+ ```bash
38
+ pg-local start
39
+ ```
40
+
41
+ Khởi chạy một instance với tên cụ thể:
42
+ ```bash
43
+ pg-local start my_app
44
+ ```
45
+ *(Nếu đây là lần đầu chạy, CLI sẽ tự động khởi tạo database cluster - chạy `initdb` trước khi start).*
46
+
47
+ ### 2. Kiểm tra trạng thái (Status)
48
+ Xem thông tin chi tiết của instance đang chạy hay dừng:
49
+
50
+ ```bash
51
+ pg-local status
52
+ ```
53
+ Hoặc xem instance cụ thể:
54
+ ```bash
55
+ pg-local status my_app
56
+ ```
57
+
58
+ ### 3. Dừng PostgreSQL (Stop)
59
+ Dừng instance một cách an toàn và giải phóng tài nguyên:
60
+
61
+ ```bash
62
+ pg-local stop
63
+ ```
64
+ Hoặc:
65
+ ```bash
66
+ pg-local stop my_app
67
+ ```
68
+
69
+ ### 4. Liệt kê các instance (List)
70
+ Xem danh sách toàn bộ các database instance mà bạn đã tạo cùng trạng thái và cổng chạy của chúng:
71
+
72
+ ```bash
73
+ pg-local list
74
+ ```
75
+
76
+ ### 5. Cấu hình Host / Port / User (Config)
77
+ Xem cấu hình hiện tại:
78
+ ```bash
79
+ pg-local config show
80
+ ```
81
+
82
+ Thay đổi các thông số cấu hình (Ví dụ thay đổi port thành `5433` để tránh xung đột cổng):
83
+ ```bash
84
+ pg-local config set port 5433
85
+ ```
86
+
87
+ Thay đổi port cho một instance cụ thể:
88
+ ```bash
89
+ pg-local config set port 15432 my_app
90
+ ```
91
+
92
+ Các khóa cấu hình hỗ trợ:
93
+ - `port`: Cổng TCP chạy database (mặc định: `5432`).
94
+ - `host`: Địa chỉ IP lắng nghe (mặc định: `127.0.0.1`).
95
+ - `user`: Tên tài khoản superuser (mặc định: `postgres`).
96
+ - `password`: Mật khẩu superuser (mặc định: `password`).
97
+ - `databaseDir`: Đường dẫn lưu dữ liệu database (mặc định trong thư mục `~/.pg-local/instances/<name>/data`).
98
+
99
+ ### 6. Xem Log của PostgreSQL (Logs)
100
+ Xem toàn bộ log đã ghi:
101
+ ```bash
102
+ pg-local logs
103
+ ```
104
+
105
+ Theo dõi log thời gian thực (real-time stream) giống như lệnh `tail -f`:
106
+ ```bash
107
+ pg-local logs -f
108
+ # Hoặc cho instance cụ thể
109
+ pg-local logs my_app -f
110
+ ```
111
+
112
+ ### 7. Xóa dữ liệu (Destroy)
113
+ Dừng database và xóa hoàn toàn thư mục chứa dữ liệu của instance đó:
114
+ ```bash
115
+ pg-local destroy
116
+ ```
117
+
118
+ ---
119
+
120
+ ## 🔌 Chuỗi kết nối (Connection String)
121
+
122
+ Mặc định, bạn có thể kết nối tới PostgreSQL bằng chuỗi kết nối:
123
+ ```
124
+ postgresql://postgres:password@127.0.0.1:5432/postgres
125
+ ```
126
+ *(Hoặc cổng mới nếu bạn đã thay đổi trong cấu hình).*
127
+
128
+ Bạn có thể kết nối từ bất kỳ công cụ quản lý nào như **DBeaver**, **TablePlus**, **pgAdmin**, hoặc các ORM như **Prisma**, **Drizzle**, **TypeORM** trong dự án của bạn.
129
+
130
+ ## 📂 Nơi lưu trữ dữ liệu
131
+
132
+ Toàn bộ cấu hình và dữ liệu của công cụ được lưu trữ độc lập tại:
133
+ - **Unix/macOS/Linux**: `~/.pg-local/`
134
+ - **Windows**: `C:\Users\<Tên_User>\.pg-local\`
135
+
136
+ ## 📄 Giấy phép
137
+
138
+ Dự án này được phân phối dưới giấy phép **MIT**.
package/bin/index.js ADDED
@@ -0,0 +1,260 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { Command } = require('commander');
4
+ const fs = require('fs/promises');
5
+ const path = require('path');
6
+ const manager = require('../lib/manager');
7
+ const { getLogPath, saveConfig, loadConfig } = require('../lib/config');
8
+
9
+ const program = new Command();
10
+
11
+ const colors = {
12
+ reset: '\x1b[0m',
13
+ bright: '\x1b[1m',
14
+ dim: '\x1b[2m',
15
+ red: '\x1b[31m',
16
+ green: '\x1b[32m',
17
+ yellow: '\x1b[33m',
18
+ blue: '\x1b[34m',
19
+ magenta: '\x1b[35m',
20
+ cyan: '\x1b[36m',
21
+ };
22
+
23
+ function formatStatus(status) {
24
+ if (status.running) {
25
+ return `${colors.green}● Running (PID: ${status.pid})${colors.reset}`;
26
+ }
27
+ return `${colors.red}○ Stopped${colors.reset}`;
28
+ }
29
+
30
+ program
31
+ .name('pg-local')
32
+ .description('Manage local PostgreSQL instances without Docker/Podman')
33
+ .version('1.0.0');
34
+
35
+ // Start Command
36
+ program
37
+ .command('start [name]')
38
+ .description('Start a local PostgreSQL instance')
39
+ .action(async (name = 'default') => {
40
+ console.log(`${colors.cyan}Starting PostgreSQL instance "${name}"...${colors.reset}`);
41
+ try {
42
+ const result = await manager.startInstance(name);
43
+ if (result.alreadyRunning) {
44
+ console.log(`${colors.yellow}Instance "${name}" is already running (PID: ${result.pid}).${colors.reset}`);
45
+ } else {
46
+ console.log(`${colors.green}✓ PostgreSQL instance "${name}" successfully started!${colors.reset}`);
47
+ }
48
+ console.log(`\nConnection details:`);
49
+ console.log(` ${colors.bright}Host:${colors.reset} ${result.config.host}`);
50
+ console.log(` ${colors.bright}Port:${colors.reset} ${result.config.port}`);
51
+ console.log(` ${colors.bright}User:${colors.reset} ${result.config.user}`);
52
+ console.log(` ${colors.bright}Password:${colors.reset} ${result.config.password}`);
53
+ console.log(` ${colors.bright}URI:${colors.reset} postgresql://${result.config.user}:${result.config.password}@${result.config.host}:${result.config.port}/postgres\n`);
54
+ } catch (err) {
55
+ console.error(`${colors.red}Error: ${err.message}${colors.reset}`);
56
+ process.exit(1);
57
+ }
58
+ });
59
+
60
+ // Stop Command
61
+ program
62
+ .command('stop [name]')
63
+ .description('Stop a local PostgreSQL instance')
64
+ .action(async (name = 'default') => {
65
+ console.log(`${colors.cyan}Stopping PostgreSQL instance "${name}"...${colors.reset}`);
66
+ try {
67
+ const result = await manager.stopInstance(name);
68
+ if (result.alreadyStopped) {
69
+ console.log(`${colors.yellow}Instance "${name}" is already stopped.${colors.reset}`);
70
+ } else if (result.forced) {
71
+ console.log(`${colors.yellow}! Instance "${name}" did not respond to graceful stop. Terminated forcefully.${colors.reset}`);
72
+ } else {
73
+ console.log(`${colors.green}✓ PostgreSQL instance "${name}" stopped gracefully.${colors.reset}`);
74
+ }
75
+ } catch (err) {
76
+ console.error(`${colors.red}Error: ${err.message}${colors.reset}`);
77
+ process.exit(1);
78
+ }
79
+ });
80
+
81
+ // Status Command
82
+ program
83
+ .command('status [name]')
84
+ .description('Check the status of a PostgreSQL instance')
85
+ .action(async (name = 'default') => {
86
+ try {
87
+ const status = await manager.getStatus(name);
88
+ console.log(`${colors.bright}Instance:${colors.reset} ${status.name}`);
89
+ console.log(`${colors.bright}Status:${colors.reset} ${formatStatus(status)}`);
90
+ console.log(`${colors.bright}Host:${colors.reset} ${status.host}`);
91
+ console.log(`${colors.bright}Port:${colors.reset} ${status.port}`);
92
+ console.log(`${colors.bright}User:${colors.reset} ${status.user}`);
93
+ console.log(`${colors.bright}Dir:${colors.reset} ${status.databaseDir}`);
94
+ if (status.running) {
95
+ console.log(`${colors.bright}URI:${colors.reset} ${status.connectionString}`);
96
+ }
97
+ } catch (err) {
98
+ console.error(`${colors.red}Error: ${err.message}${colors.reset}`);
99
+ process.exit(1);
100
+ }
101
+ });
102
+
103
+ // List Command
104
+ program
105
+ .command('list')
106
+ .description('List all local PostgreSQL instances')
107
+ .action(async () => {
108
+ try {
109
+ const instances = await manager.listInstances();
110
+ if (instances.length === 0) {
111
+ console.log(`${colors.yellow}No instances found. Start one with "pg-local start [name]".${colors.reset}`);
112
+ return;
113
+ }
114
+ console.log(`${colors.bright}Local PostgreSQL Instances:${colors.reset}`);
115
+ console.log(''.padEnd(50, '-'));
116
+ for (const inst of instances) {
117
+ const status = await manager.getStatus(inst.name);
118
+ console.log(` ${colors.bright}${inst.name.padEnd(12)}${colors.reset} | ${formatStatus(status).padEnd(20)} | Port: ${inst.port}`);
119
+ }
120
+ console.log(''.padEnd(50, '-'));
121
+ } catch (err) {
122
+ console.error(`${colors.red}Error: ${err.message}${colors.reset}`);
123
+ process.exit(1);
124
+ }
125
+ });
126
+
127
+ // Config Command
128
+ const configCmd = program
129
+ .command('config')
130
+ .description('Manage configuration for PostgreSQL instances');
131
+
132
+ configCmd
133
+ .command('show [name]')
134
+ .description('Show configuration for an instance')
135
+ .action(async (name = 'default') => {
136
+ try {
137
+ const config = await loadConfig(name);
138
+ console.log(JSON.stringify(config, null, 2));
139
+ } catch (err) {
140
+ console.error(`${colors.red}Error: ${err.message}${colors.reset}`);
141
+ process.exit(1);
142
+ }
143
+ });
144
+
145
+ configCmd
146
+ .command('set <key> <value> [name]')
147
+ .description('Set a configuration parameter (key: port, host, user, password, databaseDir)')
148
+ .action(async (key, value, name = 'default') => {
149
+ try {
150
+ const config = await loadConfig(name);
151
+
152
+ // Validation & parsing
153
+ if (key === 'port') {
154
+ const port = parseInt(value, 10);
155
+ if (isNaN(port) || port <= 0 || port > 65535) {
156
+ throw new Error('Port must be a valid number between 1 and 65535.');
157
+ }
158
+ config.port = port;
159
+ } else if (['host', 'user', 'password', 'databaseDir'].includes(key)) {
160
+ config[key] = value;
161
+ } else {
162
+ throw new Error(`Invalid config key "${key}". Valid keys: port, host, user, password, databaseDir`);
163
+ }
164
+
165
+ await saveConfig(name, config);
166
+ console.log(`${colors.green}✓ Saved configuration for "${name}": ${key} = ${value}${colors.reset}`);
167
+ console.log(`${colors.yellow}Note: If the instance is currently running, you must restart it for changes to take effect.${colors.reset}`);
168
+ } catch (err) {
169
+ console.error(`${colors.red}Error: ${err.message}${colors.reset}`);
170
+ process.exit(1);
171
+ }
172
+ });
173
+
174
+ // Logs Command
175
+ program
176
+ .command('logs [name]')
177
+ .description('Show logs of a local PostgreSQL instance')
178
+ .option('-f, --follow', 'Follow log output in real-time')
179
+ .action(async (name = 'default', options) => {
180
+ const logPath = getLogPath(name);
181
+ try {
182
+ const stats = await fs.stat(logPath);
183
+ if (!stats.isFile()) {
184
+ throw new Error('Not a file');
185
+ }
186
+ } catch (err) {
187
+ console.error(`${colors.red}No logs found at ${logPath}${colors.reset}`);
188
+ process.exit(1);
189
+ }
190
+
191
+ if (options.follow) {
192
+ console.log(`${colors.cyan}Following logs for "${name}" (Press Ctrl+C to stop)...${colors.reset}`);
193
+ try {
194
+ let stats = await fs.stat(logPath);
195
+ const initialData = await fs.readFile(logPath, 'utf8');
196
+ process.stdout.write(initialData);
197
+
198
+ let size = stats.size;
199
+ const interval = setInterval(async () => {
200
+ try {
201
+ const newStats = await fs.stat(logPath);
202
+ if (newStats.size > size) {
203
+ const fd = await fs.open(logPath, 'r');
204
+ const buffer = Buffer.alloc(newStats.size - size);
205
+ await fd.read(buffer, 0, buffer.length, size);
206
+ await fd.close();
207
+ process.stdout.write(buffer.toString('utf8'));
208
+ size = newStats.size;
209
+ } else if (newStats.size < size) {
210
+ size = newStats.size;
211
+ }
212
+ } catch (e) {}
213
+ }, 500);
214
+
215
+ process.on('SIGINT', () => {
216
+ clearInterval(interval);
217
+ process.exit(0);
218
+ });
219
+ } catch (err) {
220
+ console.error(`${colors.red}Error reading logs: ${err.message}${colors.reset}`);
221
+ process.exit(1);
222
+ }
223
+ } else {
224
+ const data = await fs.readFile(logPath, 'utf8');
225
+ process.stdout.write(data);
226
+ }
227
+ });
228
+
229
+ // Destroy Command
230
+ program
231
+ .command('destroy [name]')
232
+ .description('Stop instance and delete all data directory')
233
+ .action(async (name = 'default') => {
234
+ const readline = require('readline').createInterface({
235
+ input: process.stdin,
236
+ output: process.stdout,
237
+ });
238
+
239
+ const config = await loadConfig(name);
240
+ readline.question(
241
+ `${colors.red}${colors.bright}WARNING: This will permanently delete all data in "${config.databaseDir}".\nAre you sure you want to proceed? (y/N): ${colors.reset}`,
242
+ async (answer) => {
243
+ readline.close();
244
+ if (answer.toLowerCase() === 'y') {
245
+ console.log(`${colors.cyan}Destroying instance "${name}"...${colors.reset}`);
246
+ try {
247
+ await manager.destroyInstance(name);
248
+ console.log(`${colors.green}✓ Instance "${name}" has been destroyed.${colors.reset}`);
249
+ } catch (err) {
250
+ console.error(`${colors.red}Error: ${err.message}${colors.reset}`);
251
+ process.exit(1);
252
+ }
253
+ } else {
254
+ console.log('Action cancelled.');
255
+ }
256
+ }
257
+ );
258
+ });
259
+
260
+ program.parse(process.argv);
package/lib/config.js ADDED
@@ -0,0 +1,107 @@
1
+ const fs = require('fs/promises');
2
+ const path = require('path');
3
+ const os = require('os');
4
+
5
+ const BASE_DIR = path.join(os.homedir(), '.pg-local');
6
+
7
+ function getInstanceDir(name) {
8
+ return path.join(BASE_DIR, 'instances', name);
9
+ }
10
+
11
+ function getConfigPath(name) {
12
+ return path.join(getInstanceDir(name), 'config.json');
13
+ }
14
+
15
+ function getPidPath(name) {
16
+ return path.join(getInstanceDir(name), 'daemon.pid');
17
+ }
18
+
19
+ function getLogPath(name) {
20
+ return path.join(getInstanceDir(name), 'postgres.log');
21
+ }
22
+
23
+ const DEFAULT_CONFIG = {
24
+ port: 5432,
25
+ host: '127.0.0.1',
26
+ user: 'postgres',
27
+ password: 'password',
28
+ };
29
+
30
+ async function ensureBaseDir() {
31
+ await fs.mkdir(BASE_DIR, { recursive: true });
32
+ await fs.mkdir(path.join(BASE_DIR, 'instances'), { recursive: true });
33
+ }
34
+
35
+ async function loadConfig(name = 'default') {
36
+ await ensureBaseDir();
37
+ const configPath = getConfigPath(name);
38
+ const instanceDir = getInstanceDir(name);
39
+
40
+ let loaded = {};
41
+ try {
42
+ const data = await fs.readFile(configPath, 'utf8');
43
+ loaded = JSON.parse(data);
44
+ } catch (err) {
45
+ // Config doesn't exist yet, we will write defaults
46
+ }
47
+
48
+ // Merge with defaults and set derived properties (like databaseDir)
49
+ const config = {
50
+ name,
51
+ port: loaded.port || DEFAULT_CONFIG.port,
52
+ host: loaded.host || DEFAULT_CONFIG.host,
53
+ user: loaded.user || DEFAULT_CONFIG.user,
54
+ password: loaded.password || DEFAULT_CONFIG.password,
55
+ databaseDir: loaded.databaseDir || path.join(instanceDir, 'data'),
56
+ };
57
+
58
+ return config;
59
+ }
60
+
61
+ async function saveConfig(name, config) {
62
+ await ensureBaseDir();
63
+ const instanceDir = getInstanceDir(name);
64
+ await fs.mkdir(instanceDir, { recursive: true });
65
+
66
+ const configPath = getConfigPath(name);
67
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
68
+ }
69
+
70
+ async function listInstances() {
71
+ await ensureBaseDir();
72
+ const instancesDir = path.join(BASE_DIR, 'instances');
73
+ try {
74
+ const files = await fs.readdir(instancesDir, { withFileTypes: true });
75
+ const list = [];
76
+ for (const file of files) {
77
+ if (file.isDirectory()) {
78
+ try {
79
+ const config = await loadConfig(file.name);
80
+ list.push(config);
81
+ } catch (e) {
82
+ // Ignore invalid directories
83
+ }
84
+ }
85
+ }
86
+ return list;
87
+ } catch (err) {
88
+ return [];
89
+ }
90
+ }
91
+
92
+ async function deleteInstanceDir(name) {
93
+ const instanceDir = getInstanceDir(name);
94
+ await fs.rm(instanceDir, { recursive: true, force: true });
95
+ }
96
+
97
+ module.exports = {
98
+ BASE_DIR,
99
+ getInstanceDir,
100
+ getConfigPath,
101
+ getPidPath,
102
+ getLogPath,
103
+ loadConfig,
104
+ saveConfig,
105
+ listInstances,
106
+ deleteInstanceDir,
107
+ };
package/lib/daemon.js ADDED
@@ -0,0 +1,119 @@
1
+ const fs = require('fs');
2
+ const fsPromises = require('fs/promises');
3
+ const path = require('path');
4
+ const EmbeddedPostgres = require('embedded-postgres').default;
5
+ const { loadConfig, getPidPath, getLogPath, getInstanceDir } = require('./config');
6
+
7
+ const instanceName = process.argv[2] || 'default';
8
+
9
+ async function run() {
10
+ const config = await loadConfig(instanceName);
11
+ const pidPath = getPidPath(instanceName);
12
+ const logPath = getLogPath(instanceName);
13
+ const instanceDir = getInstanceDir(instanceName);
14
+ const stopPath = path.join(instanceDir, 'daemon.stop');
15
+
16
+ // Synchronous log writer to prevent buffering issues on exit
17
+ const writeLog = (msg) => {
18
+ const line = `[${new Date().toISOString()}] ${msg.trim()}\n`;
19
+ try {
20
+ fs.appendFileSync(logPath, line, 'utf8');
21
+ } catch (e) {
22
+ console.error('Failed to write to log file:', e);
23
+ }
24
+ };
25
+
26
+ // Write our PID to file so manager can stop us
27
+ await fsPromises.writeFile(pidPath, process.pid.toString(), 'utf8');
28
+
29
+ // Clean up any stale stop file
30
+ await fsPromises.unlink(stopPath).catch(() => {});
31
+
32
+ writeLog(`Daemon started with PID ${process.pid}`);
33
+
34
+ const pg = new EmbeddedPostgres({
35
+ databaseDir: config.databaseDir,
36
+ user: config.user,
37
+ password: config.password,
38
+ port: config.port,
39
+ persistent: true,
40
+ args: ['-h', config.host],
41
+ onLog: (msg) => writeLog(msg),
42
+ onError: (err) => writeLog(`[ERROR] ${err.message || err}`),
43
+ });
44
+
45
+ let isStopping = false;
46
+ async function gracefulShutdown() {
47
+ if (isStopping) return;
48
+ isStopping = true;
49
+
50
+ if (checkStopInterval) {
51
+ clearInterval(checkStopInterval);
52
+ }
53
+
54
+ writeLog('Received stop signal, shutting down PostgreSQL...');
55
+ try {
56
+ await pg.stop();
57
+ writeLog('PostgreSQL stopped successfully.');
58
+ } catch (err) {
59
+ writeLog(`Error stopping PostgreSQL: ${err.message || err}`);
60
+ }
61
+
62
+ // Clean up pid and stop files
63
+ try {
64
+ await fsPromises.unlink(pidPath);
65
+ } catch (e) {}
66
+ try {
67
+ await fsPromises.unlink(stopPath);
68
+ } catch (e) {}
69
+
70
+ writeLog('Daemon exiting.');
71
+ process.exit(0);
72
+ }
73
+
74
+ // Monitor for the stop file
75
+ const checkStopInterval = setInterval(async () => {
76
+ try {
77
+ await fsPromises.access(stopPath);
78
+ writeLog('Stop file detected.');
79
+ await gracefulShutdown();
80
+ } catch (e) {
81
+ // File not found, keep running
82
+ }
83
+ }, 500);
84
+
85
+ // Handle standard signals
86
+ process.on('SIGTERM', gracefulShutdown);
87
+ process.on('SIGINT', gracefulShutdown);
88
+
89
+ try {
90
+ // If not initialized, initialize it first
91
+ const isInitialized = await fsPromises.access(path.join(config.databaseDir, 'PG_VERSION'))
92
+ .then(() => true)
93
+ .catch(() => false);
94
+
95
+ if (!isInitialized) {
96
+ writeLog('Initializing database cluster...');
97
+ await pg.initialise();
98
+ writeLog('Database cluster initialized.');
99
+ }
100
+
101
+ writeLog(`Starting PostgreSQL on ${config.host}:${config.port}...`);
102
+ await pg.start();
103
+ writeLog('PostgreSQL is ready to accept connections.');
104
+ } catch (err) {
105
+ writeLog(`Fatal startup error: ${err.message || err}`);
106
+ try {
107
+ await fsPromises.unlink(pidPath);
108
+ } catch (e) {}
109
+ try {
110
+ await fsPromises.unlink(stopPath);
111
+ } catch (e) {}
112
+ process.exit(1);
113
+ }
114
+ }
115
+
116
+ run().catch((err) => {
117
+ console.error('Fatal daemon error:', err);
118
+ process.exit(1);
119
+ });
package/lib/manager.js ADDED
@@ -0,0 +1,242 @@
1
+ const fs = require('fs/promises');
2
+ const path = require('path');
3
+ const net = require('net');
4
+ const { spawn, execSync } = require('child_process');
5
+ const {
6
+ loadConfig,
7
+ saveConfig,
8
+ getPidPath,
9
+ getLogPath,
10
+ getInstanceDir,
11
+ deleteInstanceDir,
12
+ listInstances,
13
+ } = require('./config');
14
+
15
+ // Check if a TCP port is open and listening
16
+ function checkPort(port, host, timeout = 1000) {
17
+ return new Promise((resolve) => {
18
+ const socket = new net.Socket();
19
+ const onError = () => {
20
+ socket.destroy();
21
+ resolve(false);
22
+ };
23
+
24
+ socket.setTimeout(timeout);
25
+ socket.once('error', onError);
26
+ socket.once('timeout', onError);
27
+
28
+ socket.connect(port, host, () => {
29
+ socket.end();
30
+ resolve(true);
31
+ });
32
+ });
33
+ }
34
+
35
+ // Check if a process ID is running
36
+ function isPidAlive(pid) {
37
+ if (process.platform === 'win32') {
38
+ try {
39
+ const output = execSync(`tasklist /fi "PID eq ${pid}"`, { encoding: 'utf8' });
40
+ return output.includes(pid.toString());
41
+ } catch (e) {
42
+ return false;
43
+ }
44
+ } else {
45
+ try {
46
+ process.kill(pid, 0);
47
+ return true;
48
+ } catch (err) {
49
+ return err.code === 'EPERM';
50
+ }
51
+ }
52
+ }
53
+
54
+ // Forcefully terminate a process
55
+ function forceKill(pid) {
56
+ if (process.platform === 'win32') {
57
+ try {
58
+ execSync(`taskkill /pid ${pid} /f /t`);
59
+ } catch (e) {}
60
+ } else {
61
+ try {
62
+ process.kill(pid, 'SIGKILL');
63
+ } catch (e) {}
64
+ }
65
+ }
66
+
67
+ // Helper to wait
68
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
69
+
70
+ async function getRunningPid(name) {
71
+ const pidPath = getPidPath(name);
72
+ try {
73
+ const pidStr = await fs.readFile(pidPath, 'utf8');
74
+ const pid = parseInt(pidStr.trim(), 10);
75
+ if (!isNaN(pid) && isPidAlive(pid)) {
76
+ return pid;
77
+ }
78
+ } catch (e) {}
79
+ return null;
80
+ }
81
+
82
+ async function startInstance(name) {
83
+ const config = await loadConfig(name);
84
+
85
+ // Check if already running
86
+ const runningPid = await getRunningPid(name);
87
+ if (runningPid) {
88
+ return {
89
+ success: true,
90
+ alreadyRunning: true,
91
+ pid: runningPid,
92
+ config,
93
+ };
94
+ }
95
+
96
+ // Check if port is already in use by another process
97
+ const portInUse = await checkPort(config.port, config.host);
98
+ if (portInUse) {
99
+ throw new Error(`Port ${config.port} is already in use by another process on ${config.host}. Choose a different port using: pg-local config set port <new_port> ${name}`);
100
+ }
101
+
102
+ // Ensure directories are clean
103
+ const instanceDir = getInstanceDir(name);
104
+ await fs.mkdir(instanceDir, { recursive: true });
105
+
106
+ const stopPath = path.join(instanceDir, 'daemon.stop');
107
+ await fs.unlink(stopPath).catch(() => {});
108
+
109
+ // Spawn daemon in background
110
+ const daemonPath = path.join(__dirname, 'daemon.js');
111
+ const child = spawn(process.execPath, [daemonPath, name], {
112
+ detached: true,
113
+ stdio: 'ignore',
114
+ });
115
+ child.unref();
116
+
117
+ // Wait for the server to become ready (up to 15 seconds)
118
+ const startTime = Date.now();
119
+ const timeoutMs = 15000;
120
+ let isReady = false;
121
+
122
+ while (Date.now() - startTime < timeoutMs) {
123
+ // Check if the PID file exists and is alive
124
+ const currentPid = await getRunningPid(name);
125
+ if (currentPid) {
126
+ // Check if the port is open now
127
+ const portOpen = await checkPort(config.port, config.host);
128
+ if (portOpen) {
129
+ isReady = true;
130
+ break;
131
+ }
132
+ } else {
133
+ // Check if the daemon process died
134
+ try {
135
+ const pidStr = await fs.readFile(getPidPath(name), 'utf8');
136
+ const pid = parseInt(pidStr.trim(), 10);
137
+ if (!isNaN(pid) && !isPidAlive(pid)) {
138
+ // Daemon was started but is now dead!
139
+ break;
140
+ }
141
+ } catch (e) {
142
+ // PID file doesn't exist yet, or is empty. Keep waiting.
143
+ }
144
+ }
145
+ await sleep(200);
146
+ }
147
+
148
+ if (!isReady) {
149
+ // Check if the process exited or logged errors
150
+ const pid = await getRunningPid(name);
151
+ if (!pid) {
152
+ throw new Error(`PostgreSQL daemon failed to start. Check logs at: ${getLogPath(name)}`);
153
+ } else {
154
+ throw new Error(`PostgreSQL started but did not respond on ${config.host}:${config.port} within 15 seconds.`);
155
+ }
156
+ }
157
+
158
+ const pid = await getRunningPid(name);
159
+ return {
160
+ success: true,
161
+ alreadyRunning: false,
162
+ pid,
163
+ config,
164
+ };
165
+ }
166
+
167
+ async function stopInstance(name) {
168
+ const pid = await getRunningPid(name);
169
+ if (!pid) {
170
+ return { success: true, alreadyStopped: true };
171
+ }
172
+
173
+ const instanceDir = getInstanceDir(name);
174
+ const stopPath = path.join(instanceDir, 'daemon.stop');
175
+
176
+ // Request graceful shutdown by creating the stop file
177
+ await fs.writeFile(stopPath, 'stop', 'utf8');
178
+
179
+ // Also send SIGTERM for Unix systems as secondary trigger
180
+ if (process.platform !== 'win32') {
181
+ try {
182
+ process.kill(pid, 'SIGTERM');
183
+ } catch (e) {}
184
+ }
185
+
186
+ // Wait for it to stop (up to 8 seconds)
187
+ const startTime = Date.now();
188
+ const timeoutMs = 8000;
189
+ let stopped = false;
190
+
191
+ while (Date.now() - startTime < timeoutMs) {
192
+ if (!isPidAlive(pid)) {
193
+ stopped = true;
194
+ break;
195
+ }
196
+ await sleep(200);
197
+ }
198
+
199
+ // If not stopped, force kill
200
+ if (!stopped) {
201
+ forceKill(pid);
202
+ // Cleanup files
203
+ const pidPath = getPidPath(name);
204
+ await fs.unlink(pidPath).catch(() => {});
205
+ await fs.unlink(stopPath).catch(() => {});
206
+ return { success: true, forced: true };
207
+ }
208
+
209
+ return { success: true, forced: false };
210
+ }
211
+
212
+ async function getStatus(name) {
213
+ const config = await loadConfig(name);
214
+ const pid = await getRunningPid(name);
215
+ const isPortOpen = await checkPort(config.port, config.host);
216
+
217
+ return {
218
+ name,
219
+ running: !!pid && isPortOpen,
220
+ pid,
221
+ port: config.port,
222
+ host: config.host,
223
+ user: config.user,
224
+ databaseDir: config.databaseDir,
225
+ connectionString: `postgresql://${config.user}:${config.password}@${config.host}:${config.port}/postgres`,
226
+ };
227
+ }
228
+
229
+ async function destroyInstance(name) {
230
+ await stopInstance(name);
231
+ await deleteInstanceDir(name);
232
+ }
233
+
234
+ module.exports = {
235
+ startInstance,
236
+ stopInstance,
237
+ getStatus,
238
+ destroyInstance,
239
+ loadConfig,
240
+ saveConfig,
241
+ listInstances,
242
+ };
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "pg-local-cli",
3
+ "version": "1.0.0",
4
+ "description": "A Node.js CLI tool to easily create, start, and manage local PostgreSQL instances without Docker, Podman, or system dependencies.",
5
+ "main": "bin/index.js",
6
+ "bin": {
7
+ "pg-local": "bin/index.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=16"
11
+ },
12
+ "scripts": {
13
+ "test": "echo \"Error: no test specified\" && exit 1"
14
+ },
15
+ "keywords": [
16
+ "postgres",
17
+ "postgresql",
18
+ "local",
19
+ "cli",
20
+ "database",
21
+ "embedded",
22
+ "without-docker",
23
+ "development"
24
+ ],
25
+ "author": "Antigravity Dev",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "commander": "^12.0.0",
29
+ "embedded-postgres": "^18.4.0-beta.17"
30
+ }
31
+ }