pgpm 5.2.3 → 5.3.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/commands/docker.js +23 -20
- package/commands/doctor.d.ts +3 -0
- package/commands/doctor.js +104 -0
- package/commands.js +5 -3
- package/esm/commands/docker.js +23 -20
- package/esm/commands/doctor.js +102 -0
- package/esm/commands.js +5 -3
- package/esm/utils/display.js +1 -0
- package/esm/utils/doctor.js +248 -0
- package/esm/utils/index.js +3 -2
- package/package.json +3 -3
- package/utils/display.d.ts +1 -1
- package/utils/display.js +1 -0
- package/utils/doctor.d.ts +43 -0
- package/utils/doctor.js +265 -0
- package/utils/index.d.ts +3 -2
- package/utils/index.js +3 -2
package/commands/docker.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const child_process_1 = require("child_process");
|
|
4
4
|
const inquirerer_1 = require("inquirerer");
|
|
5
|
+
const doctor_1 = require("../utils/doctor");
|
|
5
6
|
const dockerUsageText = `
|
|
6
7
|
Docker Command:
|
|
7
8
|
|
|
@@ -52,25 +53,25 @@ const ADDITIONAL_SERVICES = {
|
|
|
52
53
|
image: 'minio/minio',
|
|
53
54
|
ports: [
|
|
54
55
|
{ host: 9000, container: 9000 },
|
|
55
|
-
{ host: 9001, container: 9001 }
|
|
56
|
+
{ host: 9001, container: 9001 }
|
|
56
57
|
],
|
|
57
58
|
env: {
|
|
58
59
|
MINIO_ROOT_USER: 'minioadmin',
|
|
59
|
-
MINIO_ROOT_PASSWORD: 'minioadmin'
|
|
60
|
+
MINIO_ROOT_PASSWORD: 'minioadmin'
|
|
60
61
|
},
|
|
61
62
|
command: ['server', '/data', '--console-address', ':9001'],
|
|
62
|
-
volumes: [{ name: 'minio-data', containerPath: '/data' }]
|
|
63
|
+
volumes: [{ name: 'minio-data', containerPath: '/data' }]
|
|
63
64
|
},
|
|
64
65
|
ollama: {
|
|
65
66
|
name: 'ollama',
|
|
66
67
|
image: 'ollama/ollama',
|
|
67
68
|
ports: [
|
|
68
|
-
{ host: 11434, container: 11434 }
|
|
69
|
+
{ host: 11434, container: 11434 }
|
|
69
70
|
],
|
|
70
71
|
env: {},
|
|
71
72
|
volumes: [{ name: 'ollama-data', containerPath: '/root/.ollama' }],
|
|
72
|
-
gpuCapable: true
|
|
73
|
-
}
|
|
73
|
+
gpuCapable: true
|
|
74
|
+
}
|
|
74
75
|
};
|
|
75
76
|
function run(command, args, options = {}) {
|
|
76
77
|
return new Promise((resolve, reject) => {
|
|
@@ -98,14 +99,19 @@ function run(command, args, options = {}) {
|
|
|
98
99
|
});
|
|
99
100
|
});
|
|
100
101
|
}
|
|
101
|
-
async function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return
|
|
102
|
+
async function ensureDockerReady() {
|
|
103
|
+
const status = await (0, doctor_1.getDockerStatus)();
|
|
104
|
+
if (status.binary && status.daemon) {
|
|
105
|
+
return true;
|
|
105
106
|
}
|
|
106
|
-
|
|
107
|
-
|
|
107
|
+
const platformInfo = (0, doctor_1.detectPlatform)();
|
|
108
|
+
if (!status.binary) {
|
|
109
|
+
await (0, inquirerer_1.cliExitWithError)(`Docker is not installed or not available in PATH.\n${(0, doctor_1.dockerInstallGuidance)(platformInfo)}`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
await (0, inquirerer_1.cliExitWithError)((0, doctor_1.dockerDaemonGuidance)(platformInfo));
|
|
108
113
|
}
|
|
114
|
+
return false;
|
|
109
115
|
}
|
|
110
116
|
async function isContainerRunning(name) {
|
|
111
117
|
try {
|
|
@@ -130,9 +136,7 @@ async function containerExists(name) {
|
|
|
130
136
|
}
|
|
131
137
|
async function startContainer(options) {
|
|
132
138
|
const { name, image, port, user, password, shmSize, recreate } = options;
|
|
133
|
-
|
|
134
|
-
if (!dockerAvailable) {
|
|
135
|
-
await (0, inquirerer_1.cliExitWithError)('Docker is not installed or not available in PATH. Please install Docker first.');
|
|
139
|
+
if (!(await ensureDockerReady())) {
|
|
136
140
|
return;
|
|
137
141
|
}
|
|
138
142
|
const exists = await containerExists(name);
|
|
@@ -183,9 +187,7 @@ async function startContainer(options) {
|
|
|
183
187
|
}
|
|
184
188
|
}
|
|
185
189
|
async function stopContainer(name) {
|
|
186
|
-
|
|
187
|
-
if (!dockerAvailable) {
|
|
188
|
-
await (0, inquirerer_1.cliExitWithError)('Docker is not installed or not available in PATH. Please install Docker first.');
|
|
190
|
+
if (!(await ensureDockerReady())) {
|
|
189
191
|
return;
|
|
190
192
|
}
|
|
191
193
|
const exists = await containerExists(name);
|
|
@@ -238,7 +240,7 @@ async function startService(service, recreate, gpu = false) {
|
|
|
238
240
|
const runArgs = [
|
|
239
241
|
'run',
|
|
240
242
|
'-d',
|
|
241
|
-
'--name', name
|
|
243
|
+
'--name', name
|
|
242
244
|
];
|
|
243
245
|
for (const [key, value] of Object.entries(serviceEnv)) {
|
|
244
246
|
runArgs.push('-e', `${key}=${value}`);
|
|
@@ -282,7 +284,8 @@ function resolveServiceFlags(args) {
|
|
|
282
284
|
return services;
|
|
283
285
|
}
|
|
284
286
|
async function listServices() {
|
|
285
|
-
const
|
|
287
|
+
const dockerStatus = await (0, doctor_1.getDockerStatus)();
|
|
288
|
+
const dockerAvailable = dockerStatus.binary && dockerStatus.daemon;
|
|
286
289
|
console.log('\nAvailable services:\n');
|
|
287
290
|
console.log(' Primary:');
|
|
288
291
|
if (dockerAvailable) {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const child_process_1 = require("child_process");
|
|
4
|
+
const pg_env_1 = require("pg-env");
|
|
5
|
+
const doctor_1 = require("../utils/doctor");
|
|
6
|
+
const doctorUsageText = `
|
|
7
|
+
Doctor Command:
|
|
8
|
+
|
|
9
|
+
pgpm doctor [OPTIONS]
|
|
10
|
+
|
|
11
|
+
Check that your machine has the dependencies pgpm needs
|
|
12
|
+
(Node.js, Docker, psql) and report OS-specific installation
|
|
13
|
+
guidance for anything that is missing.
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--db Also check PostgreSQL connectivity using the current environment
|
|
17
|
+
--json Output results as JSON
|
|
18
|
+
--help, -h Show this help message
|
|
19
|
+
|
|
20
|
+
Exit Codes:
|
|
21
|
+
0 All checks passed (warnings allowed)
|
|
22
|
+
1 One or more checks failed
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
pgpm doctor Check node, docker, docker compose, and psql
|
|
26
|
+
pgpm doctor --db Also verify a PostgreSQL connection
|
|
27
|
+
pgpm doctor --json Machine-readable output (for CI)
|
|
28
|
+
`;
|
|
29
|
+
const STATUS_ICONS = {
|
|
30
|
+
pass: '✅',
|
|
31
|
+
warn: '⚠️ ',
|
|
32
|
+
fail: '❌'
|
|
33
|
+
};
|
|
34
|
+
async function checkDatabaseConnectivity() {
|
|
35
|
+
const pg = (0, pg_env_1.getPgEnvOptions)();
|
|
36
|
+
const target = `${pg.host}:${pg.port}/${pg.database} (user: ${pg.user})`;
|
|
37
|
+
const result = await new Promise((resolve) => {
|
|
38
|
+
const child = (0, child_process_1.spawn)('psql', ['-X', '-A', '-t', '-c', 'SELECT 1'], {
|
|
39
|
+
stdio: 'pipe',
|
|
40
|
+
env: (0, pg_env_1.getSpawnEnvWithPg)(pg)
|
|
41
|
+
});
|
|
42
|
+
let stderr = '';
|
|
43
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
44
|
+
child.on('error', () => resolve({ code: 127, stderr: 'psql: command not found' }));
|
|
45
|
+
child.on('close', (code) => resolve({ code: code ?? 0, stderr: stderr.trim() }));
|
|
46
|
+
});
|
|
47
|
+
if (result.code === 0) {
|
|
48
|
+
return {
|
|
49
|
+
name: 'database',
|
|
50
|
+
status: 'pass',
|
|
51
|
+
message: `connected to PostgreSQL at ${target}`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
name: 'database',
|
|
56
|
+
status: 'fail',
|
|
57
|
+
message: `could not connect to PostgreSQL at ${target}: ${result.stderr}`,
|
|
58
|
+
remediation: 'Start a local PostgreSQL with `pgpm docker start`, or point PGHOST/PGPORT/PGUSER/PGPASSWORD at a running server (see `pgpm env`).'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function printResults(results) {
|
|
62
|
+
console.log('');
|
|
63
|
+
for (const result of results) {
|
|
64
|
+
console.log(` ${STATUS_ICONS[result.status]} ${result.name.padEnd(16)} ${result.message}`);
|
|
65
|
+
if (result.remediation && result.status !== 'pass') {
|
|
66
|
+
console.log(` ↳ ${result.remediation}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const { failed, warned, passed } = (0, doctor_1.summarizeChecks)(results);
|
|
70
|
+
console.log('');
|
|
71
|
+
console.log(` ${passed} passed, ${warned} warning(s), ${failed} failed`);
|
|
72
|
+
console.log('');
|
|
73
|
+
}
|
|
74
|
+
exports.default = async (argv, _prompter, _options) => {
|
|
75
|
+
if (argv.help || argv.h) {
|
|
76
|
+
console.log(doctorUsageText);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
const platformInfo = (0, doctor_1.detectPlatform)();
|
|
80
|
+
const results = [
|
|
81
|
+
(0, doctor_1.checkNode)(),
|
|
82
|
+
await (0, doctor_1.checkDocker)(platformInfo, doctor_1.runCommand),
|
|
83
|
+
await (0, doctor_1.checkDockerCompose)(platformInfo, doctor_1.runCommand),
|
|
84
|
+
await (0, doctor_1.checkPsql)(platformInfo, doctor_1.runCommand)
|
|
85
|
+
];
|
|
86
|
+
if (argv.db === true) {
|
|
87
|
+
results.push(await checkDatabaseConnectivity());
|
|
88
|
+
}
|
|
89
|
+
if (argv.json === true) {
|
|
90
|
+
const { failed, warned, passed } = (0, doctor_1.summarizeChecks)(results);
|
|
91
|
+
console.log(JSON.stringify({
|
|
92
|
+
platform: platformInfo,
|
|
93
|
+
checks: results,
|
|
94
|
+
summary: { passed, warned, failed }
|
|
95
|
+
}, null, 2));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
printResults(results);
|
|
99
|
+
}
|
|
100
|
+
const { failed } = (0, doctor_1.summarizeChecks)(results);
|
|
101
|
+
if (failed > 0) {
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
};
|
package/commands.js
CHANGED
|
@@ -14,6 +14,7 @@ const cache_1 = __importDefault(require("./commands/cache"));
|
|
|
14
14
|
const clear_1 = __importDefault(require("./commands/clear"));
|
|
15
15
|
const deploy_1 = __importDefault(require("./commands/deploy"));
|
|
16
16
|
const docker_1 = __importDefault(require("./commands/docker"));
|
|
17
|
+
const doctor_1 = __importDefault(require("./commands/doctor"));
|
|
17
18
|
const dump_1 = __importDefault(require("./commands/dump"));
|
|
18
19
|
const env_1 = __importDefault(require("./commands/env"));
|
|
19
20
|
const export_1 = __importDefault(require("./commands/export"));
|
|
@@ -24,8 +25,6 @@ const kill_1 = __importDefault(require("./commands/kill"));
|
|
|
24
25
|
const migrate_1 = __importDefault(require("./commands/migrate"));
|
|
25
26
|
const package_1 = __importDefault(require("./commands/package"));
|
|
26
27
|
const plan_1 = __importDefault(require("./commands/plan"));
|
|
27
|
-
const update_1 = __importDefault(require("./commands/update"));
|
|
28
|
-
const upgrade_1 = __importDefault(require("./commands/upgrade"));
|
|
29
28
|
const remove_1 = __importDefault(require("./commands/remove"));
|
|
30
29
|
const rename_1 = __importDefault(require("./commands/rename"));
|
|
31
30
|
const revert_1 = __importDefault(require("./commands/revert"));
|
|
@@ -33,6 +32,8 @@ const slice_1 = __importDefault(require("./commands/slice"));
|
|
|
33
32
|
const tag_1 = __importDefault(require("./commands/tag"));
|
|
34
33
|
const test_packages_1 = __importDefault(require("./commands/test-packages"));
|
|
35
34
|
const tune_1 = __importDefault(require("./commands/tune"));
|
|
35
|
+
const update_1 = __importDefault(require("./commands/update"));
|
|
36
|
+
const upgrade_1 = __importDefault(require("./commands/upgrade"));
|
|
36
37
|
const verify_1 = __importDefault(require("./commands/verify"));
|
|
37
38
|
const utils_2 = require("./utils");
|
|
38
39
|
const withPgTeardown = (fn, skipTeardown = false) => async (...args) => {
|
|
@@ -53,6 +54,7 @@ const createPgpmCommandMap = (skipPgTeardown = false) => {
|
|
|
53
54
|
clear: pgt(clear_1.default),
|
|
54
55
|
deploy: pgt(deploy_1.default),
|
|
55
56
|
docker: docker_1.default,
|
|
57
|
+
doctor: doctor_1.default,
|
|
56
58
|
dump: pgt(dump_1.default),
|
|
57
59
|
env: env_1.default,
|
|
58
60
|
verify: pgt(verify_1.default),
|
|
@@ -114,7 +116,7 @@ const commands = async (argv, prompter, options) => {
|
|
|
114
116
|
const updateResult = await (0, utils_1.checkForUpdates)({
|
|
115
117
|
pkgName: pkg.name,
|
|
116
118
|
pkgVersion: pkg.version,
|
|
117
|
-
toolName: 'pgpm'
|
|
119
|
+
toolName: 'pgpm'
|
|
118
120
|
});
|
|
119
121
|
if (updateResult.hasUpdate && updateResult.message) {
|
|
120
122
|
console.warn(updateResult.message);
|
package/esm/commands/docker.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import { cliExitWithError, extractFirst } from 'inquirerer';
|
|
3
|
+
import { detectPlatform, dockerDaemonGuidance, dockerInstallGuidance, getDockerStatus } from '../utils/doctor';
|
|
3
4
|
const dockerUsageText = `
|
|
4
5
|
Docker Command:
|
|
5
6
|
|
|
@@ -50,25 +51,25 @@ const ADDITIONAL_SERVICES = {
|
|
|
50
51
|
image: 'minio/minio',
|
|
51
52
|
ports: [
|
|
52
53
|
{ host: 9000, container: 9000 },
|
|
53
|
-
{ host: 9001, container: 9001 }
|
|
54
|
+
{ host: 9001, container: 9001 }
|
|
54
55
|
],
|
|
55
56
|
env: {
|
|
56
57
|
MINIO_ROOT_USER: 'minioadmin',
|
|
57
|
-
MINIO_ROOT_PASSWORD: 'minioadmin'
|
|
58
|
+
MINIO_ROOT_PASSWORD: 'minioadmin'
|
|
58
59
|
},
|
|
59
60
|
command: ['server', '/data', '--console-address', ':9001'],
|
|
60
|
-
volumes: [{ name: 'minio-data', containerPath: '/data' }]
|
|
61
|
+
volumes: [{ name: 'minio-data', containerPath: '/data' }]
|
|
61
62
|
},
|
|
62
63
|
ollama: {
|
|
63
64
|
name: 'ollama',
|
|
64
65
|
image: 'ollama/ollama',
|
|
65
66
|
ports: [
|
|
66
|
-
{ host: 11434, container: 11434 }
|
|
67
|
+
{ host: 11434, container: 11434 }
|
|
67
68
|
],
|
|
68
69
|
env: {},
|
|
69
70
|
volumes: [{ name: 'ollama-data', containerPath: '/root/.ollama' }],
|
|
70
|
-
gpuCapable: true
|
|
71
|
-
}
|
|
71
|
+
gpuCapable: true
|
|
72
|
+
}
|
|
72
73
|
};
|
|
73
74
|
function run(command, args, options = {}) {
|
|
74
75
|
return new Promise((resolve, reject) => {
|
|
@@ -96,14 +97,19 @@ function run(command, args, options = {}) {
|
|
|
96
97
|
});
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
|
-
async function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return
|
|
100
|
+
async function ensureDockerReady() {
|
|
101
|
+
const status = await getDockerStatus();
|
|
102
|
+
if (status.binary && status.daemon) {
|
|
103
|
+
return true;
|
|
103
104
|
}
|
|
104
|
-
|
|
105
|
-
|
|
105
|
+
const platformInfo = detectPlatform();
|
|
106
|
+
if (!status.binary) {
|
|
107
|
+
await cliExitWithError(`Docker is not installed or not available in PATH.\n${dockerInstallGuidance(platformInfo)}`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
await cliExitWithError(dockerDaemonGuidance(platformInfo));
|
|
106
111
|
}
|
|
112
|
+
return false;
|
|
107
113
|
}
|
|
108
114
|
async function isContainerRunning(name) {
|
|
109
115
|
try {
|
|
@@ -128,9 +134,7 @@ async function containerExists(name) {
|
|
|
128
134
|
}
|
|
129
135
|
async function startContainer(options) {
|
|
130
136
|
const { name, image, port, user, password, shmSize, recreate } = options;
|
|
131
|
-
|
|
132
|
-
if (!dockerAvailable) {
|
|
133
|
-
await cliExitWithError('Docker is not installed or not available in PATH. Please install Docker first.');
|
|
137
|
+
if (!(await ensureDockerReady())) {
|
|
134
138
|
return;
|
|
135
139
|
}
|
|
136
140
|
const exists = await containerExists(name);
|
|
@@ -181,9 +185,7 @@ async function startContainer(options) {
|
|
|
181
185
|
}
|
|
182
186
|
}
|
|
183
187
|
async function stopContainer(name) {
|
|
184
|
-
|
|
185
|
-
if (!dockerAvailable) {
|
|
186
|
-
await cliExitWithError('Docker is not installed or not available in PATH. Please install Docker first.');
|
|
188
|
+
if (!(await ensureDockerReady())) {
|
|
187
189
|
return;
|
|
188
190
|
}
|
|
189
191
|
const exists = await containerExists(name);
|
|
@@ -236,7 +238,7 @@ async function startService(service, recreate, gpu = false) {
|
|
|
236
238
|
const runArgs = [
|
|
237
239
|
'run',
|
|
238
240
|
'-d',
|
|
239
|
-
'--name', name
|
|
241
|
+
'--name', name
|
|
240
242
|
];
|
|
241
243
|
for (const [key, value] of Object.entries(serviceEnv)) {
|
|
242
244
|
runArgs.push('-e', `${key}=${value}`);
|
|
@@ -280,7 +282,8 @@ function resolveServiceFlags(args) {
|
|
|
280
282
|
return services;
|
|
281
283
|
}
|
|
282
284
|
async function listServices() {
|
|
283
|
-
const
|
|
285
|
+
const dockerStatus = await getDockerStatus();
|
|
286
|
+
const dockerAvailable = dockerStatus.binary && dockerStatus.daemon;
|
|
284
287
|
console.log('\nAvailable services:\n');
|
|
285
288
|
console.log(' Primary:');
|
|
286
289
|
if (dockerAvailable) {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { getPgEnvOptions, getSpawnEnvWithPg } from 'pg-env';
|
|
3
|
+
import { checkDocker, checkDockerCompose, checkNode, checkPsql, detectPlatform, runCommand, summarizeChecks } from '../utils/doctor';
|
|
4
|
+
const doctorUsageText = `
|
|
5
|
+
Doctor Command:
|
|
6
|
+
|
|
7
|
+
pgpm doctor [OPTIONS]
|
|
8
|
+
|
|
9
|
+
Check that your machine has the dependencies pgpm needs
|
|
10
|
+
(Node.js, Docker, psql) and report OS-specific installation
|
|
11
|
+
guidance for anything that is missing.
|
|
12
|
+
|
|
13
|
+
Options:
|
|
14
|
+
--db Also check PostgreSQL connectivity using the current environment
|
|
15
|
+
--json Output results as JSON
|
|
16
|
+
--help, -h Show this help message
|
|
17
|
+
|
|
18
|
+
Exit Codes:
|
|
19
|
+
0 All checks passed (warnings allowed)
|
|
20
|
+
1 One or more checks failed
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
pgpm doctor Check node, docker, docker compose, and psql
|
|
24
|
+
pgpm doctor --db Also verify a PostgreSQL connection
|
|
25
|
+
pgpm doctor --json Machine-readable output (for CI)
|
|
26
|
+
`;
|
|
27
|
+
const STATUS_ICONS = {
|
|
28
|
+
pass: '✅',
|
|
29
|
+
warn: '⚠️ ',
|
|
30
|
+
fail: '❌'
|
|
31
|
+
};
|
|
32
|
+
async function checkDatabaseConnectivity() {
|
|
33
|
+
const pg = getPgEnvOptions();
|
|
34
|
+
const target = `${pg.host}:${pg.port}/${pg.database} (user: ${pg.user})`;
|
|
35
|
+
const result = await new Promise((resolve) => {
|
|
36
|
+
const child = spawn('psql', ['-X', '-A', '-t', '-c', 'SELECT 1'], {
|
|
37
|
+
stdio: 'pipe',
|
|
38
|
+
env: getSpawnEnvWithPg(pg)
|
|
39
|
+
});
|
|
40
|
+
let stderr = '';
|
|
41
|
+
child.stderr?.on('data', (d) => { stderr += d.toString(); });
|
|
42
|
+
child.on('error', () => resolve({ code: 127, stderr: 'psql: command not found' }));
|
|
43
|
+
child.on('close', (code) => resolve({ code: code ?? 0, stderr: stderr.trim() }));
|
|
44
|
+
});
|
|
45
|
+
if (result.code === 0) {
|
|
46
|
+
return {
|
|
47
|
+
name: 'database',
|
|
48
|
+
status: 'pass',
|
|
49
|
+
message: `connected to PostgreSQL at ${target}`
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
name: 'database',
|
|
54
|
+
status: 'fail',
|
|
55
|
+
message: `could not connect to PostgreSQL at ${target}: ${result.stderr}`,
|
|
56
|
+
remediation: 'Start a local PostgreSQL with `pgpm docker start`, or point PGHOST/PGPORT/PGUSER/PGPASSWORD at a running server (see `pgpm env`).'
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function printResults(results) {
|
|
60
|
+
console.log('');
|
|
61
|
+
for (const result of results) {
|
|
62
|
+
console.log(` ${STATUS_ICONS[result.status]} ${result.name.padEnd(16)} ${result.message}`);
|
|
63
|
+
if (result.remediation && result.status !== 'pass') {
|
|
64
|
+
console.log(` ↳ ${result.remediation}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const { failed, warned, passed } = summarizeChecks(results);
|
|
68
|
+
console.log('');
|
|
69
|
+
console.log(` ${passed} passed, ${warned} warning(s), ${failed} failed`);
|
|
70
|
+
console.log('');
|
|
71
|
+
}
|
|
72
|
+
export default async (argv, _prompter, _options) => {
|
|
73
|
+
if (argv.help || argv.h) {
|
|
74
|
+
console.log(doctorUsageText);
|
|
75
|
+
process.exit(0);
|
|
76
|
+
}
|
|
77
|
+
const platformInfo = detectPlatform();
|
|
78
|
+
const results = [
|
|
79
|
+
checkNode(),
|
|
80
|
+
await checkDocker(platformInfo, runCommand),
|
|
81
|
+
await checkDockerCompose(platformInfo, runCommand),
|
|
82
|
+
await checkPsql(platformInfo, runCommand)
|
|
83
|
+
];
|
|
84
|
+
if (argv.db === true) {
|
|
85
|
+
results.push(await checkDatabaseConnectivity());
|
|
86
|
+
}
|
|
87
|
+
if (argv.json === true) {
|
|
88
|
+
const { failed, warned, passed } = summarizeChecks(results);
|
|
89
|
+
console.log(JSON.stringify({
|
|
90
|
+
platform: platformInfo,
|
|
91
|
+
checks: results,
|
|
92
|
+
summary: { passed, warned, failed }
|
|
93
|
+
}, null, 2));
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
printResults(results);
|
|
97
|
+
}
|
|
98
|
+
const { failed } = summarizeChecks(results);
|
|
99
|
+
if (failed > 0) {
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
};
|
package/esm/commands.js
CHANGED
|
@@ -8,6 +8,7 @@ import cache from './commands/cache';
|
|
|
8
8
|
import clear from './commands/clear';
|
|
9
9
|
import deploy from './commands/deploy';
|
|
10
10
|
import docker from './commands/docker';
|
|
11
|
+
import doctor from './commands/doctor';
|
|
11
12
|
import dump from './commands/dump';
|
|
12
13
|
import env from './commands/env';
|
|
13
14
|
import _export from './commands/export';
|
|
@@ -18,8 +19,6 @@ import kill from './commands/kill';
|
|
|
18
19
|
import migrate from './commands/migrate';
|
|
19
20
|
import _package from './commands/package';
|
|
20
21
|
import plan from './commands/plan';
|
|
21
|
-
import updateCmd from './commands/update';
|
|
22
|
-
import upgrade from './commands/upgrade';
|
|
23
22
|
import remove from './commands/remove';
|
|
24
23
|
import renameCmd from './commands/rename';
|
|
25
24
|
import revert from './commands/revert';
|
|
@@ -27,6 +26,8 @@ import slice from './commands/slice';
|
|
|
27
26
|
import tag from './commands/tag';
|
|
28
27
|
import testPackages from './commands/test-packages';
|
|
29
28
|
import tune from './commands/tune';
|
|
29
|
+
import updateCmd from './commands/update';
|
|
30
|
+
import upgrade from './commands/upgrade';
|
|
30
31
|
import verify from './commands/verify';
|
|
31
32
|
import { usageText } from './utils';
|
|
32
33
|
const withPgTeardown = (fn, skipTeardown = false) => async (...args) => {
|
|
@@ -47,6 +48,7 @@ export const createPgpmCommandMap = (skipPgTeardown = false) => {
|
|
|
47
48
|
clear: pgt(clear),
|
|
48
49
|
deploy: pgt(deploy),
|
|
49
50
|
docker,
|
|
51
|
+
doctor,
|
|
50
52
|
dump: pgt(dump),
|
|
51
53
|
env,
|
|
52
54
|
verify: pgt(verify),
|
|
@@ -107,7 +109,7 @@ export const commands = async (argv, prompter, options) => {
|
|
|
107
109
|
const updateResult = await checkForUpdates({
|
|
108
110
|
pkgName: pkg.name,
|
|
109
111
|
pkgVersion: pkg.version,
|
|
110
|
-
toolName: 'pgpm'
|
|
112
|
+
toolName: 'pgpm'
|
|
111
113
|
});
|
|
112
114
|
if (updateResult.hasUpdate && updateResult.message) {
|
|
113
115
|
console.warn(updateResult.message);
|
package/esm/utils/display.js
CHANGED
|
@@ -41,6 +41,7 @@ export const usageText = `
|
|
|
41
41
|
|
|
42
42
|
Development Tools:
|
|
43
43
|
docker Manage Docker containers (start/stop/ls, --minio)
|
|
44
|
+
doctor Check local dependencies (node, docker, psql) with install guidance
|
|
44
45
|
env Manage environment variables (--supabase, --minio)
|
|
45
46
|
test-packages Run integration tests on workspace packages
|
|
46
47
|
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
export const runCommand = (command, args) => {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
const child = spawn(command, args, { stdio: 'pipe' });
|
|
6
|
+
let stdout = '';
|
|
7
|
+
let stderr = '';
|
|
8
|
+
child.stdout?.on('data', (data) => {
|
|
9
|
+
stdout += data.toString();
|
|
10
|
+
});
|
|
11
|
+
child.stderr?.on('data', (data) => {
|
|
12
|
+
stderr += data.toString();
|
|
13
|
+
});
|
|
14
|
+
child.on('error', () => {
|
|
15
|
+
resolve({ code: 127, stdout: '', stderr: `${command}: command not found` });
|
|
16
|
+
});
|
|
17
|
+
child.on('close', (code) => {
|
|
18
|
+
resolve({ code: code ?? 0, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
export function detectPlatform(platform = process.platform, readFile = (p) => readFileSync(p, 'utf-8')) {
|
|
23
|
+
if (platform === 'darwin') {
|
|
24
|
+
return { platform: 'macos' };
|
|
25
|
+
}
|
|
26
|
+
if (platform === 'win32') {
|
|
27
|
+
return { platform: 'windows' };
|
|
28
|
+
}
|
|
29
|
+
if (platform === 'linux') {
|
|
30
|
+
let distro;
|
|
31
|
+
try {
|
|
32
|
+
const release = readFile('/etc/os-release');
|
|
33
|
+
const match = release.match(/^ID=("?)([^"\n]*)\1$/m);
|
|
34
|
+
if (match) {
|
|
35
|
+
distro = match[2];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// ignore
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const version = readFile('/proc/version');
|
|
43
|
+
if (/microsoft/i.test(version)) {
|
|
44
|
+
return { platform: 'wsl', distro };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// ignore
|
|
49
|
+
}
|
|
50
|
+
return { platform: 'linux', distro };
|
|
51
|
+
}
|
|
52
|
+
return { platform: 'unknown' };
|
|
53
|
+
}
|
|
54
|
+
const DEBIAN_LIKE = ['ubuntu', 'debian', 'pop', 'linuxmint', 'elementary'];
|
|
55
|
+
const RHEL_LIKE = ['fedora', 'rhel', 'centos', 'rocky', 'almalinux', 'amzn'];
|
|
56
|
+
function isDebianLike(distro) {
|
|
57
|
+
return !!distro && DEBIAN_LIKE.includes(distro);
|
|
58
|
+
}
|
|
59
|
+
function isRhelLike(distro) {
|
|
60
|
+
return !!distro && RHEL_LIKE.includes(distro);
|
|
61
|
+
}
|
|
62
|
+
export function dockerInstallGuidance(info) {
|
|
63
|
+
switch (info.platform) {
|
|
64
|
+
case 'macos':
|
|
65
|
+
return 'Install Docker Desktop for Mac: https://www.docker.com/products/docker-desktop/ (or `brew install --cask docker`)';
|
|
66
|
+
case 'windows':
|
|
67
|
+
return 'Install Docker Desktop for Windows: https://www.docker.com/products/docker-desktop/';
|
|
68
|
+
case 'wsl':
|
|
69
|
+
return 'Install Docker Desktop for Windows with the WSL 2 backend enabled: https://docs.docker.com/desktop/wsl/';
|
|
70
|
+
case 'linux':
|
|
71
|
+
if (isDebianLike(info.distro)) {
|
|
72
|
+
return 'Install Docker Engine: https://docs.docker.com/engine/install/ubuntu/ (e.g. `sudo apt-get install docker.io` or the official docker-ce packages)';
|
|
73
|
+
}
|
|
74
|
+
if (isRhelLike(info.distro)) {
|
|
75
|
+
return 'Install Docker Engine: https://docs.docker.com/engine/install/fedora/ (e.g. `sudo dnf install docker-ce`)';
|
|
76
|
+
}
|
|
77
|
+
return 'Install Docker Engine for your distribution: https://docs.docker.com/engine/install/';
|
|
78
|
+
default:
|
|
79
|
+
return 'Install Docker: https://docs.docker.com/get-docker/';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
export function dockerDaemonGuidance(info) {
|
|
83
|
+
switch (info.platform) {
|
|
84
|
+
case 'macos':
|
|
85
|
+
case 'windows':
|
|
86
|
+
return 'Docker is installed but the daemon is not running. Open the Docker Desktop application and wait for it to finish starting.';
|
|
87
|
+
case 'wsl':
|
|
88
|
+
return 'Docker is installed but the daemon is not reachable. Start Docker Desktop on Windows and make sure WSL integration is enabled for this distro.';
|
|
89
|
+
case 'linux':
|
|
90
|
+
return 'Docker is installed but the daemon is not running. Start it with `sudo systemctl start docker` (and `sudo systemctl enable docker` to start on boot). If you get a permission error, add your user to the docker group: `sudo usermod -aG docker $USER` and re-login.';
|
|
91
|
+
default:
|
|
92
|
+
return 'Docker is installed but the daemon is not reachable. Start the Docker daemon and try again.';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export function psqlInstallGuidance(info) {
|
|
96
|
+
switch (info.platform) {
|
|
97
|
+
case 'macos':
|
|
98
|
+
return 'Install the PostgreSQL client via Homebrew: `brew install libpq && brew link --force libpq`';
|
|
99
|
+
case 'windows':
|
|
100
|
+
return 'Install the PostgreSQL client tools: `winget install PostgreSQL.PostgreSQL` or download from https://www.postgresql.org/download/windows/';
|
|
101
|
+
case 'wsl':
|
|
102
|
+
case 'linux':
|
|
103
|
+
if (isRhelLike(info.distro)) {
|
|
104
|
+
return 'Install the PostgreSQL client: `sudo dnf install postgresql`';
|
|
105
|
+
}
|
|
106
|
+
return 'Install the PostgreSQL client: `sudo apt-get install postgresql-client`';
|
|
107
|
+
default:
|
|
108
|
+
return 'Install the PostgreSQL client tools (psql): https://www.postgresql.org/download/';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export async function checkDockerBinary(exec = runCommand) {
|
|
112
|
+
const result = await exec('docker', ['--version']);
|
|
113
|
+
return result.code === 0;
|
|
114
|
+
}
|
|
115
|
+
export async function checkDockerDaemon(exec = runCommand) {
|
|
116
|
+
const result = await exec('docker', ['info']);
|
|
117
|
+
return result.code === 0;
|
|
118
|
+
}
|
|
119
|
+
export async function getDockerStatus(exec = runCommand) {
|
|
120
|
+
const binary = await checkDockerBinary(exec);
|
|
121
|
+
if (!binary) {
|
|
122
|
+
return { binary: false, daemon: false };
|
|
123
|
+
}
|
|
124
|
+
const daemon = await checkDockerDaemon(exec);
|
|
125
|
+
return { binary, daemon };
|
|
126
|
+
}
|
|
127
|
+
export async function checkDocker(info, exec = runCommand) {
|
|
128
|
+
const status = await getDockerStatus(exec);
|
|
129
|
+
if (!status.binary) {
|
|
130
|
+
return {
|
|
131
|
+
name: 'docker',
|
|
132
|
+
status: 'fail',
|
|
133
|
+
message: 'docker is not installed or not on PATH',
|
|
134
|
+
remediation: dockerInstallGuidance(info)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
if (!status.daemon) {
|
|
138
|
+
return {
|
|
139
|
+
name: 'docker',
|
|
140
|
+
status: 'fail',
|
|
141
|
+
message: 'docker is installed but the daemon is not reachable',
|
|
142
|
+
remediation: dockerDaemonGuidance(info)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
name: 'docker',
|
|
147
|
+
status: 'pass',
|
|
148
|
+
message: 'docker is installed and the daemon is running'
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export async function checkDockerCompose(info, exec = runCommand) {
|
|
152
|
+
const plugin = await exec('docker', ['compose', 'version']);
|
|
153
|
+
if (plugin.code === 0) {
|
|
154
|
+
return {
|
|
155
|
+
name: 'docker-compose',
|
|
156
|
+
status: 'pass',
|
|
157
|
+
message: 'docker compose plugin is available'
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const standalone = await exec('docker-compose', ['--version']);
|
|
161
|
+
if (standalone.code === 0) {
|
|
162
|
+
return {
|
|
163
|
+
name: 'docker-compose',
|
|
164
|
+
status: 'pass',
|
|
165
|
+
message: 'docker-compose (standalone) is available'
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
name: 'docker-compose',
|
|
170
|
+
status: 'warn',
|
|
171
|
+
message: 'docker compose is not available (only needed for docker-compose based workflows)',
|
|
172
|
+
remediation: info.platform === 'linux' || info.platform === 'wsl'
|
|
173
|
+
? 'Install the compose plugin: https://docs.docker.com/compose/install/linux/'
|
|
174
|
+
: 'Docker Desktop ships with compose; install or update Docker Desktop: https://www.docker.com/products/docker-desktop/'
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
export const MIN_PSQL_MAJOR = 17;
|
|
178
|
+
export function parsePsqlMajor(versionOutput) {
|
|
179
|
+
const match = versionOutput.match(/\(PostgreSQL\)\s+(\d+)/) || versionOutput.match(/psql[^\d]*(\d+)/);
|
|
180
|
+
if (!match)
|
|
181
|
+
return null;
|
|
182
|
+
const major = parseInt(match[1], 10);
|
|
183
|
+
return Number.isNaN(major) ? null : major;
|
|
184
|
+
}
|
|
185
|
+
export async function checkPsql(info, exec = runCommand) {
|
|
186
|
+
const result = await exec('psql', ['--version']);
|
|
187
|
+
if (result.code !== 0) {
|
|
188
|
+
return {
|
|
189
|
+
name: 'psql',
|
|
190
|
+
status: 'fail',
|
|
191
|
+
message: 'psql (PostgreSQL client) is not installed or not on PATH',
|
|
192
|
+
remediation: psqlInstallGuidance(info)
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const major = parsePsqlMajor(result.stdout);
|
|
196
|
+
if (major === null) {
|
|
197
|
+
return {
|
|
198
|
+
name: 'psql',
|
|
199
|
+
status: 'warn',
|
|
200
|
+
message: `could not determine psql version from: ${result.stdout}`
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
if (major < MIN_PSQL_MAJOR) {
|
|
204
|
+
return {
|
|
205
|
+
name: 'psql',
|
|
206
|
+
status: 'warn',
|
|
207
|
+
message: `psql ${major} found; PostgreSQL ${MIN_PSQL_MAJOR}+ client is recommended`,
|
|
208
|
+
remediation: psqlInstallGuidance(info)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
name: 'psql',
|
|
213
|
+
status: 'pass',
|
|
214
|
+
message: `psql ${major} is installed`
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export const MIN_NODE_MAJOR = 18;
|
|
218
|
+
export const RECOMMENDED_NODE_MAJOR = 20;
|
|
219
|
+
export function checkNode(version = process.version) {
|
|
220
|
+
const major = parseInt(version.replace(/^v/, '').split('.')[0], 10);
|
|
221
|
+
if (Number.isNaN(major)) {
|
|
222
|
+
return { name: 'node', status: 'warn', message: `could not parse Node.js version: ${version}` };
|
|
223
|
+
}
|
|
224
|
+
if (major < MIN_NODE_MAJOR) {
|
|
225
|
+
return {
|
|
226
|
+
name: 'node',
|
|
227
|
+
status: 'fail',
|
|
228
|
+
message: `Node.js ${version} is not supported; Node.js ${MIN_NODE_MAJOR}+ is required`,
|
|
229
|
+
remediation: 'Upgrade Node.js (e.g. via nvm: `nvm install --lts`): https://nodejs.org/'
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (major < RECOMMENDED_NODE_MAJOR) {
|
|
233
|
+
return {
|
|
234
|
+
name: 'node',
|
|
235
|
+
status: 'warn',
|
|
236
|
+
message: `Node.js ${version} works but ${RECOMMENDED_NODE_MAJOR}+ is recommended`,
|
|
237
|
+
remediation: 'Upgrade Node.js (e.g. via nvm: `nvm install --lts`): https://nodejs.org/'
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return { name: 'node', status: 'pass', message: `Node.js ${version} is installed` };
|
|
241
|
+
}
|
|
242
|
+
export function summarizeChecks(results) {
|
|
243
|
+
return {
|
|
244
|
+
failed: results.filter((r) => r.status === 'fail').length,
|
|
245
|
+
warned: results.filter((r) => r.status === 'warn').length,
|
|
246
|
+
passed: results.filter((r) => r.status === 'pass').length
|
|
247
|
+
};
|
|
248
|
+
}
|
package/esm/utils/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export * from './database';
|
|
2
|
-
export * from './driver';
|
|
3
|
-
export * from './display';
|
|
4
2
|
export * from './deployed-changes';
|
|
3
|
+
export * from './display';
|
|
4
|
+
export * from './doctor';
|
|
5
|
+
export * from './driver';
|
|
5
6
|
export * from './module-utils';
|
|
6
7
|
export * from './npm-version';
|
|
7
8
|
export * from './package-alias';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pgpm",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"author": "Constructive <developers@constructive.io>",
|
|
5
5
|
"description": "PostgreSQL Package Manager - Database migration and package management CLI",
|
|
6
6
|
"main": "index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@inquirerer/utils": "^3.3.9",
|
|
49
49
|
"@pgpmjs/core": "^7.4.2",
|
|
50
50
|
"@pgpmjs/env": "^2.30.2",
|
|
51
|
-
"@pgpmjs/export": "^1.
|
|
51
|
+
"@pgpmjs/export": "^1.5.0",
|
|
52
52
|
"@pgpmjs/logger": "^2.15.2",
|
|
53
53
|
"@pgpmjs/types": "^2.37.2",
|
|
54
54
|
"@pgsql/quotes": "^18.1.0",
|
|
@@ -76,5 +76,5 @@
|
|
|
76
76
|
"pg",
|
|
77
77
|
"pgsql"
|
|
78
78
|
],
|
|
79
|
-
"gitHead": "
|
|
79
|
+
"gitHead": "518e7ab2270661c64875258436d90fdf76b4b336"
|
|
80
80
|
}
|
package/utils/display.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const usageText = "\n Usage: pgpm <command> [options]\n\n Core Database Operations:\n add Add database changes to plans and create SQL files\n deploy Deploy database changes and migrations\n verify Verify database state and migrations\n revert Revert database changes and migrations\n\n Project Management:\n init Initialize workspace or module\n extension Manage module dependencies\n plan Generate module deployment plans\n package Package module for distribution\n export Export database migrations from existing databases\n update Update pgpm to the latest version\n cache Manage cached templates (clean)\n upgrade Upgrade installed pgpm modules to latest versions (alias: up)\n\n Database Administration:\n dump Dump a database to a sql file\n kill Terminate database connections and optionally drop databases\n install Install database modules\n tag Add tags to changes for versioning\n clear Clear database state\n remove Remove database changes\n analyze Analyze database structure\n rename Rename database changes\n admin-users Manage admin users\n tune Tune PostgreSQL for throwaway environments (CI/test)\n\n Testing:\n test-packages Run integration tests on all workspace packages\n\n Migration Tools:\n migrate Migration management subcommands\n init Initialize migration tracking\n status Show migration status\n list List all changes\n deps Show change dependencies\n \n Development Tools:\n docker Manage Docker containers (start/stop/ls, --minio)\n env Manage environment variables (--supabase, --minio)\n test-packages Run integration tests on workspace packages\n \n Global Options:\n -h, --help Display this help information\n -v, --version Display version information\n --cwd <directory> Working directory (default: current directory)\n\n Individual Command Help:\n pgpm <command> --help Display detailed help for specific command\n pgpm <command> -h Display detailed help for specific command\n\n Examples:\n pgpm deploy --help Show deploy command options\n pgpm init workspace Initialize new workspace\n pgpm install @pgpm/base32 Install a database module\n ";
|
|
1
|
+
export declare const usageText = "\n Usage: pgpm <command> [options]\n\n Core Database Operations:\n add Add database changes to plans and create SQL files\n deploy Deploy database changes and migrations\n verify Verify database state and migrations\n revert Revert database changes and migrations\n\n Project Management:\n init Initialize workspace or module\n extension Manage module dependencies\n plan Generate module deployment plans\n package Package module for distribution\n export Export database migrations from existing databases\n update Update pgpm to the latest version\n cache Manage cached templates (clean)\n upgrade Upgrade installed pgpm modules to latest versions (alias: up)\n\n Database Administration:\n dump Dump a database to a sql file\n kill Terminate database connections and optionally drop databases\n install Install database modules\n tag Add tags to changes for versioning\n clear Clear database state\n remove Remove database changes\n analyze Analyze database structure\n rename Rename database changes\n admin-users Manage admin users\n tune Tune PostgreSQL for throwaway environments (CI/test)\n\n Testing:\n test-packages Run integration tests on all workspace packages\n\n Migration Tools:\n migrate Migration management subcommands\n init Initialize migration tracking\n status Show migration status\n list List all changes\n deps Show change dependencies\n \n Development Tools:\n docker Manage Docker containers (start/stop/ls, --minio)\n doctor Check local dependencies (node, docker, psql) with install guidance\n env Manage environment variables (--supabase, --minio)\n test-packages Run integration tests on workspace packages\n \n Global Options:\n -h, --help Display this help information\n -v, --version Display version information\n --cwd <directory> Working directory (default: current directory)\n\n Individual Command Help:\n pgpm <command> --help Display detailed help for specific command\n pgpm <command> -h Display detailed help for specific command\n\n Examples:\n pgpm deploy --help Show deploy command options\n pgpm init workspace Initialize new workspace\n pgpm install @pgpm/base32 Install a database module\n ";
|
package/utils/display.js
CHANGED
|
@@ -44,6 +44,7 @@ exports.usageText = `
|
|
|
44
44
|
|
|
45
45
|
Development Tools:
|
|
46
46
|
docker Manage Docker containers (start/stop/ls, --minio)
|
|
47
|
+
doctor Check local dependencies (node, docker, psql) with install guidance
|
|
47
48
|
env Manage environment variables (--supabase, --minio)
|
|
48
49
|
test-packages Run integration tests on workspace packages
|
|
49
50
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type Platform = 'macos' | 'linux' | 'wsl' | 'windows' | 'unknown';
|
|
2
|
+
export interface PlatformInfo {
|
|
3
|
+
platform: Platform;
|
|
4
|
+
distro?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ExecResult {
|
|
7
|
+
code: number;
|
|
8
|
+
stdout: string;
|
|
9
|
+
stderr: string;
|
|
10
|
+
}
|
|
11
|
+
export type CommandRunner = (command: string, args: string[]) => Promise<ExecResult>;
|
|
12
|
+
export type CheckStatus = 'pass' | 'warn' | 'fail';
|
|
13
|
+
export interface CheckResult {
|
|
14
|
+
name: string;
|
|
15
|
+
status: CheckStatus;
|
|
16
|
+
message: string;
|
|
17
|
+
remediation?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const runCommand: CommandRunner;
|
|
20
|
+
export declare function detectPlatform(platform?: NodeJS.Platform, readFile?: (path: string) => string): PlatformInfo;
|
|
21
|
+
export declare function dockerInstallGuidance(info: PlatformInfo): string;
|
|
22
|
+
export declare function dockerDaemonGuidance(info: PlatformInfo): string;
|
|
23
|
+
export declare function psqlInstallGuidance(info: PlatformInfo): string;
|
|
24
|
+
export declare function checkDockerBinary(exec?: CommandRunner): Promise<boolean>;
|
|
25
|
+
export declare function checkDockerDaemon(exec?: CommandRunner): Promise<boolean>;
|
|
26
|
+
export interface DockerStatus {
|
|
27
|
+
binary: boolean;
|
|
28
|
+
daemon: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function getDockerStatus(exec?: CommandRunner): Promise<DockerStatus>;
|
|
31
|
+
export declare function checkDocker(info: PlatformInfo, exec?: CommandRunner): Promise<CheckResult>;
|
|
32
|
+
export declare function checkDockerCompose(info: PlatformInfo, exec?: CommandRunner): Promise<CheckResult>;
|
|
33
|
+
export declare const MIN_PSQL_MAJOR = 17;
|
|
34
|
+
export declare function parsePsqlMajor(versionOutput: string): number | null;
|
|
35
|
+
export declare function checkPsql(info: PlatformInfo, exec?: CommandRunner): Promise<CheckResult>;
|
|
36
|
+
export declare const MIN_NODE_MAJOR = 18;
|
|
37
|
+
export declare const RECOMMENDED_NODE_MAJOR = 20;
|
|
38
|
+
export declare function checkNode(version?: string): CheckResult;
|
|
39
|
+
export declare function summarizeChecks(results: CheckResult[]): {
|
|
40
|
+
failed: number;
|
|
41
|
+
warned: number;
|
|
42
|
+
passed: number;
|
|
43
|
+
};
|
package/utils/doctor.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RECOMMENDED_NODE_MAJOR = exports.MIN_NODE_MAJOR = exports.MIN_PSQL_MAJOR = exports.runCommand = void 0;
|
|
4
|
+
exports.detectPlatform = detectPlatform;
|
|
5
|
+
exports.dockerInstallGuidance = dockerInstallGuidance;
|
|
6
|
+
exports.dockerDaemonGuidance = dockerDaemonGuidance;
|
|
7
|
+
exports.psqlInstallGuidance = psqlInstallGuidance;
|
|
8
|
+
exports.checkDockerBinary = checkDockerBinary;
|
|
9
|
+
exports.checkDockerDaemon = checkDockerDaemon;
|
|
10
|
+
exports.getDockerStatus = getDockerStatus;
|
|
11
|
+
exports.checkDocker = checkDocker;
|
|
12
|
+
exports.checkDockerCompose = checkDockerCompose;
|
|
13
|
+
exports.parsePsqlMajor = parsePsqlMajor;
|
|
14
|
+
exports.checkPsql = checkPsql;
|
|
15
|
+
exports.checkNode = checkNode;
|
|
16
|
+
exports.summarizeChecks = summarizeChecks;
|
|
17
|
+
const child_process_1 = require("child_process");
|
|
18
|
+
const fs_1 = require("fs");
|
|
19
|
+
const runCommand = (command, args) => {
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
const child = (0, child_process_1.spawn)(command, args, { stdio: 'pipe' });
|
|
22
|
+
let stdout = '';
|
|
23
|
+
let stderr = '';
|
|
24
|
+
child.stdout?.on('data', (data) => {
|
|
25
|
+
stdout += data.toString();
|
|
26
|
+
});
|
|
27
|
+
child.stderr?.on('data', (data) => {
|
|
28
|
+
stderr += data.toString();
|
|
29
|
+
});
|
|
30
|
+
child.on('error', () => {
|
|
31
|
+
resolve({ code: 127, stdout: '', stderr: `${command}: command not found` });
|
|
32
|
+
});
|
|
33
|
+
child.on('close', (code) => {
|
|
34
|
+
resolve({ code: code ?? 0, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
exports.runCommand = runCommand;
|
|
39
|
+
function detectPlatform(platform = process.platform, readFile = (p) => (0, fs_1.readFileSync)(p, 'utf-8')) {
|
|
40
|
+
if (platform === 'darwin') {
|
|
41
|
+
return { platform: 'macos' };
|
|
42
|
+
}
|
|
43
|
+
if (platform === 'win32') {
|
|
44
|
+
return { platform: 'windows' };
|
|
45
|
+
}
|
|
46
|
+
if (platform === 'linux') {
|
|
47
|
+
let distro;
|
|
48
|
+
try {
|
|
49
|
+
const release = readFile('/etc/os-release');
|
|
50
|
+
const match = release.match(/^ID=("?)([^"\n]*)\1$/m);
|
|
51
|
+
if (match) {
|
|
52
|
+
distro = match[2];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// ignore
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const version = readFile('/proc/version');
|
|
60
|
+
if (/microsoft/i.test(version)) {
|
|
61
|
+
return { platform: 'wsl', distro };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// ignore
|
|
66
|
+
}
|
|
67
|
+
return { platform: 'linux', distro };
|
|
68
|
+
}
|
|
69
|
+
return { platform: 'unknown' };
|
|
70
|
+
}
|
|
71
|
+
const DEBIAN_LIKE = ['ubuntu', 'debian', 'pop', 'linuxmint', 'elementary'];
|
|
72
|
+
const RHEL_LIKE = ['fedora', 'rhel', 'centos', 'rocky', 'almalinux', 'amzn'];
|
|
73
|
+
function isDebianLike(distro) {
|
|
74
|
+
return !!distro && DEBIAN_LIKE.includes(distro);
|
|
75
|
+
}
|
|
76
|
+
function isRhelLike(distro) {
|
|
77
|
+
return !!distro && RHEL_LIKE.includes(distro);
|
|
78
|
+
}
|
|
79
|
+
function dockerInstallGuidance(info) {
|
|
80
|
+
switch (info.platform) {
|
|
81
|
+
case 'macos':
|
|
82
|
+
return 'Install Docker Desktop for Mac: https://www.docker.com/products/docker-desktop/ (or `brew install --cask docker`)';
|
|
83
|
+
case 'windows':
|
|
84
|
+
return 'Install Docker Desktop for Windows: https://www.docker.com/products/docker-desktop/';
|
|
85
|
+
case 'wsl':
|
|
86
|
+
return 'Install Docker Desktop for Windows with the WSL 2 backend enabled: https://docs.docker.com/desktop/wsl/';
|
|
87
|
+
case 'linux':
|
|
88
|
+
if (isDebianLike(info.distro)) {
|
|
89
|
+
return 'Install Docker Engine: https://docs.docker.com/engine/install/ubuntu/ (e.g. `sudo apt-get install docker.io` or the official docker-ce packages)';
|
|
90
|
+
}
|
|
91
|
+
if (isRhelLike(info.distro)) {
|
|
92
|
+
return 'Install Docker Engine: https://docs.docker.com/engine/install/fedora/ (e.g. `sudo dnf install docker-ce`)';
|
|
93
|
+
}
|
|
94
|
+
return 'Install Docker Engine for your distribution: https://docs.docker.com/engine/install/';
|
|
95
|
+
default:
|
|
96
|
+
return 'Install Docker: https://docs.docker.com/get-docker/';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function dockerDaemonGuidance(info) {
|
|
100
|
+
switch (info.platform) {
|
|
101
|
+
case 'macos':
|
|
102
|
+
case 'windows':
|
|
103
|
+
return 'Docker is installed but the daemon is not running. Open the Docker Desktop application and wait for it to finish starting.';
|
|
104
|
+
case 'wsl':
|
|
105
|
+
return 'Docker is installed but the daemon is not reachable. Start Docker Desktop on Windows and make sure WSL integration is enabled for this distro.';
|
|
106
|
+
case 'linux':
|
|
107
|
+
return 'Docker is installed but the daemon is not running. Start it with `sudo systemctl start docker` (and `sudo systemctl enable docker` to start on boot). If you get a permission error, add your user to the docker group: `sudo usermod -aG docker $USER` and re-login.';
|
|
108
|
+
default:
|
|
109
|
+
return 'Docker is installed but the daemon is not reachable. Start the Docker daemon and try again.';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function psqlInstallGuidance(info) {
|
|
113
|
+
switch (info.platform) {
|
|
114
|
+
case 'macos':
|
|
115
|
+
return 'Install the PostgreSQL client via Homebrew: `brew install libpq && brew link --force libpq`';
|
|
116
|
+
case 'windows':
|
|
117
|
+
return 'Install the PostgreSQL client tools: `winget install PostgreSQL.PostgreSQL` or download from https://www.postgresql.org/download/windows/';
|
|
118
|
+
case 'wsl':
|
|
119
|
+
case 'linux':
|
|
120
|
+
if (isRhelLike(info.distro)) {
|
|
121
|
+
return 'Install the PostgreSQL client: `sudo dnf install postgresql`';
|
|
122
|
+
}
|
|
123
|
+
return 'Install the PostgreSQL client: `sudo apt-get install postgresql-client`';
|
|
124
|
+
default:
|
|
125
|
+
return 'Install the PostgreSQL client tools (psql): https://www.postgresql.org/download/';
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async function checkDockerBinary(exec = exports.runCommand) {
|
|
129
|
+
const result = await exec('docker', ['--version']);
|
|
130
|
+
return result.code === 0;
|
|
131
|
+
}
|
|
132
|
+
async function checkDockerDaemon(exec = exports.runCommand) {
|
|
133
|
+
const result = await exec('docker', ['info']);
|
|
134
|
+
return result.code === 0;
|
|
135
|
+
}
|
|
136
|
+
async function getDockerStatus(exec = exports.runCommand) {
|
|
137
|
+
const binary = await checkDockerBinary(exec);
|
|
138
|
+
if (!binary) {
|
|
139
|
+
return { binary: false, daemon: false };
|
|
140
|
+
}
|
|
141
|
+
const daemon = await checkDockerDaemon(exec);
|
|
142
|
+
return { binary, daemon };
|
|
143
|
+
}
|
|
144
|
+
async function checkDocker(info, exec = exports.runCommand) {
|
|
145
|
+
const status = await getDockerStatus(exec);
|
|
146
|
+
if (!status.binary) {
|
|
147
|
+
return {
|
|
148
|
+
name: 'docker',
|
|
149
|
+
status: 'fail',
|
|
150
|
+
message: 'docker is not installed or not on PATH',
|
|
151
|
+
remediation: dockerInstallGuidance(info)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (!status.daemon) {
|
|
155
|
+
return {
|
|
156
|
+
name: 'docker',
|
|
157
|
+
status: 'fail',
|
|
158
|
+
message: 'docker is installed but the daemon is not reachable',
|
|
159
|
+
remediation: dockerDaemonGuidance(info)
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
name: 'docker',
|
|
164
|
+
status: 'pass',
|
|
165
|
+
message: 'docker is installed and the daemon is running'
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
async function checkDockerCompose(info, exec = exports.runCommand) {
|
|
169
|
+
const plugin = await exec('docker', ['compose', 'version']);
|
|
170
|
+
if (plugin.code === 0) {
|
|
171
|
+
return {
|
|
172
|
+
name: 'docker-compose',
|
|
173
|
+
status: 'pass',
|
|
174
|
+
message: 'docker compose plugin is available'
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const standalone = await exec('docker-compose', ['--version']);
|
|
178
|
+
if (standalone.code === 0) {
|
|
179
|
+
return {
|
|
180
|
+
name: 'docker-compose',
|
|
181
|
+
status: 'pass',
|
|
182
|
+
message: 'docker-compose (standalone) is available'
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
name: 'docker-compose',
|
|
187
|
+
status: 'warn',
|
|
188
|
+
message: 'docker compose is not available (only needed for docker-compose based workflows)',
|
|
189
|
+
remediation: info.platform === 'linux' || info.platform === 'wsl'
|
|
190
|
+
? 'Install the compose plugin: https://docs.docker.com/compose/install/linux/'
|
|
191
|
+
: 'Docker Desktop ships with compose; install or update Docker Desktop: https://www.docker.com/products/docker-desktop/'
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
exports.MIN_PSQL_MAJOR = 17;
|
|
195
|
+
function parsePsqlMajor(versionOutput) {
|
|
196
|
+
const match = versionOutput.match(/\(PostgreSQL\)\s+(\d+)/) || versionOutput.match(/psql[^\d]*(\d+)/);
|
|
197
|
+
if (!match)
|
|
198
|
+
return null;
|
|
199
|
+
const major = parseInt(match[1], 10);
|
|
200
|
+
return Number.isNaN(major) ? null : major;
|
|
201
|
+
}
|
|
202
|
+
async function checkPsql(info, exec = exports.runCommand) {
|
|
203
|
+
const result = await exec('psql', ['--version']);
|
|
204
|
+
if (result.code !== 0) {
|
|
205
|
+
return {
|
|
206
|
+
name: 'psql',
|
|
207
|
+
status: 'fail',
|
|
208
|
+
message: 'psql (PostgreSQL client) is not installed or not on PATH',
|
|
209
|
+
remediation: psqlInstallGuidance(info)
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
const major = parsePsqlMajor(result.stdout);
|
|
213
|
+
if (major === null) {
|
|
214
|
+
return {
|
|
215
|
+
name: 'psql',
|
|
216
|
+
status: 'warn',
|
|
217
|
+
message: `could not determine psql version from: ${result.stdout}`
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (major < exports.MIN_PSQL_MAJOR) {
|
|
221
|
+
return {
|
|
222
|
+
name: 'psql',
|
|
223
|
+
status: 'warn',
|
|
224
|
+
message: `psql ${major} found; PostgreSQL ${exports.MIN_PSQL_MAJOR}+ client is recommended`,
|
|
225
|
+
remediation: psqlInstallGuidance(info)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
name: 'psql',
|
|
230
|
+
status: 'pass',
|
|
231
|
+
message: `psql ${major} is installed`
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
exports.MIN_NODE_MAJOR = 18;
|
|
235
|
+
exports.RECOMMENDED_NODE_MAJOR = 20;
|
|
236
|
+
function checkNode(version = process.version) {
|
|
237
|
+
const major = parseInt(version.replace(/^v/, '').split('.')[0], 10);
|
|
238
|
+
if (Number.isNaN(major)) {
|
|
239
|
+
return { name: 'node', status: 'warn', message: `could not parse Node.js version: ${version}` };
|
|
240
|
+
}
|
|
241
|
+
if (major < exports.MIN_NODE_MAJOR) {
|
|
242
|
+
return {
|
|
243
|
+
name: 'node',
|
|
244
|
+
status: 'fail',
|
|
245
|
+
message: `Node.js ${version} is not supported; Node.js ${exports.MIN_NODE_MAJOR}+ is required`,
|
|
246
|
+
remediation: 'Upgrade Node.js (e.g. via nvm: `nvm install --lts`): https://nodejs.org/'
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (major < exports.RECOMMENDED_NODE_MAJOR) {
|
|
250
|
+
return {
|
|
251
|
+
name: 'node',
|
|
252
|
+
status: 'warn',
|
|
253
|
+
message: `Node.js ${version} works but ${exports.RECOMMENDED_NODE_MAJOR}+ is recommended`,
|
|
254
|
+
remediation: 'Upgrade Node.js (e.g. via nvm: `nvm install --lts`): https://nodejs.org/'
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return { name: 'node', status: 'pass', message: `Node.js ${version} is installed` };
|
|
258
|
+
}
|
|
259
|
+
function summarizeChecks(results) {
|
|
260
|
+
return {
|
|
261
|
+
failed: results.filter((r) => r.status === 'fail').length,
|
|
262
|
+
warned: results.filter((r) => r.status === 'warn').length,
|
|
263
|
+
passed: results.filter((r) => r.status === 'pass').length
|
|
264
|
+
};
|
|
265
|
+
}
|
package/utils/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export * from './database';
|
|
2
|
-
export * from './driver';
|
|
3
|
-
export * from './display';
|
|
4
2
|
export * from './deployed-changes';
|
|
3
|
+
export * from './display';
|
|
4
|
+
export * from './doctor';
|
|
5
|
+
export * from './driver';
|
|
5
6
|
export * from './module-utils';
|
|
6
7
|
export * from './npm-version';
|
|
7
8
|
export * from './package-alias';
|
package/utils/index.js
CHANGED
|
@@ -15,9 +15,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./database"), exports);
|
|
18
|
-
__exportStar(require("./driver"), exports);
|
|
19
|
-
__exportStar(require("./display"), exports);
|
|
20
18
|
__exportStar(require("./deployed-changes"), exports);
|
|
19
|
+
__exportStar(require("./display"), exports);
|
|
20
|
+
__exportStar(require("./doctor"), exports);
|
|
21
|
+
__exportStar(require("./driver"), exports);
|
|
21
22
|
__exportStar(require("./module-utils"), exports);
|
|
22
23
|
__exportStar(require("./npm-version"), exports);
|
|
23
24
|
__exportStar(require("./package-alias"), exports);
|